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
+}