diff --git a/doc/changelog.rst b/doc/changelog.rst index 3fd2387f9..42efdacba 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.5 (2025/XX/XX) -------------------------------------- diff --git a/doc/contributors.rst b/doc/contributors.rst index 08296e959..0bd815ce3 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 e7e2f5803..53b499249 100644 --- a/pymongo/asynchronous/collection.py +++ b/pymongo/asynchronous/collection.py @@ -3310,7 +3310,7 @@ class AsyncCollection(common.BaseObject, Generic[_DocumentType]): 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 @@ class AsyncCollection(common.BaseObject, Generic[_DocumentType]): >>> 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 @@ class AsyncCollection(common.BaseObject, Generic[_DocumentType]): 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 @@ class AsyncCollection(common.BaseObject, Generic[_DocumentType]): 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 4e5f7d08f..edc604733 100644 --- a/pymongo/synchronous/collection.py +++ b/pymongo/synchronous/collection.py @@ -3303,7 +3303,7 @@ class Collection(common.BaseObject, Generic[_DocumentType]): 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 @@ class Collection(common.BaseObject, Generic[_DocumentType]): >>> 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 @@ class Collection(common.BaseObject, Generic[_DocumentType]): 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 @@ class Collection(common.BaseObject, Generic[_DocumentType]): 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.