Compare commits

...

10 Commits
master ... v4.7

Author SHA1 Message Date
Steven Silvester
c36bfbc3f9
PYTHON-4373 [v4.7] Add SBOM file (#1658) 2024-06-05 14:52:57 -05:00
Steven Silvester
7604387148
BUMP 4.7.4.dev0 2024-06-04 13:44:12 -05:00
Steven Silvester
398be02d23
BUMP 4.7.3 2024-06-04 13:43:20 -05:00
Shane Harvey
839904f4f9
PYTHON-4261 [v4.7] Reduce verbosity of "Waiting for suitable server to become available" log message (#1657) 2024-06-04 11:02:50 -07:00
Steven Silvester
c7db2c215f
PYTHON-4384 [v4.7] Add Custom CodeQL Scanning (#1655) 2024-06-03 13:18:31 -05:00
Steven Silvester
358a4864c1
PYTHON-4441 [v4.7] Use deferred imports instead of lazy module loading (#1650) 2024-06-03 12:49:10 -05:00
Shane Harvey
fa80968120
PYTHON-4473 Optimize find/aggregate/command by avoiding duplicate calls to _get_topology (#1652) (#1654) 2024-05-31 18:03:00 -07:00
Steven Silvester
d4592b659f
PYTHON-4455 [v4.7] Improve import time on Windows (#1647) 2024-05-30 15:46:38 -05:00
Steven Silvester
f38dfd672e
BUMP 4.7.3.dev0 2024-05-07 15:22:24 -05:00
Steven Silvester
29f4d5cf89
PYTHON-4429 Prep for 4.7.2 Release (#1633) 2024-05-07 15:19:26 -05:00
28 changed files with 237 additions and 148 deletions

62
.github/workflows/codeql.yml vendored Normal file
View File

@ -0,0 +1,62 @@
name: "CodeQL"
on:
push:
branches: [ "master", "v*"]
tags: ['*']
pull_request:
schedule:
- cron: '17 10 * * 2'
concurrency:
group: codeql-${{ github.ref }}
cancel-in-progress: true
jobs:
analyze:
name: Analyze (${{ matrix.language }})
runs-on: "ubuntu-latest"
timeout-minutes: 360
permissions:
# required for all workflows
security-events: write
# required to fetch internal or private CodeQL packs
packages: read
strategy:
fail-fast: false
matrix:
include:
- language: c-cpp
build-mode: manual
- language: python
build-mode: none
steps:
- name: Checkout repository
uses: actions/checkout@v4
- uses: actions/setup-python@v3
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
# For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
queries: security-extended
config: |
paths-ignore:
- '.github/**'
- 'doc/**'
- 'tools/**'
- 'test/**'
- if: matrix.build-mode == 'manual'
run: |
pip install -e .
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
with:
category: "/language:${{matrix.language}}"

View File

@ -53,7 +53,7 @@ jobs:
strategy: strategy:
matrix: matrix:
os: [ubuntu-20.04] os: [ubuntu-20.04]
python-version: ["3.7", "3.11", "pypy-3.8"] python-version: ["3.7", "3.11", "pypy-3.9"]
name: CPython ${{ matrix.python-version }}-${{ matrix.os }} name: CPython ${{ matrix.python-version }}-${{ matrix.os }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
@ -137,7 +137,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
python: ["3.7", "3.11"] python: ["3.8", "3.11"]
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-python@v5 - uses: actions/setup-python@v5

View File

@ -8,6 +8,7 @@ exclude .git-blame-ignore-revs
exclude .pre-commit-config.yaml exclude .pre-commit-config.yaml
exclude .readthedocs.yaml exclude .readthedocs.yaml
exclude CONTRIBUTING.md exclude CONTRIBUTING.md
include sbom.json
exclude RELEASE.md exclude RELEASE.md
recursive-include doc *.rst recursive-include doc *.rst
recursive-include doc *.py recursive-include doc *.py

View File

@ -1,6 +1,39 @@
Changelog Changelog
========= =========
Changes in Version 4.7.3
-------------------------
Version 4.7.3 has further fixes for lazily loading modules.
- Use deferred imports instead of importlib lazy module loading.
- Improve import time on Windows.
- Reduce verbosity of "Waiting for suitable server to become available" log message from info to debug.
Issues Resolved
...............
See the `PyMongo 4.7.3 release notes in JIRA`_ for the list of resolved issues
in this release.
.. _PyMongo 4.7.3 release notes in JIRA: https://jira.mongodb.org/secure/ReleaseNote.jspa?projectId=10004&version=39865
Changes in Version 4.7.2
-------------------------
Version 4.7.2 fixes a bug introduced in 4.7.0:
- Fixed a bug where PyMongo could not be used with the Nuitka compiler.
Issues Resolved
...............
See the `PyMongo 4.7.2 release notes in JIRA`_ for the list of resolved issues
in this release.
.. _PyMongo 4.7.2 release notes in JIRA: https://jira.mongodb.org/secure/ReleaseNote.jspa?projectId=10004&version=39710
Changes in Version 4.7.1 Changes in Version 4.7.1
------------------------- -------------------------

View File

@ -16,10 +16,11 @@
from __future__ import annotations from __future__ import annotations
from typing import Any from typing import Any
from urllib.request import Request, urlopen
def _get_gcp_response(resource: str, timeout: float = 5) -> dict[str, Any]: def _get_gcp_response(resource: str, timeout: float = 5) -> dict[str, Any]:
from urllib.request import Request, urlopen
url = "http://metadata/computeMetadata/v1/instance/service-accounts/default/identity" url = "http://metadata/computeMetadata/v1/instance/service-accounts/default/identity"
url += f"?audience={resource}" url += f"?audience={resource}"
headers = {"Metadata-Flavor": "Google"} headers = {"Metadata-Flavor": "Google"}

View File

@ -1,43 +0,0 @@
# Copyright 2024-present MongoDB, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you
# may not use this file except in compliance with the License. You
# may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied. See the License for the specific language governing
# permissions and limitations under the License.
from __future__ import annotations
import importlib.util
import sys
from types import ModuleType
def lazy_import(name: str) -> ModuleType:
"""Lazily import a module by name
From https://docs.python.org/3/library/importlib.html#implementing-lazy-imports
"""
# Workaround for PYTHON-4424.
if "__compiled__" in globals():
return importlib.import_module(name)
try:
spec = importlib.util.find_spec(name)
except ValueError:
# Note: this cannot be ModuleNotFoundError, see PYTHON-4424.
raise ImportError(name=name) from None
if spec is None:
# Note: this cannot be ModuleNotFoundError, see PYTHON-4424.
raise ImportError(name=name)
assert spec is not None
loader = importlib.util.LazyLoader(spec.loader) # type:ignore[arg-type]
spec.loader = loader
module = importlib.util.module_from_spec(spec)
sys.modules[name] = module
loader.exec_module(module)
return module

View File

@ -17,7 +17,7 @@ from __future__ import annotations
from typing import Tuple, Union from typing import Tuple, Union
version_tuple: Tuple[Union[int, str], ...] = (4, 8, 0, ".dev0") version_tuple: Tuple[Union[int, str], ...] = (4, 7, 4, ".dev0")
def get_version_string() -> str: def get_version_string() -> str:

View File

@ -15,15 +15,6 @@
"""MONGODB-AWS Authentication helpers.""" """MONGODB-AWS Authentication helpers."""
from __future__ import annotations from __future__ import annotations
from pymongo._lazy_import import lazy_import
try:
pymongo_auth_aws = lazy_import("pymongo_auth_aws")
_HAVE_MONGODB_AWS = True
except ImportError:
_HAVE_MONGODB_AWS = False
from typing import TYPE_CHECKING, Any, Mapping, Type from typing import TYPE_CHECKING, Any, Mapping, Type
import bson import bson
@ -38,11 +29,13 @@ if TYPE_CHECKING:
def _authenticate_aws(credentials: MongoCredential, conn: Connection) -> None: def _authenticate_aws(credentials: MongoCredential, conn: Connection) -> None:
"""Authenticate using MONGODB-AWS.""" """Authenticate using MONGODB-AWS."""
if not _HAVE_MONGODB_AWS: try:
import pymongo_auth_aws # type:ignore[import]
except ImportError as e:
raise ConfigurationError( raise ConfigurationError(
"MONGODB-AWS authentication requires pymongo-auth-aws: " "MONGODB-AWS authentication requires pymongo-auth-aws: "
"install with: python -m pip install 'pymongo[aws]'" "install with: python -m pip install 'pymongo[aws]'"
) ) from e
# Delayed import. # Delayed import.
from pymongo_auth_aws.auth import ( # type:ignore[import] from pymongo_auth_aws.auth import ( # type:ignore[import]

View File

@ -19,7 +19,6 @@ from typing import TYPE_CHECKING, Any, Mapping, Optional, Sequence, cast
from bson.codec_options import _parse_codec_options from bson.codec_options import _parse_codec_options
from pymongo import common from pymongo import common
from pymongo.auth import MongoCredential, _build_credentials_tuple
from pymongo.compression_support import CompressionSettings from pymongo.compression_support import CompressionSettings
from pymongo.errors import ConfigurationError from pymongo.errors import ConfigurationError
from pymongo.monitoring import _EventListener, _EventListeners from pymongo.monitoring import _EventListener, _EventListeners
@ -36,6 +35,7 @@ from pymongo.write_concern import WriteConcern, validate_boolean
if TYPE_CHECKING: if TYPE_CHECKING:
from bson.codec_options import CodecOptions from bson.codec_options import CodecOptions
from pymongo.auth import MongoCredential
from pymongo.encryption_options import AutoEncryptionOpts from pymongo.encryption_options import AutoEncryptionOpts
from pymongo.pyopenssl_context import SSLContext from pymongo.pyopenssl_context import SSLContext
from pymongo.topology_description import _ServerSelector from pymongo.topology_description import _ServerSelector
@ -48,6 +48,8 @@ def _parse_credentials(
mechanism = options.get("authmechanism", "DEFAULT" if username else None) mechanism = options.get("authmechanism", "DEFAULT" if username else None)
source = options.get("authsource") source = options.get("authsource")
if username or mechanism: if username or mechanism:
from pymongo.auth import _build_credentials_tuple
return _build_credentials_tuple(mechanism, source, username, password, options, database) return _build_credentials_tuple(mechanism, source, username, password, options, database)
return None return None

View File

@ -40,8 +40,6 @@ from bson import SON
from bson.binary import UuidRepresentation from bson.binary import UuidRepresentation
from bson.codec_options import CodecOptions, DatetimeConversion, TypeRegistry from bson.codec_options import CodecOptions, DatetimeConversion, TypeRegistry
from bson.raw_bson import RawBSONDocument from bson.raw_bson import RawBSONDocument
from pymongo.auth import MECHANISMS
from pymongo.auth_oidc import OIDCCallback
from pymongo.compression_support import ( from pymongo.compression_support import (
validate_compressors, validate_compressors,
validate_zlib_compression_level, validate_zlib_compression_level,
@ -380,6 +378,8 @@ def validate_read_preference_mode(dummy: Any, value: Any) -> _ServerMode:
def validate_auth_mechanism(option: str, value: Any) -> str: def validate_auth_mechanism(option: str, value: Any) -> str:
"""Validate the authMechanism URI option.""" """Validate the authMechanism URI option."""
from pymongo.auth import MECHANISMS
if value not in MECHANISMS: if value not in MECHANISMS:
raise ValueError(f"{option} must be in {tuple(MECHANISMS)}") raise ValueError(f"{option} must be in {tuple(MECHANISMS)}")
return value return value
@ -444,6 +444,8 @@ def validate_auth_mechanism_properties(option: str, value: Any) -> dict[str, Uni
elif key in ["ALLOWED_HOSTS"] and isinstance(value, list): elif key in ["ALLOWED_HOSTS"] and isinstance(value, list):
props[key] = value props[key] = value
elif key in ["OIDC_CALLBACK", "OIDC_HUMAN_CALLBACK"]: elif key in ["OIDC_CALLBACK", "OIDC_HUMAN_CALLBACK"]:
from pymongo.auth_oidc import OIDCCallback
if not isinstance(value, OIDCCallback): if not isinstance(value, OIDCCallback):
raise ValueError("callback must be an OIDCCallback object") raise ValueError("callback must be an OIDCCallback object")
props[key] = value props[key] = value

View File

@ -16,36 +16,41 @@ from __future__ import annotations
import warnings import warnings
from typing import Any, Iterable, Optional, Union from typing import Any, Iterable, Optional, Union
from pymongo._lazy_import import lazy_import
from pymongo.hello import HelloCompat from pymongo.hello import HelloCompat
from pymongo.monitoring import _SENSITIVE_COMMANDS from pymongo.helpers import _SENSITIVE_COMMANDS
try:
snappy = lazy_import("snappy")
_HAVE_SNAPPY = True
except ImportError:
# python-snappy isn't available.
_HAVE_SNAPPY = False
try:
zlib = lazy_import("zlib")
_HAVE_ZLIB = True
except ImportError:
# Python built without zlib support.
_HAVE_ZLIB = False
try:
zstandard = lazy_import("zstandard")
_HAVE_ZSTD = True
except ImportError:
_HAVE_ZSTD = False
_SUPPORTED_COMPRESSORS = {"snappy", "zlib", "zstd"} _SUPPORTED_COMPRESSORS = {"snappy", "zlib", "zstd"}
_NO_COMPRESSION = {HelloCompat.CMD, HelloCompat.LEGACY_CMD} _NO_COMPRESSION = {HelloCompat.CMD, HelloCompat.LEGACY_CMD}
_NO_COMPRESSION.update(_SENSITIVE_COMMANDS) _NO_COMPRESSION.update(_SENSITIVE_COMMANDS)
def _have_snappy() -> bool:
try:
import snappy # type:ignore[import] # noqa: F401
return True
except ImportError:
return False
def _have_zlib() -> bool:
try:
import zlib # noqa: F401
return True
except ImportError:
return False
def _have_zstd() -> bool:
try:
import zstandard # noqa: F401
return True
except ImportError:
return False
def validate_compressors(dummy: Any, value: Union[str, Iterable[str]]) -> list[str]: def validate_compressors(dummy: Any, value: Union[str, Iterable[str]]) -> list[str]:
try: try:
# `value` is string. # `value` is string.
@ -58,21 +63,21 @@ def validate_compressors(dummy: Any, value: Union[str, Iterable[str]]) -> list[s
if compressor not in _SUPPORTED_COMPRESSORS: if compressor not in _SUPPORTED_COMPRESSORS:
compressors.remove(compressor) compressors.remove(compressor)
warnings.warn(f"Unsupported compressor: {compressor}", stacklevel=2) warnings.warn(f"Unsupported compressor: {compressor}", stacklevel=2)
elif compressor == "snappy" and not _HAVE_SNAPPY: elif compressor == "snappy" and not _have_snappy():
compressors.remove(compressor) compressors.remove(compressor)
warnings.warn( warnings.warn(
"Wire protocol compression with snappy is not available. " "Wire protocol compression with snappy is not available. "
"You must install the python-snappy module for snappy support.", "You must install the python-snappy module for snappy support.",
stacklevel=2, stacklevel=2,
) )
elif compressor == "zlib" and not _HAVE_ZLIB: elif compressor == "zlib" and not _have_zlib():
compressors.remove(compressor) compressors.remove(compressor)
warnings.warn( warnings.warn(
"Wire protocol compression with zlib is not available. " "Wire protocol compression with zlib is not available. "
"The zlib module is not available.", "The zlib module is not available.",
stacklevel=2, stacklevel=2,
) )
elif compressor == "zstd" and not _HAVE_ZSTD: elif compressor == "zstd" and not _have_zstd():
compressors.remove(compressor) compressors.remove(compressor)
warnings.warn( warnings.warn(
"Wire protocol compression with zstandard is not available. " "Wire protocol compression with zstandard is not available. "
@ -117,6 +122,8 @@ class SnappyContext:
@staticmethod @staticmethod
def compress(data: bytes) -> bytes: def compress(data: bytes) -> bytes:
import snappy
return snappy.compress(data) return snappy.compress(data)
@ -127,6 +134,8 @@ class ZlibContext:
self.level = level self.level = level
def compress(self, data: bytes) -> bytes: def compress(self, data: bytes) -> bytes:
import zlib
return zlib.compress(data, self.level) return zlib.compress(data, self.level)
@ -137,6 +146,8 @@ class ZstdContext:
def compress(data: bytes) -> bytes: def compress(data: bytes) -> bytes:
# ZstdCompressor is not thread safe. # ZstdCompressor is not thread safe.
# TODO: Use a pool? # TODO: Use a pool?
import zstandard
return zstandard.ZstdCompressor().compress(data) return zstandard.ZstdCompressor().compress(data)
@ -146,12 +157,18 @@ def decompress(data: bytes, compressor_id: int) -> bytes:
# https://github.com/andrix/python-snappy/issues/65 # https://github.com/andrix/python-snappy/issues/65
# This only matters when data is a memoryview since # This only matters when data is a memoryview since
# id(bytes(data)) == id(data) when data is a bytes. # id(bytes(data)) == id(data) when data is a bytes.
import snappy
return snappy.uncompress(bytes(data)) return snappy.uncompress(bytes(data))
elif compressor_id == ZlibContext.compressor_id: elif compressor_id == ZlibContext.compressor_id:
import zlib
return zlib.decompress(data) return zlib.decompress(data)
elif compressor_id == ZstdContext.compressor_id: elif compressor_id == ZstdContext.compressor_id:
# ZstdDecompressor is not thread safe. # ZstdDecompressor is not thread safe.
# TODO: Use a pool? # TODO: Use a pool?
import zstandard
return zstandard.ZstdDecompressor().decompress(data) return zstandard.ZstdDecompressor().decompress(data)
else: else:
raise ValueError("Unknown compressorId %d" % (compressor_id,)) raise ValueError("Unknown compressorId %d" % (compressor_id,))

View File

@ -93,6 +93,21 @@ _REAUTHENTICATION_REQUIRED_CODE: int = 391
# Server code raised when authentication fails. # Server code raised when authentication fails.
_AUTHENTICATION_FAILURE_CODE: int = 18 _AUTHENTICATION_FAILURE_CODE: int = 18
# Note - to avoid bugs from forgetting which if these is all lowercase and
# which are camelCase, and at the same time avoid having to add a test for
# every command, use all lowercase here and test against command_name.lower().
_SENSITIVE_COMMANDS: set = {
"authenticate",
"saslstart",
"saslcontinue",
"getnonce",
"createuser",
"updateuser",
"copydbgetnonce",
"copydbsaslstart",
"copydb",
}
def _gen_index_name(keys: _IndexList) -> str: def _gen_index_name(keys: _IndexList) -> str:
"""Generate an index name from the set of fields it is over.""" """Generate an index name from the set of fields it is over."""

View File

@ -1344,8 +1344,9 @@ class MongoClient(common.BaseObject, Generic[_DocumentType]):
# always send primaryPreferred when directly connected to a repl set # always send primaryPreferred when directly connected to a repl set
# member. # member.
# Thread safe: if the type is single it cannot change. # Thread safe: if the type is single it cannot change.
topology = self._get_topology() # NOTE: We already opened the Topology when selecting a server so there's no need
single = topology.description.topology_type == TOPOLOGY_TYPE.Single # to call _get_topology() again.
single = self._topology.description.topology_type == TOPOLOGY_TYPE.Single
with self._checkout(server, session) as conn: with self._checkout(server, session) as conn:
if single: if single:
@ -1365,7 +1366,6 @@ class MongoClient(common.BaseObject, Generic[_DocumentType]):
operation: str, operation: str,
) -> ContextManager[tuple[Connection, _ServerMode]]: ) -> ContextManager[tuple[Connection, _ServerMode]]:
assert read_preference is not None, "read_preference must not be None" assert read_preference is not None, "read_preference must not be None"
_ = self._get_topology()
server = self._select_server(read_preference, session, operation) server = self._select_server(read_preference, session, operation)
return self._conn_from_server(read_preference, server, session) return self._conn_from_server(read_preference, server, session)

View File

@ -191,7 +191,7 @@ from typing import TYPE_CHECKING, Any, Mapping, Optional, Sequence
from bson.objectid import ObjectId from bson.objectid import ObjectId
from pymongo.hello import Hello, HelloCompat from pymongo.hello import Hello, HelloCompat
from pymongo.helpers import _handle_exception from pymongo.helpers import _SENSITIVE_COMMANDS, _handle_exception
from pymongo.typings import _Address, _DocumentOut from pymongo.typings import _Address, _DocumentOut
if TYPE_CHECKING: if TYPE_CHECKING:
@ -507,22 +507,6 @@ def register(listener: _EventListener) -> None:
_LISTENERS.cmap_listeners.append(listener) _LISTENERS.cmap_listeners.append(listener)
# Note - to avoid bugs from forgetting which if these is all lowercase and
# which are camelCase, and at the same time avoid having to add a test for
# every command, use all lowercase here and test against command_name.lower().
_SENSITIVE_COMMANDS: set = {
"authenticate",
"saslstart",
"saslcontinue",
"getnonce",
"createuser",
"updateuser",
"copydbgetnonce",
"copydbsaslstart",
"copydb",
}
# The "hello" command is also deemed sensitive when attempting speculative # The "hello" command is also deemed sensitive when attempting speculative
# authentication. # authentication.
def _is_speculative_authenticate(command_name: str, doc: Mapping[str, Any]) -> bool: def _is_speculative_authenticate(command_name: str, doc: Mapping[str, Any]) -> bool:

View File

@ -41,7 +41,7 @@ from typing import (
import bson import bson
from bson import DEFAULT_CODEC_OPTIONS from bson import DEFAULT_CODEC_OPTIONS
from pymongo import __version__, _csot, auth, helpers from pymongo import __version__, _csot, helpers
from pymongo.client_session import _validate_session_write_concern from pymongo.client_session import _validate_session_write_concern
from pymongo.common import ( from pymongo.common import (
MAX_BSON_SIZE, MAX_BSON_SIZE,
@ -211,13 +211,14 @@ elif sys.platform == "darwin":
"version": platform.mac_ver()[0], "version": platform.mac_ver()[0],
} }
elif sys.platform == "win32": elif sys.platform == "win32":
_ver = sys.getwindowsversion()
_METADATA["os"] = { _METADATA["os"] = {
"type": platform.system(), "type": "Windows",
# "Windows XP", "Windows 7", "Windows 10", etc. "name": "Windows",
"name": " ".join((platform.system(), platform.release())), # Avoid using platform calls, see PYTHON-4455.
"architecture": platform.machine(), "architecture": os.environ.get("PROCESSOR_ARCHITECTURE") or platform.machine(),
# Windows patch level (e.g. 5.1.2600-SP3) # Windows patch level (e.g. 10.0.17763-SP0).
"version": "-".join(platform.win32_ver()[1:3]), "version": ".".join(map(str, _ver[:3])) + f"-SP{_ver[-1] or '0'}",
} }
elif sys.platform.startswith("java"): elif sys.platform.startswith("java"):
_name, _ver, _arch = platform.java_ver()[-1] _name, _ver, _arch = platform.java_ver()[-1]
@ -859,6 +860,8 @@ class Connection:
if creds: if creds:
if creds.mechanism == "DEFAULT" and creds.username: if creds.mechanism == "DEFAULT" and creds.username:
cmd["saslSupportedMechs"] = creds.source + "." + creds.username cmd["saslSupportedMechs"] = creds.source + "." + creds.username
from pymongo import auth
auth_ctx = auth._AuthContext.from_credentials(creds, self.address) auth_ctx = auth._AuthContext.from_credentials(creds, self.address)
if auth_ctx: if auth_ctx:
speculative_authenticate = auth_ctx.speculate_command() speculative_authenticate = auth_ctx.speculate_command()
@ -1090,6 +1093,8 @@ class Connection:
if not self.ready: if not self.ready:
creds = self.opts._credentials creds = self.opts._credentials
if creds: if creds:
from pymongo import auth
auth.authenticate(creds, self, reauthenticate=reauthenticate) auth.authenticate(creds, self, reauthenticate=reauthenticate)
self.ready = True self.ready = True
if self.enabled_for_cmap: if self.enabled_for_cmap:

View File

@ -25,10 +25,11 @@ from errno import EINTR as _EINTR
from ipaddress import ip_address as _ip_address from ipaddress import ip_address as _ip_address
from typing import TYPE_CHECKING, Any, Callable, Optional, TypeVar, Union from typing import TYPE_CHECKING, Any, Callable, Optional, TypeVar, Union
import cryptography.x509 as x509
import service_identity
from OpenSSL import SSL as _SSL from OpenSSL import SSL as _SSL
from OpenSSL import crypto as _crypto from OpenSSL import crypto as _crypto
from pymongo._lazy_import import lazy_import
from pymongo.errors import ConfigurationError as _ConfigurationError from pymongo.errors import ConfigurationError as _ConfigurationError
from pymongo.errors import _CertificateError # type:ignore[attr-defined] from pymongo.errors import _CertificateError # type:ignore[attr-defined]
from pymongo.ocsp_cache import _OCSPCache from pymongo.ocsp_cache import _OCSPCache
@ -37,14 +38,9 @@ from pymongo.socket_checker import SocketChecker as _SocketChecker
from pymongo.socket_checker import _errno_from_exception from pymongo.socket_checker import _errno_from_exception
from pymongo.write_concern import validate_boolean from pymongo.write_concern import validate_boolean
_x509 = lazy_import("cryptography.x509")
_service_identity = lazy_import("service_identity")
_service_identity_pyopenssl = lazy_import("service_identity.pyopenssl")
if TYPE_CHECKING: if TYPE_CHECKING:
from ssl import VerifyMode from ssl import VerifyMode
from cryptography.x509 import Certificate
_T = TypeVar("_T") _T = TypeVar("_T")
@ -184,7 +180,7 @@ class _CallbackData:
"""Data class which is passed to the OCSP callback.""" """Data class which is passed to the OCSP callback."""
def __init__(self) -> None: def __init__(self) -> None:
self.trusted_ca_certs: Optional[list[Certificate]] = None self.trusted_ca_certs: Optional[list[x509.Certificate]] = None
self.check_ocsp_endpoint: Optional[bool] = None self.check_ocsp_endpoint: Optional[bool] = None
self.ocsp_response_cache = _OCSPCache() self.ocsp_response_cache = _OCSPCache()
@ -336,11 +332,12 @@ class SSLContext:
"""Attempt to load CA certs from Windows trust store.""" """Attempt to load CA certs from Windows trust store."""
cert_store = self._ctx.get_cert_store() cert_store = self._ctx.get_cert_store()
oid = _stdlibssl.Purpose.SERVER_AUTH.oid oid = _stdlibssl.Purpose.SERVER_AUTH.oid
for cert, encoding, trust in _stdlibssl.enum_certificates(store): # type: ignore for cert, encoding, trust in _stdlibssl.enum_certificates(store): # type: ignore
if encoding == "x509_asn": if encoding == "x509_asn":
if trust is True or oid in trust: if trust is True or oid in trust:
cert_store.add_cert( cert_store.add_cert(
_crypto.X509.from_cryptography(_x509.load_der_x509_certificate(cert)) _crypto.X509.from_cryptography(x509.load_der_x509_certificate(cert))
) )
def load_default_certs(self) -> None: def load_default_certs(self) -> None:
@ -404,14 +401,16 @@ class SSLContext:
# XXX: Do this in a callback registered with # XXX: Do this in a callback registered with
# SSLContext.set_info_callback? See Twisted for an example. # SSLContext.set_info_callback? See Twisted for an example.
if self.check_hostname and server_hostname is not None: if self.check_hostname and server_hostname is not None:
from service_identity import pyopenssl
try: try:
if _is_ip_address(server_hostname): if _is_ip_address(server_hostname):
_service_identity_pyopenssl.verify_ip_address(ssl_conn, server_hostname) pyopenssl.verify_ip_address(ssl_conn, server_hostname)
else: else:
_service_identity_pyopenssl.verify_hostname(ssl_conn, server_hostname) pyopenssl.verify_hostname(ssl_conn, server_hostname)
except ( except ( # type:ignore[misc]
_service_identity.SICertificateError, service_identity.SICertificateError,
_service_identity.SIVerificationError, service_identity.SIVerificationError,
) as exc: ) as exc:
raise _CertificateError(str(exc)) from None raise _CertificateError(str(exc)) from None
return ssl_conn return ssl_conn

View File

@ -17,17 +17,22 @@ from __future__ import annotations
import ipaddress import ipaddress
import random import random
from typing import Any, Optional, Union from typing import TYPE_CHECKING, Any, Optional, Union
from pymongo.common import CONNECT_TIMEOUT from pymongo.common import CONNECT_TIMEOUT
from pymongo.errors import ConfigurationError from pymongo.errors import ConfigurationError
try: if TYPE_CHECKING:
from dns import resolver from dns import resolver
_HAVE_DNSPYTHON = True
except ImportError: def _have_dnspython() -> bool:
_HAVE_DNSPYTHON = False try:
import dns # noqa: F401
return True
except ImportError:
return False
# dnspython can return bytes or str from various parts # dnspython can return bytes or str from various parts
@ -40,6 +45,8 @@ def maybe_decode(text: Union[str, bytes]) -> str:
# PYTHON-2667 Lazily call dns.resolver methods for compatibility with eventlet. # PYTHON-2667 Lazily call dns.resolver methods for compatibility with eventlet.
def _resolve(*args: Any, **kwargs: Any) -> resolver.Answer: def _resolve(*args: Any, **kwargs: Any) -> resolver.Answer:
from dns import resolver
if hasattr(resolver, "resolve"): if hasattr(resolver, "resolve"):
# dnspython >= 2 # dnspython >= 2
return resolver.resolve(*args, **kwargs) return resolver.resolve(*args, **kwargs)
@ -81,6 +88,8 @@ class _SrvResolver:
raise ConfigurationError(_INVALID_HOST_MSG % (fqdn,)) raise ConfigurationError(_INVALID_HOST_MSG % (fqdn,))
def get_options(self) -> Optional[str]: def get_options(self) -> Optional[str]:
from dns import resolver
try: try:
results = _resolve(self.__fqdn, "TXT", lifetime=self.__connect_timeout) results = _resolve(self.__fqdn, "TXT", lifetime=self.__connect_timeout)
except (resolver.NoAnswer, resolver.NXDOMAIN): except (resolver.NoAnswer, resolver.NXDOMAIN):

View File

@ -44,7 +44,6 @@ from pymongo.lock import _create_lock
from pymongo.logger import ( from pymongo.logger import (
_SERVER_SELECTION_LOGGER, _SERVER_SELECTION_LOGGER,
_debug_log, _debug_log,
_info_log,
_ServerSelectionStatusMessage, _ServerSelectionStatusMessage,
) )
from pymongo.monitor import SrvMonitor from pymongo.monitor import SrvMonitor
@ -306,7 +305,7 @@ class Topology:
) )
if not logged_waiting: if not logged_waiting:
_info_log( _debug_log(
_SERVER_SELECTION_LOGGER, _SERVER_SELECTION_LOGGER,
message=_ServerSelectionStatusMessage.WAITING, message=_ServerSelectionStatusMessage.WAITING,
selector=selector, selector=selector,

View File

@ -40,7 +40,7 @@ from pymongo.common import (
get_validated_options, get_validated_options,
) )
from pymongo.errors import ConfigurationError, InvalidURI from pymongo.errors import ConfigurationError, InvalidURI
from pymongo.srv_resolver import _HAVE_DNSPYTHON, _SrvResolver from pymongo.srv_resolver import _have_dnspython, _SrvResolver
from pymongo.typings import _Address from pymongo.typings import _Address
if TYPE_CHECKING: if TYPE_CHECKING:
@ -472,7 +472,7 @@ def parse_uri(
is_srv = False is_srv = False
scheme_free = uri[SCHEME_LEN:] scheme_free = uri[SCHEME_LEN:]
elif uri.startswith(SRV_SCHEME): elif uri.startswith(SRV_SCHEME):
if not _HAVE_DNSPYTHON: if not _have_dnspython():
python_path = sys.executable or "python" python_path = sys.executable or "python"
raise ConfigurationError( raise ConfigurationError(
'The "dnspython" module must be ' 'The "dnspython" module must be '

10
sbom.json Normal file
View File

@ -0,0 +1,10 @@
{
"metadata": {
"timestamp": "2024-06-05T10:36:04.606968+00:00"
},
"serialNumber": "urn:uuid:a94f1412-ea1f-4821-b9f0-e14788b0776e",
"version": 1,
"$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
"bomFormat": "CycloneDX",
"specVersion": "1.5"
}

View File

@ -194,7 +194,7 @@
} }
}, },
{ {
"level": "info", "level": "debug",
"component": "serverSelection", "component": "serverSelection",
"data": { "data": {
"message": "Waiting for suitable server to become available", "message": "Waiting for suitable server to become available",

View File

@ -184,7 +184,7 @@
} }
}, },
{ {
"level": "info", "level": "debug",
"component": "serverSelection", "component": "serverSelection",
"data": { "data": {
"message": "Waiting for suitable server to become available", "message": "Waiting for suitable server to become available",

View File

@ -193,7 +193,7 @@
} }
}, },
{ {
"level": "info", "level": "debug",
"component": "serverSelection", "component": "serverSelection",
"data": { "data": {
"message": "Waiting for suitable server to become available", "message": "Waiting for suitable server to become available",

View File

@ -211,7 +211,7 @@
} }
}, },
{ {
"level": "info", "level": "debug",
"component": "serverSelection", "component": "serverSelection",
"data": { "data": {
"message": "Waiting for suitable server to become available", "message": "Waiting for suitable server to become available",

View File

@ -86,7 +86,7 @@ from pymongo import event_loggers, message, monitoring
from pymongo.client_options import ClientOptions from pymongo.client_options import ClientOptions
from pymongo.command_cursor import CommandCursor from pymongo.command_cursor import CommandCursor
from pymongo.common import _UUID_REPRESENTATIONS, CONNECT_TIMEOUT from pymongo.common import _UUID_REPRESENTATIONS, CONNECT_TIMEOUT
from pymongo.compression_support import _HAVE_SNAPPY, _HAVE_ZSTD from pymongo.compression_support import _have_snappy, _have_zstd
from pymongo.cursor import Cursor, CursorType from pymongo.cursor import Cursor, CursorType
from pymongo.database import Database from pymongo.database import Database
from pymongo.driver_info import DriverInfo from pymongo.driver_info import DriverInfo
@ -1558,7 +1558,7 @@ class TestClient(IntegrationTest):
self.assertEqual(opts.compressors, ["zlib"]) self.assertEqual(opts.compressors, ["zlib"])
self.assertEqual(opts.zlib_compression_level, -1) self.assertEqual(opts.zlib_compression_level, -1)
if not _HAVE_SNAPPY: if not _have_snappy():
uri = "mongodb://localhost:27017/?compressors=snappy" uri = "mongodb://localhost:27017/?compressors=snappy"
client = MongoClient(uri, connect=False) client = MongoClient(uri, connect=False)
opts = compression_settings(client) opts = compression_settings(client)
@ -1573,7 +1573,7 @@ class TestClient(IntegrationTest):
opts = compression_settings(client) opts = compression_settings(client)
self.assertEqual(opts.compressors, ["snappy", "zlib"]) self.assertEqual(opts.compressors, ["snappy", "zlib"])
if not _HAVE_ZSTD: if not _have_zstd():
uri = "mongodb://localhost:27017/?compressors=zstd" uri = "mongodb://localhost:27017/?compressors=zstd"
client = MongoClient(uri, connect=False) client = MongoClient(uri, connect=False)
opts = compression_settings(client) opts = compression_settings(client)

View File

@ -28,7 +28,7 @@ import pymongo
from pymongo import common from pymongo import common
from pymongo.errors import ConfigurationError from pymongo.errors import ConfigurationError
from pymongo.mongo_client import MongoClient from pymongo.mongo_client import MongoClient
from pymongo.srv_resolver import _HAVE_DNSPYTHON from pymongo.srv_resolver import _have_dnspython
WAIT_TIME = 0.1 WAIT_TIME = 0.1
@ -148,7 +148,7 @@ class TestSrvPolling(unittest.TestCase):
return True return True
def run_scenario(self, dns_response, expect_change): def run_scenario(self, dns_response, expect_change):
self.assertEqual(_HAVE_DNSPYTHON, True) self.assertEqual(_have_dnspython(), True)
if callable(dns_response): if callable(dns_response):
dns_resolver_response = dns_response dns_resolver_response = dns_response
else: else:

View File

@ -27,7 +27,7 @@ sys.path[0:0] = [""]
from test import clear_warning_registry, unittest from test import clear_warning_registry, unittest
from pymongo.common import INTERNAL_URI_OPTION_NAME_MAP, validate from pymongo.common import INTERNAL_URI_OPTION_NAME_MAP, validate
from pymongo.compression_support import _HAVE_SNAPPY from pymongo.compression_support import _have_snappy
from pymongo.uri_parser import SRV_SCHEME, parse_uri from pymongo.uri_parser import SRV_SCHEME, parse_uri
CONN_STRING_TEST_PATH = os.path.join( CONN_STRING_TEST_PATH = os.path.join(
@ -95,7 +95,7 @@ def run_scenario_in_dir(target_workdir):
def create_test(test, test_workdir): def create_test(test, test_workdir):
def run_scenario(self): def run_scenario(self):
compressors = (test.get("options") or {}).get("compressors", []) compressors = (test.get("options") or {}).get("compressors", [])
if "snappy" in compressors and not _HAVE_SNAPPY: if "snappy" in compressors and not _have_snappy():
self.skipTest("This test needs the snappy module.") self.skipTest("This test needs the snappy module.")
valid = True valid = True
warning = False warning = False

View File

@ -39,9 +39,9 @@ from pymongo.collection import ReturnDocument
from pymongo.cursor import CursorType from pymongo.cursor import CursorType
from pymongo.errors import ConfigurationError, OperationFailure from pymongo.errors import ConfigurationError, OperationFailure
from pymongo.hello import HelloCompat from pymongo.hello import HelloCompat
from pymongo.helpers import _SENSITIVE_COMMANDS
from pymongo.lock import _create_lock from pymongo.lock import _create_lock
from pymongo.monitoring import ( from pymongo.monitoring import (
_SENSITIVE_COMMANDS,
ConnectionCheckedInEvent, ConnectionCheckedInEvent,
ConnectionCheckedOutEvent, ConnectionCheckedOutEvent,
ConnectionCheckOutFailedEvent, ConnectionCheckOutFailedEvent,