diff --git a/doc/changelog.rst b/doc/changelog.rst index dbf24aaaad..7cc2151047 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -17,6 +17,8 @@ PyMongo 4.16 brings a number of changes including: - 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. +- Fixed return type annotation for ``find_one_and_*`` methods on :class:`~pymongo.asynchronous.collection.AsyncCollection` + and :class:`~pymongo.synchronous.collection.Collection` to include ``None``. Changes in Version 4.15.4 (2025/10/21) -------------------------------------- diff --git a/doc/contributors.rst b/doc/contributors.rst index 08296e9595..0bd815ce3f 100644 --- a/doc/contributors.rst +++ b/doc/contributors.rst @@ -107,3 +107,4 @@ The following is a list of people who have contributed to - Jeffrey A. Clark (aclark4life) - Steven Silvester (blink1073) - Noah Stapp (NoahStapp) +- Cal Jacobson (cj81499) diff --git a/pymongo/asynchronous/collection.py b/pymongo/asynchronous/collection.py index e7e2f58031..53b4992493 100644 --- a/pymongo/asynchronous/collection.py +++ b/pymongo/asynchronous/collection.py @@ -3310,7 +3310,7 @@ async def find_one_and_delete( let: Optional[Mapping[str, Any]] = None, comment: Optional[Any] = None, **kwargs: Any, - ) -> _DocumentType: + ) -> Optional[_DocumentType]: """Finds a single document and deletes it, returning the document. >>> await db.test.count_documents({'x': 1}) @@ -3320,6 +3320,10 @@ async def find_one_and_delete( >>> await db.test.count_documents({'x': 1}) 1 + Returns ``None`` if no document matches the filter. + + >>> await db.test.find_one_and_delete({'_exists': False}) + If multiple documents match *filter*, a *sort* can be applied. >>> async for doc in db.test.find({'x': 1}): @@ -3402,10 +3406,22 @@ async def find_one_and_replace( let: Optional[Mapping[str, Any]] = None, comment: Optional[Any] = None, **kwargs: Any, - ) -> _DocumentType: + ) -> Optional[_DocumentType]: """Finds a single document and replaces it, returning either the original or the replaced document. + >>> await db.test.find_one({'x': 1}) + {'_id': 0, 'x': 1} + >>> await db.test.find_one_and_replace({'x': 1}, {'y': 2}) + {'_id': 0, 'x': 1} + >>> await db.test.find_one({'x': 1}) + >>> await db.test.find_one({'y': 2}) + {'_id': 0, 'y': 2} + + Returns ``None`` if no document matches the filter. + + >>> await db.test.find_one_and_replace({'_exists': False}, {'x': 1}) + The :meth:`find_one_and_replace` method differs from :meth:`find_one_and_update` by replacing the document matched by *filter*, rather than modifying the existing document. @@ -3510,13 +3526,17 @@ async def find_one_and_update( let: Optional[Mapping[str, Any]] = None, comment: Optional[Any] = None, **kwargs: Any, - ) -> _DocumentType: + ) -> Optional[_DocumentType]: """Finds a single document and updates it, returning either the original or the updated document. + >>> await db.test.find_one({'_id': 665}) + {'_id': 665, 'done': False, 'count': 25} >>> await db.test.find_one_and_update( ... {'_id': 665}, {'$inc': {'count': 1}, '$set': {'done': True}}) - {'_id': 665, 'done': False, 'count': 25}} + {'_id': 665, 'done': False, 'count': 25} + >>> await db.test.find_one({'_id': 665}) + {'_id': 665, 'done': True, 'count': 26} Returns ``None`` if no document matches the filter. diff --git a/pymongo/synchronous/collection.py b/pymongo/synchronous/collection.py index 4e5f7d08fb..edc6047330 100644 --- a/pymongo/synchronous/collection.py +++ b/pymongo/synchronous/collection.py @@ -3303,7 +3303,7 @@ def find_one_and_delete( let: Optional[Mapping[str, Any]] = None, comment: Optional[Any] = None, **kwargs: Any, - ) -> _DocumentType: + ) -> Optional[_DocumentType]: """Finds a single document and deletes it, returning the document. >>> db.test.count_documents({'x': 1}) @@ -3313,6 +3313,10 @@ def find_one_and_delete( >>> db.test.count_documents({'x': 1}) 1 + Returns ``None`` if no document matches the filter. + + >>> db.test.find_one_and_delete({'_exists': False}) + If multiple documents match *filter*, a *sort* can be applied. >>> for doc in db.test.find({'x': 1}): @@ -3395,10 +3399,22 @@ def find_one_and_replace( let: Optional[Mapping[str, Any]] = None, comment: Optional[Any] = None, **kwargs: Any, - ) -> _DocumentType: + ) -> Optional[_DocumentType]: """Finds a single document and replaces it, returning either the original or the replaced document. + >>> db.test.find_one({'x': 1}) + {'_id': 0, 'x': 1} + >>> db.test.find_one_and_replace({'x': 1}, {'y': 2}) + {'_id': 0, 'x': 1} + >>> db.test.find_one({'x': 1}) + >>> db.test.find_one({'y': 2}) + {'_id': 0, 'y': 2} + + Returns ``None`` if no document matches the filter. + + >>> db.test.find_one_and_replace({'_exists': False}, {'x': 1}) + The :meth:`find_one_and_replace` method differs from :meth:`find_one_and_update` by replacing the document matched by *filter*, rather than modifying the existing document. @@ -3503,13 +3519,17 @@ def find_one_and_update( let: Optional[Mapping[str, Any]] = None, comment: Optional[Any] = None, **kwargs: Any, - ) -> _DocumentType: + ) -> Optional[_DocumentType]: """Finds a single document and updates it, returning either the original or the updated document. + >>> db.test.find_one({'_id': 665}) + {'_id': 665, 'done': False, 'count': 25} >>> db.test.find_one_and_update( ... {'_id': 665}, {'$inc': {'count': 1}, '$set': {'done': True}}) - {'_id': 665, 'done': False, 'count': 25}} + {'_id': 665, 'done': False, 'count': 25} + >>> db.test.find_one({'_id': 665}) + {'_id': 665, 'done': True, 'count': 26} Returns ``None`` if no document matches the filter.