commit:     0c322fe28adb90de4c444677d7d15ebc3db38634
Author:     Michał Górny <mgorny <AT> gentoo <DOT> org>
AuthorDate: Mon Oct  6 18:11:03 2025 +0000
Commit:     Michał Górny <mgorny <AT> gentoo <DOT> org>
CommitDate: Mon Oct  6 19:08:26 2025 +0000
URL:        https://gitweb.gentoo.org/repo/gentoo.git/commit/?id=0c322fe2

dev-python/pymongo: Backport upstream changes to fix crash

Signed-off-by: Michał Górny <mgorny <AT> gentoo.org>

 .../pymongo/files/pymongo-4.15.2-backports.patch   | 212 +++++++++++++++++++++
 dev-python/pymongo/pymongo-4.15.2-r1.ebuild        | 207 ++++++++++++++++++++
 2 files changed, 419 insertions(+)

diff --git a/dev-python/pymongo/files/pymongo-4.15.2-backports.patch 
b/dev-python/pymongo/files/pymongo-4.15.2-backports.patch
new file mode 100644
index 000000000000..0bc1a4c49256
--- /dev/null
+++ b/dev-python/pymongo/files/pymongo-4.15.2-backports.patch
@@ -0,0 +1,212 @@
+diff --git a/bson/__init__.py b/bson/__init__.py
+index b655e30c2c..6b2ba293a6 100644
+--- a/bson/__init__.py
++++ b/bson/__init__.py
+@@ -1009,7 +1009,7 @@ def _dict_to_bson(
+                 try:
+                     elements.append(_element_to_bson(key, value, check_keys, 
opts))
+                 except InvalidDocument as err:
+-                    raise InvalidDocument(f"Invalid document {doc} | {err}") 
from err
++                    raise InvalidDocument(f"Invalid document: {err}", doc) 
from err
+     except AttributeError:
+         raise TypeError(f"encoder expected a mapping type but got: {doc!r}") 
from None
+ 
+diff --git a/bson/_cbsonmodule.c b/bson/_cbsonmodule.c
+index be91e41734..bee7198567 100644
+--- a/bson/_cbsonmodule.c
++++ b/bson/_cbsonmodule.c
+@@ -1645,11 +1645,11 @@ static int write_raw_doc(buffer_t buffer, PyObject* 
raw, PyObject* _raw_str) {
+ }
+ 
+ 
+-/* Update Invalid Document error message to include doc.
++/* Update Invalid Document error to include doc as a property.
+  */
+ void handle_invalid_doc_error(PyObject* dict) {
+     PyObject *etype = NULL, *evalue = NULL, *etrace = NULL;
+-    PyObject *msg = NULL, *dict_str = NULL, *new_msg = NULL;
++    PyObject *msg = NULL, *new_msg = NULL, *new_evalue = NULL;
+     PyErr_Fetch(&etype, &evalue, &etrace);
+     PyObject *InvalidDocument = _error("InvalidDocument");
+     if (InvalidDocument == NULL) {
+@@ -1659,26 +1659,22 @@ void handle_invalid_doc_error(PyObject* dict) {
+     if (evalue && PyErr_GivenExceptionMatches(etype, InvalidDocument)) {
+         PyObject *msg = PyObject_Str(evalue);
+         if (msg) {
+-            // Prepend doc to the existing message
+-            PyObject *dict_str = PyObject_Str(dict);
+-            if (dict_str == NULL) {
+-                goto cleanup;
+-            }
+-            const char * dict_str_utf8 = PyUnicode_AsUTF8(dict_str);
+-            if (dict_str_utf8 == NULL) {
+-                goto cleanup;
+-            }
+             const char * msg_utf8 = PyUnicode_AsUTF8(msg);
+             if (msg_utf8 == NULL) {
+                 goto cleanup;
+             }
+-            PyObject *new_msg = PyUnicode_FromFormat("Invalid document %s | 
%s", dict_str_utf8, msg_utf8);
++            PyObject *new_msg = PyUnicode_FromFormat("Invalid document: %s", 
msg_utf8);
++            if (new_msg == NULL) {
++                goto cleanup;
++            }
++            // Add doc to the error instance as a property.
++            PyObject *new_evalue = 
PyObject_CallFunctionObjArgs(InvalidDocument, new_msg, dict, NULL);
+             Py_DECREF(evalue);
+             Py_DECREF(etype);
+             etype = InvalidDocument;
+             InvalidDocument = NULL;
+-            if (new_msg) {
+-                evalue = new_msg;
++            if (new_evalue) {
++                evalue = new_evalue;
+             } else {
+                 evalue = msg;
+             }
+@@ -1689,7 +1685,7 @@ void handle_invalid_doc_error(PyObject* dict) {
+     PyErr_Restore(etype, evalue, etrace);
+     Py_XDECREF(msg);
+     Py_XDECREF(InvalidDocument);
+-    Py_XDECREF(dict_str);
++    Py_XDECREF(new_evalue);
+     Py_XDECREF(new_msg);
+ }
+ 
+diff --git a/bson/errors.py b/bson/errors.py
+index a3699e704c..ffc117f7ac 100644
+--- a/bson/errors.py
++++ b/bson/errors.py
+@@ -15,6 +15,8 @@
+ """Exceptions raised by the BSON package."""
+ from __future__ import annotations
+ 
++from typing import Any, Optional
++
+ 
+ class BSONError(Exception):
+     """Base class for all BSON exceptions."""
+@@ -31,6 +33,17 @@ class InvalidStringData(BSONError):
+ class InvalidDocument(BSONError):
+     """Raised when trying to create a BSON object from an invalid document."""
+ 
++    def __init__(self, message: str, document: Optional[Any] = None) -> None:
++        super().__init__(message)
++        self._document = document
++
++    @property
++    def document(self) -> Any:
++        """The invalid document that caused the error.
++
++        ..versionadded:: 4.16"""
++        return self._document
++
+ 
+ class InvalidId(BSONError):
+     """Raised when trying to create an ObjectId from invalid data."""
+diff --git a/doc/changelog.rst b/doc/changelog.rst
+index 082c22fafc..7270043d41 100644
+--- a/doc/changelog.rst
++++ b/doc/changelog.rst
+@@ -1,6 +1,15 @@
+ Changelog
+ =========
+ 
++Changes in Version 4.16.0 (XXXX/XX/XX)
++--------------------------------------
++
++PyMongo 4.16 brings a number of changes including:
++
++- 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`.
++
+ Changes in Version 4.15.1 (2025/09/16)
+ --------------------------------------
+ 
+diff --git a/test/test_bson.py b/test/test_bson.py
+index e4cf85c46c..f792db1e89 100644
+--- a/test/test_bson.py
++++ b/test/test_bson.py
+@@ -1163,7 +1163,7 @@ def __repr__(self):
+         ):
+             encode({"t": Wrapper(1)})
+ 
+-    def test_doc_in_invalid_document_error_message(self):
++    def test_doc_in_invalid_document_error_as_property(self):
+         class Wrapper:
+             def __init__(self, val):
+                 self.val = val
+@@ -1173,10 +1173,11 @@ def __repr__(self):
+ 
+         self.assertEqual("1", repr(Wrapper(1)))
+         doc = {"t": Wrapper(1)}
+-        with self.assertRaisesRegex(InvalidDocument, f"Invalid document 
{doc}"):
++        with self.assertRaisesRegex(InvalidDocument, "Invalid document:") as 
cm:
+             encode(doc)
++        self.assertEqual(cm.exception.document, doc)
+ 
+-    def test_doc_in_invalid_document_error_message_mapping(self):
++    def test_doc_in_invalid_document_error_as_property_mapping(self):
+         class MyMapping(abc.Mapping):
+             def keys(self):
+                 return ["t"]
+@@ -1192,6 +1193,11 @@ def __len__(self):
+             def __iter__(self):
+                 return iter(["t"])
+ 
++            def __eq__(self, other):
++                if isinstance(other, MyMapping):
++                    return True
++                return False
++
+         class Wrapper:
+             def __init__(self, val):
+                 self.val = val
+@@ -1201,8 +1207,9 @@ def __repr__(self):
+ 
+         self.assertEqual("1", repr(Wrapper(1)))
+         doc = MyMapping()
+-        with self.assertRaisesRegex(InvalidDocument, f"Invalid document 
{doc}"):
++        with self.assertRaisesRegex(InvalidDocument, "Invalid document:") as 
cm:
+             encode(doc)
++        self.assertEqual(cm.exception.document, doc)
+ 
+ 
+ class TestCodecOptions(unittest.TestCase):
+diff --git a/bson/_cbsonmodule.c b/bson/_cbsonmodule.c
+index bee7198567..7d184641c5 100644
+--- a/bson/_cbsonmodule.c
++++ b/bson/_cbsonmodule.c
+@@ -1657,26 +1657,28 @@ void handle_invalid_doc_error(PyObject* dict) {
+     }
+ 
+     if (evalue && PyErr_GivenExceptionMatches(etype, InvalidDocument)) {
+-        PyObject *msg = PyObject_Str(evalue);
++        msg = PyObject_Str(evalue);
+         if (msg) {
+             const char * msg_utf8 = PyUnicode_AsUTF8(msg);
+             if (msg_utf8 == NULL) {
+                 goto cleanup;
+             }
+-            PyObject *new_msg = PyUnicode_FromFormat("Invalid document: %s", 
msg_utf8);
++            new_msg = PyUnicode_FromFormat("Invalid document: %s", msg_utf8);
+             if (new_msg == NULL) {
+                 goto cleanup;
+             }
+             // Add doc to the error instance as a property.
+-            PyObject *new_evalue = 
PyObject_CallFunctionObjArgs(InvalidDocument, new_msg, dict, NULL);
++            new_evalue = PyObject_CallFunctionObjArgs(InvalidDocument, 
new_msg, dict, NULL);
+             Py_DECREF(evalue);
+             Py_DECREF(etype);
+             etype = InvalidDocument;
+             InvalidDocument = NULL;
+             if (new_evalue) {
+                 evalue = new_evalue;
++                new_evalue = NULL;
+             } else {
+                 evalue = msg;
++                msg = NULL;
+             }
+         }
+         PyErr_NormalizeException(&etype, &evalue, &etrace);

diff --git a/dev-python/pymongo/pymongo-4.15.2-r1.ebuild 
b/dev-python/pymongo/pymongo-4.15.2-r1.ebuild
new file mode 100644
index 000000000000..d80547c6d4e3
--- /dev/null
+++ b/dev-python/pymongo/pymongo-4.15.2-r1.ebuild
@@ -0,0 +1,207 @@
+# Copyright 1999-2025 Gentoo Authors
+# Distributed under the terms of the GNU General Public License v2
+
+EAPI=8
+
+DISTUTILS_EXT=1
+DISTUTILS_USE_PEP517=hatchling
+PYTHON_COMPAT=( pypy3_11 python3_{11..14} )
+
+inherit check-reqs distutils-r1
+
+MY_P=mongo-python-driver-${PV}
+DESCRIPTION="Python driver for MongoDB"
+HOMEPAGE="
+       https://github.com/mongodb/mongo-python-driver/
+       https://pypi.org/project/pymongo/
+"
+SRC_URI="
+       https://github.com/mongodb/mongo-python-driver/archive/${PV}.tar.gz
+               -> ${MY_P}.gh.tar.gz
+"
+S=${WORKDIR}/${MY_P}
+
+LICENSE="Apache-2.0"
+SLOT="0"
+KEYWORDS="~alpha ~amd64 ~arm ~arm64 ~hppa ~loong ~mips ~ppc ~ppc64 ~riscv 
~s390 ~sparc ~x86"
+IUSE="doc kerberos +native-extensions +test-full"
+
+RDEPEND="
+       <dev-python/dnspython-3.0.0[${PYTHON_USEDEP}]
+       kerberos? ( dev-python/kerberos[${PYTHON_USEDEP}] )
+"
+BDEPEND="
+       dev-python/setuptools[${PYTHON_USEDEP}]
+       test? (
+               test-full? (
+                       >=dev-db/mongodb-2.6.0
+               )
+       )
+"
+
+distutils_enable_sphinx doc
+
+EPYTEST_PLUGINS=( pytest-asyncio )
+EPYTEST_RERUNS=5
+distutils_enable_tests pytest
+
+reqcheck() {
+       if use test && use test-full; then
+               # During the tests, database size reaches 1.5G.
+               local CHECKREQS_DISK_BUILD=1536M
+
+               check-reqs_${1}
+       fi
+}
+
+pkg_pretend() {
+       reqcheck pkg_pretend
+}
+
+pkg_setup() {
+       reqcheck pkg_setup
+}
+
+src_prepare() {
+       local PATCHES=(
+               # https://github.com/mongodb/mongo-python-driver/pull/2539
+               # https://github.com/mongodb/mongo-python-driver/pull/2573
+               "${FILESDIR}/${P}-backports.patch"
+       )
+
+       distutils-r1_src_prepare
+       # we do not want hatch-requirements-txt and its ton of NIH deps
+       sed -i -e '/requirements/d' pyproject.toml || die
+}
+
+python_compile() {
+       # causes build errors to be fatal
+       local -x TOX_ENV_NAME=whatever
+       local DISTUTILS_ARGS=()
+       # unconditionally implicitly disabled on pypy3
+       if ! use native-extensions; then
+               export NO_EXT=1
+       else
+               export PYMONGO_C_EXT_MUST_BUILD=1
+               unset NO_EXT
+       fi
+
+       distutils-r1_python_compile
+
+       # upstream forces setup.py build_ext -i in their setuptools hack
+       find -name '*.so' -delete || die
+}
+
+python_test() {
+       rm -rf bson pymongo || die
+
+       local EPYTEST_DESELECT=(
+               # network-sandbox
+               
test/asynchronous/test_async_loop_unblocked.py::TestClientLoopUnblocked::test_client_does_not_block_loop
+               
test/asynchronous/test_client.py::AsyncClientUnitTest::test_connection_timeout_ms_propagates_to_DNS_resolver
+               
test/asynchronous/test_client.py::AsyncClientUnitTest::test_detected_environment_logging
+               
test/asynchronous/test_client.py::AsyncClientUnitTest::test_detected_environment_warning
+               
test/asynchronous/test_client.py::TestClient::test_service_name_from_kwargs
+               
test/asynchronous/test_client.py::TestClient::test_srv_max_hosts_kwarg
+               
test/test_client.py::ClientUnitTest::test_connection_timeout_ms_propagates_to_DNS_resolver
+               
test/test_client.py::ClientUnitTest::test_detected_environment_logging
+               
test/test_client.py::ClientUnitTest::test_detected_environment_warning
+               test/test_client.py::TestClient::test_service_name_from_kwargs
+               test/test_client.py::TestClient::test_srv_max_hosts_kwarg
+               
test/test_dns.py::TestCaseInsensitive::test_connect_case_insensitive
+               
test/asynchronous/test_dns.py::IsolatedAsyncioTestCaseInsensitive::test_connect_case_insensitive
+               test/test_srv_polling.py
+               test/asynchronous/test_srv_polling.py
+               
test/test_uri_spec.py::TestAllScenarios::test_test_uri_options_srv-options_SRV_URI_with_custom_srvServiceName
+               
test/test_uri_spec.py::TestAllScenarios::test_test_uri_options_srv-options_SRV_URI_with_invalid_type_for_srvMaxHosts
+               
test/test_uri_spec.py::TestAllScenarios::test_test_uri_options_srv-options_SRV_URI_with_negative_integer_for_srvMaxHosts
+               
test/test_uri_spec.py::TestAllScenarios::test_test_uri_options_srv-options_SRV_URI_with_positive_srvMaxHosts_and_loadBalanced=fa
+               
test/test_uri_spec.py::TestAllScenarios::test_test_uri_options_srv-options_SRV_URI_with_srvMaxHosts
+               
test/test_uri_spec.py::TestAllScenarios::test_test_uri_options_srv-options_SRV_URI_with_srvMaxHosts=0_and_loadBalanced=true
+               
test/test_uri_spec.py::TestAllScenarios::test_test_uri_options_srv-options_SRV_URI_with_srvMaxHosts=0_and_replicaSet
+
+               # broken regularly by changes in mypy
+               test/test_typing.py::TestMypyFails::test_mypy_failures
+
+               # fragile to timing? fails because we're getting too many logs
+               
test/test_connection_logging.py::TestConnectionLoggingConnectionPoolOptions::test_maxConnecting_should_be_included_in_connection_pool_created_message_when_specified
+
+               # hangs?
+               
test/asynchronous/test_grid_file.py::AsyncTestGridFile::test_small_chunks
+
+               # broken async tests?
+               test/asynchronous/test_encryption.py
+
+               # -Werror
+               
test/test_read_preferences.py::TestMongosAndReadPreference::test_read_preference_hedge_deprecated
+               
test/asynchronous/test_read_preferences.py::TestMongosAndReadPreference::test_read_preference_hedge_deprecated
+
+               # fragile to timing? Internet?
+               test/test_client.py::TestClient::test_repr_srv_host
+               test/asynchronous/test_client.py::TestClient::test_repr_srv_host
+               
test/asynchronous/test_ssl.py::TestSSL::test_pyopenssl_ignored_in_async
+       )
+
+       if ! use test-full; then
+               # .invalid is guaranteed to return NXDOMAIN per RFC 6761
+               local -x DB_IP=mongodb.invalid
+               epytest -p asyncio
+               return
+       fi
+
+       # Yes, we need TCP/IP for that...
+       local -x DB_IP=127.0.0.1
+       local -x DB_PORT=27000
+
+       local dbpath=${TMPDIR}/mongo.db
+       local logpath=${TMPDIR}/mongod.log
+
+       local failed=
+       mkdir -p "${dbpath}" || die
+       while true; do
+               ebegin "Trying to start mongod on port ${DB_PORT}"
+
+               # mongodb is extremely inefficient
+               # 
https://www.mongodb.com/docs/manual/reference/ulimit/#review-and-set-resource-limits
+               ulimit -n 64000 || die
+
+               local mongod_options=(
+                       --dbpath "${dbpath}"
+                       --bind_ip "${DB_IP}"
+                       --port "${DB_PORT}"
+                       --unixSocketPrefix "${TMPDIR}"
+                       --logpath "${logpath}"
+                       --fork
+
+                       # try to reduce resource use
+                       --wiredTigerCacheSizeGB 0.25
+               )
+
+               LC_ALL=C mongod "${mongod_options[@]}" && sleep 2
+
+               # Now we need to check if the server actually started...
+               if [[ ${?} -eq 0 && -S "${TMPDIR}"/mongodb-${DB_PORT}.sock ]]; 
then
+                       # yay!
+                       eend 0
+                       break
+               elif grep -q 'Address already in use' "${logpath}"; then
+                       # ay, someone took our port!
+                       eend 1
+                       : $(( DB_PORT += 1 ))
+                       continue
+               else
+                       eend 1
+                       eerror "Unable to start mongod for tests. See the 
server log:"
+                       eerror "        ${logpath}"
+                       die "Unable to start mongod for tests."
+               fi
+       done
+
+       nonfatal epytest -m "default or default_async or encryption" || failed=1
+
+       mongod --dbpath "${dbpath}" --shutdown || die
+
+       [[ ${failed} ]] && die "Tests fail with ${EPYTHON}"
+
+       rm -rf "${dbpath}" || die
+}

Reply via email to