From 2c0e189425ab0099f76a6d61e1b0c76aed9e5616 Mon Sep 17 00:00:00 2001 From: Noah Stapp Date: Wed, 4 Feb 2026 14:17:49 -0500 Subject: [PATCH 01/11] PYTHON-4542 - Improved sessions API --- pymongo/asynchronous/client_session.py | 26 +++++++++++++++++++++++++- pymongo/synchronous/client_session.py | 26 +++++++++++++++++++++++++- tools/synchro.py | 1 + 3 files changed, 51 insertions(+), 2 deletions(-) diff --git a/pymongo/asynchronous/client_session.py b/pymongo/asynchronous/client_session.py index a12ca1f11b..7c2d99c465 100644 --- a/pymongo/asynchronous/client_session.py +++ b/pymongo/asynchronous/client_session.py @@ -139,6 +139,7 @@ import time import uuid from collections.abc import Mapping as _Mapping +from contextvars import ContextVar from typing import ( TYPE_CHECKING, Any, @@ -181,6 +182,14 @@ _IS_SYNC = False +_SESSION: ContextVar[Optional[_AsyncBoundClientSession]] = ContextVar("SESSION", default=None) + + +class _AsyncBoundClientSession: + def __init__(self, session: AsyncClientSession, client_id: int): + self.session = session + self.client_id = client_id + class SessionOptions: """Options for a new :class:`AsyncClientSession`. @@ -517,6 +526,9 @@ def __init__( self._attached_to_cursor = False # Should we leave the session alive when the cursor is closed? self._leave_alive = False + # Is this session bound to a scope? + self._bound = False + self._session_token: Optional[ContextVar[_AsyncBoundClientSession]] = None async def end_session(self) -> None: """Finish this session. If a transaction has started, abort it. @@ -547,11 +559,23 @@ def _check_ended(self) -> None: if self._server_session is None: raise InvalidOperation("Cannot use ended session") + def bind(self) -> AsyncClientSession: + self._bound = True + return self + async def __aenter__(self) -> AsyncClientSession: + if self._bound: + bound_session = _AsyncBoundClientSession(self, id(self._client)) + self._session_token = _SESSION.set(bound_session) return self async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: - await self._end_session(lock=True) + if self._session_token: + _SESSION.reset(self._session_token) + self._session_token = None + self._bound = False + else: + await self._end_session(lock=True) @property def client(self) -> AsyncMongoClient[Any]: diff --git a/pymongo/synchronous/client_session.py b/pymongo/synchronous/client_session.py index 8755e57261..9ed4573b60 100644 --- a/pymongo/synchronous/client_session.py +++ b/pymongo/synchronous/client_session.py @@ -139,6 +139,7 @@ import time import uuid from collections.abc import Mapping as _Mapping +from contextvars import ContextVar from typing import ( TYPE_CHECKING, Any, @@ -180,6 +181,14 @@ _IS_SYNC = True +_SESSION: ContextVar[Optional[_BoundClientSession]] = ContextVar("SESSION", default=None) + + +class _BoundClientSession: + def __init__(self, session: ClientSession, client_id: int): + self.session = session + self.client_id = client_id + class SessionOptions: """Options for a new :class:`ClientSession`. @@ -516,6 +525,9 @@ def __init__( self._attached_to_cursor = False # Should we leave the session alive when the cursor is closed? self._leave_alive = False + # Is this session bound to a scope? + self._bound = False + self._session_token: Optional[ContextVar[_BoundClientSession]] = None def end_session(self) -> None: """Finish this session. If a transaction has started, abort it. @@ -546,11 +558,23 @@ def _check_ended(self) -> None: if self._server_session is None: raise InvalidOperation("Cannot use ended session") + def bind(self) -> ClientSession: + self._bound = True + return self + def __enter__(self) -> ClientSession: + if self._bound: + bound_session = _BoundClientSession(self, id(self._client)) + self._session_token = _SESSION.set(bound_session) return self def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: - self._end_session(lock=True) + if self._session_token: + _SESSION.reset(self._session_token) + self._session_token = None + self._bound = False + else: + self._end_session(lock=True) @property def client(self) -> MongoClient[Any]: diff --git a/tools/synchro.py b/tools/synchro.py index 5735d0052a..d1056a41ed 100644 --- a/tools/synchro.py +++ b/tools/synchro.py @@ -37,6 +37,7 @@ "AsyncRawBatchCursor": "RawBatchCursor", "AsyncRawBatchCommandCursor": "RawBatchCommandCursor", "AsyncClientSession": "ClientSession", + "_AsyncBoundClientSession": "_BoundClientSession", "AsyncChangeStream": "ChangeStream", "AsyncCollectionChangeStream": "CollectionChangeStream", "AsyncDatabaseChangeStream": "DatabaseChangeStream", From 5c2193941b6982bd1e9c96d7a12858019a122712 Mon Sep 17 00:00:00 2001 From: Noah Stapp Date: Mon, 23 Feb 2026 12:20:51 -0500 Subject: [PATCH 02/11] Add test coverage --- pymongo/asynchronous/client_session.py | 8 ++- pymongo/asynchronous/mongo_client.py | 21 +++++- pymongo/synchronous/client_session.py | 8 ++- pymongo/synchronous/mongo_client.py | 21 +++++- test/asynchronous/test_session.py | 95 ++++++++++++++++++++++++++ test/test_session.py | 95 ++++++++++++++++++++++++++ 6 files changed, 238 insertions(+), 10 deletions(-) diff --git a/pymongo/asynchronous/client_session.py b/pymongo/asynchronous/client_session.py index 7c2d99c465..5af272721f 100644 --- a/pymongo/asynchronous/client_session.py +++ b/pymongo/asynchronous/client_session.py @@ -154,6 +154,8 @@ TypeVar, ) +from _contextvars import Token + from bson.binary import Binary from bson.int64 import Int64 from bson.timestamp import Timestamp @@ -528,7 +530,7 @@ def __init__( self._leave_alive = False # Is this session bound to a scope? self._bound = False - self._session_token: Optional[ContextVar[_AsyncBoundClientSession]] = None + self._session_token: Optional[Token[_AsyncBoundClientSession]] = None async def end_session(self) -> None: """Finish this session. If a transaction has started, abort it. @@ -566,12 +568,12 @@ def bind(self) -> AsyncClientSession: async def __aenter__(self) -> AsyncClientSession: if self._bound: bound_session = _AsyncBoundClientSession(self, id(self._client)) - self._session_token = _SESSION.set(bound_session) + self._session_token = _SESSION.set(bound_session) # type: ignore[assignment] return self async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: if self._session_token: - _SESSION.reset(self._session_token) + _SESSION.reset(self._session_token) # type: ignore[arg-type] self._session_token = None self._bound = False else: diff --git a/pymongo/asynchronous/mongo_client.py b/pymongo/asynchronous/mongo_client.py index 4f3c43f23c..afd634a76a 100644 --- a/pymongo/asynchronous/mongo_client.py +++ b/pymongo/asynchronous/mongo_client.py @@ -65,7 +65,7 @@ from pymongo.asynchronous import client_session, database, uri_parser from pymongo.asynchronous.change_stream import AsyncChangeStream, AsyncClusterChangeStream from pymongo.asynchronous.client_bulk import _AsyncClientBulk -from pymongo.asynchronous.client_session import _EmptyServerSession +from pymongo.asynchronous.client_session import _SESSION, _EmptyServerSession from pymongo.asynchronous.command_cursor import AsyncCommandCursor from pymongo.asynchronous.settings import TopologySettings from pymongo.asynchronous.topology import Topology, _ErrorContext @@ -1408,7 +1408,8 @@ def start_session( def _ensure_session( self, session: Optional[AsyncClientSession] = None ) -> Optional[AsyncClientSession]: - """If provided session is None, lend a temporary session.""" + """If provided session and bound session are None, lend a temporary session.""" + session = session or self._get_bound_session() if session: return session @@ -2267,6 +2268,10 @@ async def _tmp_session( self, session: Optional[client_session.AsyncClientSession] ) -> AsyncGenerator[Optional[client_session.AsyncClientSession], None]: """If provided session is None, lend a temporary session.""" + + # Check for a bound session. If one exists, treat it as an explicitly passed session. + session = session or self._get_bound_session() + if session is not None: if not isinstance(session, client_session.AsyncClientSession): raise ValueError( @@ -2301,6 +2306,18 @@ async def _process_response( if session is not None: session._process_response(reply) + def _get_bound_session(self) -> Optional[AsyncClientSession]: + bound_session = _SESSION.get() + if bound_session: + if bound_session.client_id == id(self): + return bound_session.session + else: + raise InvalidOperation( + "Only the client that created the bound session can perform operations within its context block. See for more information." + ) + else: + return None + async def server_info( self, session: Optional[client_session.AsyncClientSession] = None ) -> dict[str, Any]: diff --git a/pymongo/synchronous/client_session.py b/pymongo/synchronous/client_session.py index 9ed4573b60..87f6b318f2 100644 --- a/pymongo/synchronous/client_session.py +++ b/pymongo/synchronous/client_session.py @@ -153,6 +153,8 @@ TypeVar, ) +from _contextvars import Token + from bson.binary import Binary from bson.int64 import Int64 from bson.timestamp import Timestamp @@ -527,7 +529,7 @@ def __init__( self._leave_alive = False # Is this session bound to a scope? self._bound = False - self._session_token: Optional[ContextVar[_BoundClientSession]] = None + self._session_token: Optional[Token[_BoundClientSession]] = None def end_session(self) -> None: """Finish this session. If a transaction has started, abort it. @@ -565,12 +567,12 @@ def bind(self) -> ClientSession: def __enter__(self) -> ClientSession: if self._bound: bound_session = _BoundClientSession(self, id(self._client)) - self._session_token = _SESSION.set(bound_session) + self._session_token = _SESSION.set(bound_session) # type: ignore[assignment] return self def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: if self._session_token: - _SESSION.reset(self._session_token) + _SESSION.reset(self._session_token) # type: ignore[arg-type] self._session_token = None self._bound = False else: diff --git a/pymongo/synchronous/mongo_client.py b/pymongo/synchronous/mongo_client.py index cd0d19141f..863bce5bed 100644 --- a/pymongo/synchronous/mongo_client.py +++ b/pymongo/synchronous/mongo_client.py @@ -108,7 +108,7 @@ from pymongo.synchronous import client_session, database, uri_parser from pymongo.synchronous.change_stream import ChangeStream, ClusterChangeStream from pymongo.synchronous.client_bulk import _ClientBulk -from pymongo.synchronous.client_session import _EmptyServerSession +from pymongo.synchronous.client_session import _SESSION, _EmptyServerSession from pymongo.synchronous.command_cursor import CommandCursor from pymongo.synchronous.settings import TopologySettings from pymongo.synchronous.topology import Topology, _ErrorContext @@ -1406,7 +1406,8 @@ def start_session( ) def _ensure_session(self, session: Optional[ClientSession] = None) -> Optional[ClientSession]: - """If provided session is None, lend a temporary session.""" + """If provided session and bound session are None, lend a temporary session.""" + session = session or self._get_bound_session() if session: return session @@ -2263,6 +2264,10 @@ def _tmp_session( self, session: Optional[client_session.ClientSession] ) -> Generator[Optional[client_session.ClientSession], None]: """If provided session is None, lend a temporary session.""" + + # Check for a bound session. If one exists, treat it as an explicitly passed session. + session = session or self._get_bound_session() + if session is not None: if not isinstance(session, client_session.ClientSession): raise ValueError( @@ -2295,6 +2300,18 @@ def _process_response(self, reply: Mapping[str, Any], session: Optional[ClientSe if session is not None: session._process_response(reply) + def _get_bound_session(self) -> Optional[ClientSession]: + bound_session = _SESSION.get() + if bound_session: + if bound_session.client_id == id(self): + return bound_session.session + else: + raise InvalidOperation( + "Only the client that created the bound session can perform operations within its context block. See for more information." + ) + else: + return None + def server_info(self, session: Optional[client_session.ClientSession] = None) -> dict[str, Any]: """Get information about the MongoDB server we're connected to. diff --git a/test/asynchronous/test_session.py b/test/asynchronous/test_session.py index 19ce868c56..1f1412b581 100644 --- a/test/asynchronous/test_session.py +++ b/test/asynchronous/test_session.py @@ -189,6 +189,52 @@ async def _test_ops(self, client, *ops): f"{f.__name__} did not return implicit session to pool", ) + # Explicit bound session + for f, args, kw in ops: + async with client.start_session() as s: + async with s.bind(): + listener.reset() + s._materialize() + last_use = s._server_session.last_use + start = time.monotonic() + self.assertLessEqual(last_use, start) + # In case "f" modifies its inputs. + args = copy.copy(args) + kw = copy.copy(kw) + await f(*args, **kw) + self.assertGreaterEqual(len(listener.started_events), 1) + for event in listener.started_events: + self.assertIn( + "lsid", + event.command, + f"{f.__name__} sent no lsid with {event.command_name}", + ) + + self.assertEqual( + s.session_id, + event.command["lsid"], + f"{f.__name__} sent wrong lsid with {event.command_name}", + ) + + self.assertFalse(s.has_ended) + + self.assertTrue(s.has_ended) + with self.assertRaisesRegex(InvalidOperation, "ended session"): + async with s.bind(): + await f(*args, **kw) + + # Test a session cannot be used on another client. + async with self.client2.start_session() as s: + async with s.bind(): + # In case "f" modifies its inputs. + args = copy.copy(args) + kw = copy.copy(kw) + with self.assertRaisesRegex( + InvalidOperation, + "Only the client that created the bound session can perform operations within its context block", + ): + await f(*args, **kw) + async def test_implicit_sessions_checkout(self): # "To confirm that implicit sessions only allocate their server session after a # successful connection checkout" test from Driver Sessions Spec. @@ -825,6 +871,55 @@ async def test_session_not_copyable(self): async with client.start_session() as s: self.assertRaises(TypeError, lambda: copy.copy(s)) + async def test_nested_session_binding(self): + coll = self.client.pymongo_test.test + await coll.insert_one({"x": 1}) + + session1 = self.client.start_session() + session2 = self.client.start_session() + try: + self.listener.reset() + # Uses implicit session + await coll.find_one() + implicit_lsid = self.listener.started_events[0].command.get("lsid") + self.assertIsNotNone(implicit_lsid) + self.assertNotEqual(implicit_lsid, session1.session_id) + self.assertNotEqual(implicit_lsid, session2.session_id) + + async with session1.bind(): + self.listener.reset() + # Uses bound session1 + await coll.find_one() + session1_lsid = self.listener.started_events[0].command.get("lsid") + self.assertEqual(session1_lsid, session1.session_id) + + async with session2.bind(): + self.listener.reset() + # Uses bound session2 + await coll.find_one() + session2_lsid = self.listener.started_events[0].command.get("lsid") + self.assertEqual(session2_lsid, session2.session_id) + self.assertNotEqual(session2_lsid, session1.session_id) + + self.listener.reset() + # Use bound session1 again + await coll.find_one() + session1_lsid = self.listener.started_events[0].command.get("lsid") + self.assertEqual(session1_lsid, session1.session_id) + self.assertNotEqual(session1_lsid, session2.session_id) + + self.listener.reset() + # Uses implicit session + await coll.find_one() + implicit_lsid = self.listener.started_events[0].command.get("lsid") + self.assertIsNotNone(implicit_lsid) + self.assertNotEqual(implicit_lsid, session1.session_id) + self.assertNotEqual(implicit_lsid, session2.session_id) + + finally: + await session1.end_session() + await session2.end_session() + class TestCausalConsistency(AsyncUnitTest): listener: SessionTestListener diff --git a/test/test_session.py b/test/test_session.py index 40d0a53afb..61bf4ef37b 100644 --- a/test/test_session.py +++ b/test/test_session.py @@ -189,6 +189,52 @@ def _test_ops(self, client, *ops): f"{f.__name__} did not return implicit session to pool", ) + # Explicit bound session + for f, args, kw in ops: + with client.start_session() as s: + with s.bind(): + listener.reset() + s._materialize() + last_use = s._server_session.last_use + start = time.monotonic() + self.assertLessEqual(last_use, start) + # In case "f" modifies its inputs. + args = copy.copy(args) + kw = copy.copy(kw) + f(*args, **kw) + self.assertGreaterEqual(len(listener.started_events), 1) + for event in listener.started_events: + self.assertIn( + "lsid", + event.command, + f"{f.__name__} sent no lsid with {event.command_name}", + ) + + self.assertEqual( + s.session_id, + event.command["lsid"], + f"{f.__name__} sent wrong lsid with {event.command_name}", + ) + + self.assertFalse(s.has_ended) + + self.assertTrue(s.has_ended) + with self.assertRaisesRegex(InvalidOperation, "ended session"): + with s.bind(): + f(*args, **kw) + + # Test a session cannot be used on another client. + with self.client2.start_session() as s: + with s.bind(): + # In case "f" modifies its inputs. + args = copy.copy(args) + kw = copy.copy(kw) + with self.assertRaisesRegex( + InvalidOperation, + "Only the client that created the bound session can perform operations within its context block", + ): + f(*args, **kw) + def test_implicit_sessions_checkout(self): # "To confirm that implicit sessions only allocate their server session after a # successful connection checkout" test from Driver Sessions Spec. @@ -825,6 +871,55 @@ def test_session_not_copyable(self): with client.start_session() as s: self.assertRaises(TypeError, lambda: copy.copy(s)) + def test_nested_session_binding(self): + coll = self.client.pymongo_test.test + coll.insert_one({"x": 1}) + + session1 = self.client.start_session() + session2 = self.client.start_session() + try: + self.listener.reset() + # Uses implicit session + coll.find_one() + implicit_lsid = self.listener.started_events[0].command.get("lsid") + self.assertIsNotNone(implicit_lsid) + self.assertNotEqual(implicit_lsid, session1.session_id) + self.assertNotEqual(implicit_lsid, session2.session_id) + + with session1.bind(): + self.listener.reset() + # Uses bound session1 + coll.find_one() + session1_lsid = self.listener.started_events[0].command.get("lsid") + self.assertEqual(session1_lsid, session1.session_id) + + with session2.bind(): + self.listener.reset() + # Uses bound session2 + coll.find_one() + session2_lsid = self.listener.started_events[0].command.get("lsid") + self.assertEqual(session2_lsid, session2.session_id) + self.assertNotEqual(session2_lsid, session1.session_id) + + self.listener.reset() + # Use bound session1 again + coll.find_one() + session1_lsid = self.listener.started_events[0].command.get("lsid") + self.assertEqual(session1_lsid, session1.session_id) + self.assertNotEqual(session1_lsid, session2.session_id) + + self.listener.reset() + # Uses implicit session + coll.find_one() + implicit_lsid = self.listener.started_events[0].command.get("lsid") + self.assertIsNotNone(implicit_lsid) + self.assertNotEqual(implicit_lsid, session1.session_id) + self.assertNotEqual(implicit_lsid, session2.session_id) + + finally: + session1.end_session() + session2.end_session() + class TestCausalConsistency(UnitTest): listener: SessionTestListener From 969abb2c152af96a137330b80d7613ce5c5bda04 Mon Sep 17 00:00:00 2001 From: Noah Stapp Date: Mon, 23 Feb 2026 12:27:58 -0500 Subject: [PATCH 03/11] Update changelog --- doc/changelog.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/doc/changelog.rst b/doc/changelog.rst index 571ce3b63e..f38709203c 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -1,6 +1,15 @@ Changelog ========= +Changes in Version 4.17.0 (2026/XX/XX) +-------------------------------------- + +PyMongo 4.17 brings a number of changes including: + +- Added the :meth:`~pymongo.asynchronous.client_session.AsyncClientSession.bind` and :meth:`~pymongo.client_session.ClientSession.bind` methods + that allow users to bind a session to all database operations within the scope of a context manager instead of having to explicitly pass the session to each individual operation. + See for examples and more information. + Changes in Version 4.16.0 (2026/01/07) -------------------------------------- From f2416507f020996509ccfd83d05a12c3dfc57a37 Mon Sep 17 00:00:00 2001 From: Noah Stapp Date: Mon, 23 Feb 2026 14:17:47 -0500 Subject: [PATCH 04/11] Fix test --- pymongo/asynchronous/mongo_client.py | 11 +++++------ pymongo/synchronous/mongo_client.py | 11 +++++------ test/asynchronous/test_session.py | 2 ++ test/test_session.py | 2 ++ 4 files changed, 14 insertions(+), 12 deletions(-) diff --git a/pymongo/asynchronous/mongo_client.py b/pymongo/asynchronous/mongo_client.py index afd634a76a..d70186586c 100644 --- a/pymongo/asynchronous/mongo_client.py +++ b/pymongo/asynchronous/mongo_client.py @@ -2268,15 +2268,14 @@ async def _tmp_session( self, session: Optional[client_session.AsyncClientSession] ) -> AsyncGenerator[Optional[client_session.AsyncClientSession], None]: """If provided session is None, lend a temporary session.""" + if session is not None and not isinstance(session, client_session.AsyncClientSession): + raise ValueError( + f"'session' argument must be an AsyncClientSession or None, not {type(session)}" + ) # Check for a bound session. If one exists, treat it as an explicitly passed session. session = session or self._get_bound_session() - - if session is not None: - if not isinstance(session, client_session.AsyncClientSession): - raise ValueError( - f"'session' argument must be an AsyncClientSession or None, not {type(session)}" - ) + if session: # Don't call end_session. yield session return diff --git a/pymongo/synchronous/mongo_client.py b/pymongo/synchronous/mongo_client.py index 863bce5bed..943cd1f5b3 100644 --- a/pymongo/synchronous/mongo_client.py +++ b/pymongo/synchronous/mongo_client.py @@ -2264,15 +2264,14 @@ def _tmp_session( self, session: Optional[client_session.ClientSession] ) -> Generator[Optional[client_session.ClientSession], None]: """If provided session is None, lend a temporary session.""" + if session is not None and not isinstance(session, client_session.ClientSession): + raise ValueError( + f"'session' argument must be a ClientSession or None, not {type(session)}" + ) # Check for a bound session. If one exists, treat it as an explicitly passed session. session = session or self._get_bound_session() - - if session is not None: - if not isinstance(session, client_session.ClientSession): - raise ValueError( - f"'session' argument must be a ClientSession or None, not {type(session)}" - ) + if session: # Don't call end_session. yield session return diff --git a/test/asynchronous/test_session.py b/test/asynchronous/test_session.py index 1f1412b581..3ef2f73376 100644 --- a/test/asynchronous/test_session.py +++ b/test/asynchronous/test_session.py @@ -877,6 +877,8 @@ async def test_nested_session_binding(self): session1 = self.client.start_session() session2 = self.client.start_session() + session1._materialize() + session2._materialize() try: self.listener.reset() # Uses implicit session diff --git a/test/test_session.py b/test/test_session.py index 61bf4ef37b..4c58596930 100644 --- a/test/test_session.py +++ b/test/test_session.py @@ -877,6 +877,8 @@ def test_nested_session_binding(self): session1 = self.client.start_session() session2 = self.client.start_session() + session1._materialize() + session2._materialize() try: self.listener.reset() # Uses implicit session From d6b883b0a1ade6420946b6e9beec721a89f24e9a Mon Sep 17 00:00:00 2001 From: Noah Stapp Date: Thu, 26 Feb 2026 15:56:19 -0800 Subject: [PATCH 05/11] AC review --- pymongo/asynchronous/client_session.py | 2 +- pymongo/synchronous/client_session.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pymongo/asynchronous/client_session.py b/pymongo/asynchronous/client_session.py index 5af272721f..95c0855856 100644 --- a/pymongo/asynchronous/client_session.py +++ b/pymongo/asynchronous/client_session.py @@ -528,7 +528,7 @@ def __init__( self._attached_to_cursor = False # Should we leave the session alive when the cursor is closed? self._leave_alive = False - # Is this session bound to a scope? + # Is this session bound to a context manager scope? self._bound = False self._session_token: Optional[Token[_AsyncBoundClientSession]] = None diff --git a/pymongo/synchronous/client_session.py b/pymongo/synchronous/client_session.py index 87f6b318f2..85ff79f99f 100644 --- a/pymongo/synchronous/client_session.py +++ b/pymongo/synchronous/client_session.py @@ -527,7 +527,7 @@ def __init__( self._attached_to_cursor = False # Should we leave the session alive when the cursor is closed? self._leave_alive = False - # Is this session bound to a scope? + # Is this session bound to a context manager scope? self._bound = False self._session_token: Optional[Token[_BoundClientSession]] = None From 13f1a15d8d2ea39087da16660afb25577ad1eaac Mon Sep 17 00:00:00 2001 From: Noah Stapp Date: Thu, 26 Feb 2026 16:38:18 -0800 Subject: [PATCH 06/11] CP review --- pymongo/asynchronous/client_session.py | 44 +++++++++++++++----------- pymongo/synchronous/client_session.py | 44 +++++++++++++++----------- tools/synchro.py | 1 + 3 files changed, 53 insertions(+), 36 deletions(-) diff --git a/pymongo/asynchronous/client_session.py b/pymongo/asynchronous/client_session.py index 95c0855856..fe2dd07252 100644 --- a/pymongo/asynchronous/client_session.py +++ b/pymongo/asynchronous/client_session.py @@ -139,7 +139,7 @@ import time import uuid from collections.abc import Mapping as _Mapping -from contextvars import ContextVar +from contextvars import ContextVar, Token from typing import ( TYPE_CHECKING, Any, @@ -154,8 +154,6 @@ TypeVar, ) -from _contextvars import Token - from bson.binary import Binary from bson.int64 import Int64 from bson.timestamp import Timestamp @@ -193,6 +191,24 @@ def __init__(self, session: AsyncClientSession, client_id: int): self.client_id = client_id +class AsyncBoundSessionContext: + """Context manager returned by AsyncClientSession.bind() that manages bound state.""" + + def __init__(self, session: AsyncClientSession) -> None: + self._session = session + self._session_token: Optional[Token[_AsyncBoundClientSession]] = None + + async def __aenter__(self) -> AsyncClientSession: + bound_session = _AsyncBoundClientSession(self._session, id(self._session._client)) + self._session_token = _SESSION.set(bound_session) # type: ignore[assignment] + return self._session + + async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: + if self._session_token: + _SESSION.reset(self._session_token) # type: ignore[arg-type] + self._session_token = None + + class SessionOptions: """Options for a new :class:`AsyncClientSession`. @@ -528,9 +544,6 @@ def __init__( self._attached_to_cursor = False # Should we leave the session alive when the cursor is closed? self._leave_alive = False - # Is this session bound to a context manager scope? - self._bound = False - self._session_token: Optional[Token[_AsyncBoundClientSession]] = None async def end_session(self) -> None: """Finish this session. If a transaction has started, abort it. @@ -561,23 +574,18 @@ def _check_ended(self) -> None: if self._server_session is None: raise InvalidOperation("Cannot use ended session") - def bind(self) -> AsyncClientSession: - self._bound = True - return self + def bind(self) -> AsyncBoundSessionContext: + """Bind this session so it is implicitly passed to all database operations within the returned context. + + .. versionadded:: 4.17 + """ + return AsyncBoundSessionContext(self) async def __aenter__(self) -> AsyncClientSession: - if self._bound: - bound_session = _AsyncBoundClientSession(self, id(self._client)) - self._session_token = _SESSION.set(bound_session) # type: ignore[assignment] return self async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: - if self._session_token: - _SESSION.reset(self._session_token) # type: ignore[arg-type] - self._session_token = None - self._bound = False - else: - await self._end_session(lock=True) + await self._end_session(lock=True) @property def client(self) -> AsyncMongoClient[Any]: diff --git a/pymongo/synchronous/client_session.py b/pymongo/synchronous/client_session.py index 85ff79f99f..7dbfb7fe95 100644 --- a/pymongo/synchronous/client_session.py +++ b/pymongo/synchronous/client_session.py @@ -139,7 +139,7 @@ import time import uuid from collections.abc import Mapping as _Mapping -from contextvars import ContextVar +from contextvars import ContextVar, Token from typing import ( TYPE_CHECKING, Any, @@ -153,8 +153,6 @@ TypeVar, ) -from _contextvars import Token - from bson.binary import Binary from bson.int64 import Int64 from bson.timestamp import Timestamp @@ -192,6 +190,24 @@ def __init__(self, session: ClientSession, client_id: int): self.client_id = client_id +class BoundSessionContext: + """Context manager returned by ClientSession.bind() that manages bound state.""" + + def __init__(self, session: ClientSession) -> None: + self._session = session + self._session_token: Optional[Token[_BoundClientSession]] = None + + def __enter__(self) -> ClientSession: + bound_session = _BoundClientSession(self._session, id(self._session._client)) + self._session_token = _SESSION.set(bound_session) # type: ignore[assignment] + return self._session + + def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: + if self._session_token: + _SESSION.reset(self._session_token) # type: ignore[arg-type] + self._session_token = None + + class SessionOptions: """Options for a new :class:`ClientSession`. @@ -527,9 +543,6 @@ def __init__( self._attached_to_cursor = False # Should we leave the session alive when the cursor is closed? self._leave_alive = False - # Is this session bound to a context manager scope? - self._bound = False - self._session_token: Optional[Token[_BoundClientSession]] = None def end_session(self) -> None: """Finish this session. If a transaction has started, abort it. @@ -560,23 +573,18 @@ def _check_ended(self) -> None: if self._server_session is None: raise InvalidOperation("Cannot use ended session") - def bind(self) -> ClientSession: - self._bound = True - return self + def bind(self) -> BoundSessionContext: + """Bind this session so it is implicitly passed to all database operations within the returned context. + + .. versionadded:: 4.17 + """ + return BoundSessionContext(self) def __enter__(self) -> ClientSession: - if self._bound: - bound_session = _BoundClientSession(self, id(self._client)) - self._session_token = _SESSION.set(bound_session) # type: ignore[assignment] return self def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: - if self._session_token: - _SESSION.reset(self._session_token) # type: ignore[arg-type] - self._session_token = None - self._bound = False - else: - self._end_session(lock=True) + self._end_session(lock=True) @property def client(self) -> MongoClient[Any]: diff --git a/tools/synchro.py b/tools/synchro.py index d1056a41ed..18fb852ccf 100644 --- a/tools/synchro.py +++ b/tools/synchro.py @@ -38,6 +38,7 @@ "AsyncRawBatchCommandCursor": "RawBatchCommandCursor", "AsyncClientSession": "ClientSession", "_AsyncBoundClientSession": "_BoundClientSession", + "AsyncBoundSessionContext": "BoundSessionContext", "AsyncChangeStream": "ChangeStream", "AsyncCollectionChangeStream": "CollectionChangeStream", "AsyncDatabaseChangeStream": "DatabaseChangeStream", From 5edd9a0fba4a258156b43b7d3681bafb811478fc Mon Sep 17 00:00:00 2001 From: Noah Stapp Date: Tue, 3 Mar 2026 09:59:16 -0800 Subject: [PATCH 07/11] SH review --- pymongo/asynchronous/client_session.py | 19 ++++++------------- pymongo/asynchronous/mongo_client.py | 4 ++-- pymongo/synchronous/client_session.py | 13 +++---------- pymongo/synchronous/mongo_client.py | 4 ++-- tools/synchro.py | 3 +-- 5 files changed, 14 insertions(+), 29 deletions(-) diff --git a/pymongo/asynchronous/client_session.py b/pymongo/asynchronous/client_session.py index fe2dd07252..8c19aecca7 100644 --- a/pymongo/asynchronous/client_session.py +++ b/pymongo/asynchronous/client_session.py @@ -182,25 +182,18 @@ _IS_SYNC = False -_SESSION: ContextVar[Optional[_AsyncBoundClientSession]] = ContextVar("SESSION", default=None) +_SESSION: ContextVar[Optional[AsyncClientSession]] = ContextVar("SESSION", default=None) -class _AsyncBoundClientSession: - def __init__(self, session: AsyncClientSession, client_id: int): - self.session = session - self.client_id = client_id - - -class AsyncBoundSessionContext: +class _AsyncBoundSessionContext: """Context manager returned by AsyncClientSession.bind() that manages bound state.""" def __init__(self, session: AsyncClientSession) -> None: self._session = session - self._session_token: Optional[Token[_AsyncBoundClientSession]] = None + self._session_token: Optional[Token[AsyncClientSession]] = None async def __aenter__(self) -> AsyncClientSession: - bound_session = _AsyncBoundClientSession(self._session, id(self._session._client)) - self._session_token = _SESSION.set(bound_session) # type: ignore[assignment] + self._session_token = _SESSION.set(self._session) # type: ignore[assignment] return self._session async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: @@ -574,12 +567,12 @@ def _check_ended(self) -> None: if self._server_session is None: raise InvalidOperation("Cannot use ended session") - def bind(self) -> AsyncBoundSessionContext: + def bind(self) -> _AsyncBoundSessionContext: """Bind this session so it is implicitly passed to all database operations within the returned context. .. versionadded:: 4.17 """ - return AsyncBoundSessionContext(self) + return _AsyncBoundSessionContext(self) async def __aenter__(self) -> AsyncClientSession: return self diff --git a/pymongo/asynchronous/mongo_client.py b/pymongo/asynchronous/mongo_client.py index 52db120454..95f2e3746e 100644 --- a/pymongo/asynchronous/mongo_client.py +++ b/pymongo/asynchronous/mongo_client.py @@ -2308,8 +2308,8 @@ async def _process_response( def _get_bound_session(self) -> Optional[AsyncClientSession]: bound_session = _SESSION.get() if bound_session: - if bound_session.client_id == id(self): - return bound_session.session + if bound_session.client is self: + return bound_session else: raise InvalidOperation( "Only the client that created the bound session can perform operations within its context block. See for more information." diff --git a/pymongo/synchronous/client_session.py b/pymongo/synchronous/client_session.py index 7dbfb7fe95..e40b0c06ff 100644 --- a/pymongo/synchronous/client_session.py +++ b/pymongo/synchronous/client_session.py @@ -181,13 +181,7 @@ _IS_SYNC = True -_SESSION: ContextVar[Optional[_BoundClientSession]] = ContextVar("SESSION", default=None) - - -class _BoundClientSession: - def __init__(self, session: ClientSession, client_id: int): - self.session = session - self.client_id = client_id +_SESSION: ContextVar[Optional[ClientSession]] = ContextVar("SESSION", default=None) class BoundSessionContext: @@ -195,11 +189,10 @@ class BoundSessionContext: def __init__(self, session: ClientSession) -> None: self._session = session - self._session_token: Optional[Token[_BoundClientSession]] = None + self._session_token: Optional[Token[ClientSession]] = None def __enter__(self) -> ClientSession: - bound_session = _BoundClientSession(self._session, id(self._session._client)) - self._session_token = _SESSION.set(bound_session) # type: ignore[assignment] + self._session_token = _SESSION.set(self._session) # type: ignore[assignment] return self._session def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: diff --git a/pymongo/synchronous/mongo_client.py b/pymongo/synchronous/mongo_client.py index aa88d5b8b0..161a28d48d 100644 --- a/pymongo/synchronous/mongo_client.py +++ b/pymongo/synchronous/mongo_client.py @@ -2302,8 +2302,8 @@ def _process_response(self, reply: Mapping[str, Any], session: Optional[ClientSe def _get_bound_session(self) -> Optional[ClientSession]: bound_session = _SESSION.get() if bound_session: - if bound_session.client_id == id(self): - return bound_session.session + if bound_session.client is self: + return bound_session else: raise InvalidOperation( "Only the client that created the bound session can perform operations within its context block. See for more information." diff --git a/tools/synchro.py b/tools/synchro.py index 18fb852ccf..029a50a849 100644 --- a/tools/synchro.py +++ b/tools/synchro.py @@ -37,8 +37,7 @@ "AsyncRawBatchCursor": "RawBatchCursor", "AsyncRawBatchCommandCursor": "RawBatchCommandCursor", "AsyncClientSession": "ClientSession", - "_AsyncBoundClientSession": "_BoundClientSession", - "AsyncBoundSessionContext": "BoundSessionContext", + "_AsyncBoundSessionContext": "BoundSessionContext", "AsyncChangeStream": "ChangeStream", "AsyncCollectionChangeStream": "CollectionChangeStream", "AsyncDatabaseChangeStream": "DatabaseChangeStream", From 6eaf09430bced6b7465cebbd9094bb3962742ba5 Mon Sep 17 00:00:00 2001 From: Noah Stapp Date: Tue, 3 Mar 2026 11:34:16 -0800 Subject: [PATCH 08/11] Add end_session parameter to ClientSession.bind() --- pymongo/asynchronous/client_session.py | 11 ++++++++--- pymongo/synchronous/client_session.py | 11 ++++++++--- test/asynchronous/test_session.py | 16 ++++++++++++++++ test/test_session.py | 16 ++++++++++++++++ 4 files changed, 48 insertions(+), 6 deletions(-) diff --git a/pymongo/asynchronous/client_session.py b/pymongo/asynchronous/client_session.py index 8c19aecca7..555dad3351 100644 --- a/pymongo/asynchronous/client_session.py +++ b/pymongo/asynchronous/client_session.py @@ -188,9 +188,10 @@ class _AsyncBoundSessionContext: """Context manager returned by AsyncClientSession.bind() that manages bound state.""" - def __init__(self, session: AsyncClientSession) -> None: + def __init__(self, session: AsyncClientSession, end_session: bool) -> None: self._session = session self._session_token: Optional[Token[AsyncClientSession]] = None + self._end_session = end_session async def __aenter__(self) -> AsyncClientSession: self._session_token = _SESSION.set(self._session) # type: ignore[assignment] @@ -200,6 +201,8 @@ async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: if self._session_token: _SESSION.reset(self._session_token) # type: ignore[arg-type] self._session_token = None + if self._end_session: + await self._session.end_session() class SessionOptions: @@ -567,12 +570,14 @@ def _check_ended(self) -> None: if self._server_session is None: raise InvalidOperation("Cannot use ended session") - def bind(self) -> _AsyncBoundSessionContext: + def bind(self, end_session: bool = False) -> _AsyncBoundSessionContext: """Bind this session so it is implicitly passed to all database operations within the returned context. + :param end_session: Whether to end the session on exiting the returned context. Defaults to False. + .. versionadded:: 4.17 """ - return _AsyncBoundSessionContext(self) + return _AsyncBoundSessionContext(self, end_session) async def __aenter__(self) -> AsyncClientSession: return self diff --git a/pymongo/synchronous/client_session.py b/pymongo/synchronous/client_session.py index e40b0c06ff..560779d99d 100644 --- a/pymongo/synchronous/client_session.py +++ b/pymongo/synchronous/client_session.py @@ -187,9 +187,10 @@ class BoundSessionContext: """Context manager returned by ClientSession.bind() that manages bound state.""" - def __init__(self, session: ClientSession) -> None: + def __init__(self, session: ClientSession, end_session: bool) -> None: self._session = session self._session_token: Optional[Token[ClientSession]] = None + self._end_session = end_session def __enter__(self) -> ClientSession: self._session_token = _SESSION.set(self._session) # type: ignore[assignment] @@ -199,6 +200,8 @@ def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: if self._session_token: _SESSION.reset(self._session_token) # type: ignore[arg-type] self._session_token = None + if self._end_session: + self._session.end_session() class SessionOptions: @@ -566,12 +569,14 @@ def _check_ended(self) -> None: if self._server_session is None: raise InvalidOperation("Cannot use ended session") - def bind(self) -> BoundSessionContext: + def bind(self, end_session: bool = False) -> BoundSessionContext: """Bind this session so it is implicitly passed to all database operations within the returned context. + :param end_session: Whether to end the session on exiting the returned context. Defaults to False. + .. versionadded:: 4.17 """ - return BoundSessionContext(self) + return BoundSessionContext(self, end_session) def __enter__(self) -> ClientSession: return self diff --git a/test/asynchronous/test_session.py b/test/asynchronous/test_session.py index 3ef2f73376..bcfa43b39a 100644 --- a/test/asynchronous/test_session.py +++ b/test/asynchronous/test_session.py @@ -922,6 +922,22 @@ async def test_nested_session_binding(self): await session1.end_session() await session2.end_session() + async def test_session_binding_end_session(self): + coll = self.client.pymongo_test.test + await coll.insert_one({"x": 1}) + + async with self.client.start_session().bind(end_session=True) as s1: + await coll.find_one() + + self.assertTrue(s1.has_ended) + + async with self.client.start_session().bind() as s2: + await coll.find_one() + + self.assertFalse(s2.has_ended) + + await s2.end_session() + class TestCausalConsistency(AsyncUnitTest): listener: SessionTestListener diff --git a/test/test_session.py b/test/test_session.py index 4c58596930..d3cf9e5fb8 100644 --- a/test/test_session.py +++ b/test/test_session.py @@ -922,6 +922,22 @@ def test_nested_session_binding(self): session1.end_session() session2.end_session() + def test_session_binding_end_session(self): + coll = self.client.pymongo_test.test + coll.insert_one({"x": 1}) + + with self.client.start_session().bind(end_session=True) as s1: + coll.find_one() + + self.assertTrue(s1.has_ended) + + with self.client.start_session().bind() as s2: + coll.find_one() + + self.assertFalse(s2.has_ended) + + s2.end_session() + class TestCausalConsistency(UnitTest): listener: SessionTestListener From b0afe91acb816f614a6e507c3970641601a1369c Mon Sep 17 00:00:00 2001 From: Noah Stapp Date: Tue, 3 Mar 2026 14:54:40 -0800 Subject: [PATCH 09/11] SH review --- pymongo/synchronous/client_session.py | 6 +++--- tools/synchro.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pymongo/synchronous/client_session.py b/pymongo/synchronous/client_session.py index 560779d99d..f31c33102c 100644 --- a/pymongo/synchronous/client_session.py +++ b/pymongo/synchronous/client_session.py @@ -184,7 +184,7 @@ _SESSION: ContextVar[Optional[ClientSession]] = ContextVar("SESSION", default=None) -class BoundSessionContext: +class _BoundSessionContext: """Context manager returned by ClientSession.bind() that manages bound state.""" def __init__(self, session: ClientSession, end_session: bool) -> None: @@ -569,14 +569,14 @@ def _check_ended(self) -> None: if self._server_session is None: raise InvalidOperation("Cannot use ended session") - def bind(self, end_session: bool = False) -> BoundSessionContext: + def bind(self, end_session: bool = False) -> _BoundSessionContext: """Bind this session so it is implicitly passed to all database operations within the returned context. :param end_session: Whether to end the session on exiting the returned context. Defaults to False. .. versionadded:: 4.17 """ - return BoundSessionContext(self, end_session) + return _BoundSessionContext(self, end_session) def __enter__(self) -> ClientSession: return self diff --git a/tools/synchro.py b/tools/synchro.py index 029a50a849..ee719d7429 100644 --- a/tools/synchro.py +++ b/tools/synchro.py @@ -37,7 +37,7 @@ "AsyncRawBatchCursor": "RawBatchCursor", "AsyncRawBatchCommandCursor": "RawBatchCommandCursor", "AsyncClientSession": "ClientSession", - "_AsyncBoundSessionContext": "BoundSessionContext", + "_AsyncBoundSessionContext": "_BoundSessionContext", "AsyncChangeStream": "ChangeStream", "AsyncCollectionChangeStream": "CollectionChangeStream", "AsyncDatabaseChangeStream": "DatabaseChangeStream", From a1234ed9536bc7b9bd70462330d18250c68a3557 Mon Sep 17 00:00:00 2001 From: Noah Stapp Date: Tue, 3 Mar 2026 15:24:58 -0800 Subject: [PATCH 10/11] bind() defaults to endSession=True --- pymongo/asynchronous/client_session.py | 6 ++++-- pymongo/synchronous/client_session.py | 6 ++++-- test/asynchronous/test_session.py | 8 ++++---- test/test_session.py | 8 ++++---- 4 files changed, 16 insertions(+), 12 deletions(-) diff --git a/pymongo/asynchronous/client_session.py b/pymongo/asynchronous/client_session.py index 555dad3351..96fdeb1631 100644 --- a/pymongo/asynchronous/client_session.py +++ b/pymongo/asynchronous/client_session.py @@ -570,10 +570,12 @@ def _check_ended(self) -> None: if self._server_session is None: raise InvalidOperation("Cannot use ended session") - def bind(self, end_session: bool = False) -> _AsyncBoundSessionContext: + def bind(self, end_session: bool = True) -> _AsyncBoundSessionContext: """Bind this session so it is implicitly passed to all database operations within the returned context. - :param end_session: Whether to end the session on exiting the returned context. Defaults to False. + :param end_session: Whether to end the session on exiting the returned context. Defaults to True. + If set to False, :meth:`~pymongo.asynchronous.client_session.AsyncClientSession.end_session()` must be called + once the session is no longer used. .. versionadded:: 4.17 """ diff --git a/pymongo/synchronous/client_session.py b/pymongo/synchronous/client_session.py index f31c33102c..dcce9e5ebd 100644 --- a/pymongo/synchronous/client_session.py +++ b/pymongo/synchronous/client_session.py @@ -569,10 +569,12 @@ def _check_ended(self) -> None: if self._server_session is None: raise InvalidOperation("Cannot use ended session") - def bind(self, end_session: bool = False) -> _BoundSessionContext: + def bind(self, end_session: bool = True) -> _BoundSessionContext: """Bind this session so it is implicitly passed to all database operations within the returned context. - :param end_session: Whether to end the session on exiting the returned context. Defaults to False. + :param end_session: Whether to end the session on exiting the returned context. Defaults to True. + If set to False, :meth:`~pymongo.client_session.ClientSession.end_session()` must be called + once the session is no longer used. .. versionadded:: 4.17 """ diff --git a/test/asynchronous/test_session.py b/test/asynchronous/test_session.py index bcfa43b39a..404a69fdee 100644 --- a/test/asynchronous/test_session.py +++ b/test/asynchronous/test_session.py @@ -888,14 +888,14 @@ async def test_nested_session_binding(self): self.assertNotEqual(implicit_lsid, session1.session_id) self.assertNotEqual(implicit_lsid, session2.session_id) - async with session1.bind(): + async with session1.bind(end_session=False): self.listener.reset() # Uses bound session1 await coll.find_one() session1_lsid = self.listener.started_events[0].command.get("lsid") self.assertEqual(session1_lsid, session1.session_id) - async with session2.bind(): + async with session2.bind(end_session=False): self.listener.reset() # Uses bound session2 await coll.find_one() @@ -926,12 +926,12 @@ async def test_session_binding_end_session(self): coll = self.client.pymongo_test.test await coll.insert_one({"x": 1}) - async with self.client.start_session().bind(end_session=True) as s1: + async with self.client.start_session().bind() as s1: await coll.find_one() self.assertTrue(s1.has_ended) - async with self.client.start_session().bind() as s2: + async with self.client.start_session().bind(end_session=False) as s2: await coll.find_one() self.assertFalse(s2.has_ended) diff --git a/test/test_session.py b/test/test_session.py index d3cf9e5fb8..3963f88da0 100644 --- a/test/test_session.py +++ b/test/test_session.py @@ -888,14 +888,14 @@ def test_nested_session_binding(self): self.assertNotEqual(implicit_lsid, session1.session_id) self.assertNotEqual(implicit_lsid, session2.session_id) - with session1.bind(): + with session1.bind(end_session=False): self.listener.reset() # Uses bound session1 coll.find_one() session1_lsid = self.listener.started_events[0].command.get("lsid") self.assertEqual(session1_lsid, session1.session_id) - with session2.bind(): + with session2.bind(end_session=False): self.listener.reset() # Uses bound session2 coll.find_one() @@ -926,12 +926,12 @@ def test_session_binding_end_session(self): coll = self.client.pymongo_test.test coll.insert_one({"x": 1}) - with self.client.start_session().bind(end_session=True) as s1: + with self.client.start_session().bind() as s1: coll.find_one() self.assertTrue(s1.has_ended) - with self.client.start_session().bind() as s2: + with self.client.start_session().bind(end_session=False) as s2: coll.find_one() self.assertFalse(s2.has_ended) From e00ac1831e0d32a6940b8847dc29a39f9b192690 Mon Sep 17 00:00:00 2001 From: Noah Stapp Date: Wed, 4 Mar 2026 15:24:55 -0800 Subject: [PATCH 11/11] Add docstring example --- pymongo/asynchronous/client_session.py | 7 +++++++ pymongo/synchronous/client_session.py | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/pymongo/asynchronous/client_session.py b/pymongo/asynchronous/client_session.py index 96fdeb1631..c1e5a404d2 100644 --- a/pymongo/asynchronous/client_session.py +++ b/pymongo/asynchronous/client_session.py @@ -573,6 +573,13 @@ def _check_ended(self) -> None: def bind(self, end_session: bool = True) -> _AsyncBoundSessionContext: """Bind this session so it is implicitly passed to all database operations within the returned context. + .. code-block:: python + + async with client.start_session() as s: + async with s.bind(): + # session=s is passed implicitly + await client.db.collection.insert_one({"x": 1}) + :param end_session: Whether to end the session on exiting the returned context. Defaults to True. If set to False, :meth:`~pymongo.asynchronous.client_session.AsyncClientSession.end_session()` must be called once the session is no longer used. diff --git a/pymongo/synchronous/client_session.py b/pymongo/synchronous/client_session.py index dcce9e5ebd..5ef18a66bd 100644 --- a/pymongo/synchronous/client_session.py +++ b/pymongo/synchronous/client_session.py @@ -572,6 +572,13 @@ def _check_ended(self) -> None: def bind(self, end_session: bool = True) -> _BoundSessionContext: """Bind this session so it is implicitly passed to all database operations within the returned context. + .. code-block:: python + + with client.start_session() as s: + with s.bind(): + # session=s is passed implicitly + client.db.collection.insert_one({"x": 1}) + :param end_session: Whether to end the session on exiting the returned context. Defaults to True. If set to False, :meth:`~pymongo.client_session.ClientSession.end_session()` must be called once the session is no longer used.