From: Daniel Krzeminski Date: Tue, 22 Aug 2023 18:18:48 +0000 (-0400) Subject: Added an option to permanently close sessions. X-Git-Tag: rel_2_0_22~17^2 X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=8c72495e4dbd0e39968d0b99dcdede24f68fd9d1;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git Added an option to permanently close sessions. Set to ``False`` the new parameter :paramref:`_orm.Session.close_is_reset` will prevent a :class:`_orm.Session` from performing any other operation after :meth:`_orm.Session.close` has been called. Added new method :meth:`_orm.Session.reset` that will reset a :class:`_orm.Session` to its initial state. This is an alias of :meth:`_orm.Session.close`, unless :paramref:`_orm.Session.close_is_reset` is set to ``False``. Fixes: #7787 Closes: #10137 Pull-request: https://github.com/sqlalchemy/sqlalchemy/pull/10137 Pull-request-sha: 881241e19b24f34e4553b2d58070bfa99597f4e4 Change-Id: Ic3512874600daff4ed66bb0cd29a3a88f667d258 --- diff --git a/doc/build/changelog/unreleased_20/7787.rst b/doc/build/changelog/unreleased_20/7787.rst new file mode 100644 index 0000000000..4ba0c9c0b1 --- /dev/null +++ b/doc/build/changelog/unreleased_20/7787.rst @@ -0,0 +1,12 @@ +.. change:: + :tags: usecase, orm + :tickets: 7787 + + Added an option to permanently close sessions. + Set to ``False`` the new parameter :paramref:`_orm.Session.close_resets_only` + will prevent a :class:`_orm.Session` from performing any other + operation after :meth:`_orm.Session.close` has been called. + + Added new method :meth:`_orm.Session.reset` that will reset a :class:`_orm.Session` + to its initial state. This is an alias of :meth:`_orm.Session.close`, + unless :paramref:`_orm.Session.close_resets_only` is set to ``False``. \ No newline at end of file diff --git a/doc/build/orm/session_basics.rst b/doc/build/orm/session_basics.rst index 6cdb58e1a2..0fcbf7900b 100644 --- a/doc/build/orm/session_basics.rst +++ b/doc/build/orm/session_basics.rst @@ -723,10 +723,22 @@ transactional/connection resources from the :class:`_engine.Engine` object(s) to which it is bound. When connections are returned to the connection pool, transactional state is rolled back as well. -When the :class:`_orm.Session` is closed, it is essentially in the +By default, when the :class:`_orm.Session` is closed, it is essentially in the original state as when it was first constructed, and **may be used again**. In this sense, the :meth:`_orm.Session.close` method is more like a "reset" back to the clean state and not as much like a "database close" method. +In this mode of operation the method :meth:`_orm.Session.reset` is an alias to +:meth:`_orm.Session.close` and behaves in the same way. + +The default behavior of :meth:`_orm.Session.close` can be changed by setting the +parameter :paramref:`_orm.Session.close_resets_only` to ``False``, indicating that +the :class:`_orm.Session` cannot be reused after the method +:meth:`_orm.Session.close` has been called. In this mode of operation the +:meth:`_orm.Session.reset` method will allow multiple "reset" of the session, +behaving like :meth:`_orm.Session.close` when +:paramref:`_orm.Session.close_resets_only` is set to ``True``. + +.. versionadded:: 2.0.22 It's recommended that the scope of a :class:`_orm.Session` be limited by a call to :meth:`_orm.Session.close` at the end, especially if the diff --git a/lib/sqlalchemy/ext/asyncio/scoping.py b/lib/sqlalchemy/ext/asyncio/scoping.py index b70c3366b1..7c0ce06f1c 100644 --- a/lib/sqlalchemy/ext/asyncio/scoping.py +++ b/lib/sqlalchemy/ext/asyncio/scoping.py @@ -77,6 +77,7 @@ _T = TypeVar("_T", bound=Any) "begin", "begin_nested", "close", + "reset", "commit", "connection", "delete", @@ -447,33 +448,44 @@ class async_scoped_session(Generic[_AS]): Proxied for the :class:`_asyncio.AsyncSession` class on behalf of the :class:`_asyncio.scoping.async_scoped_session` class. - This expunges all ORM objects associated with this - :class:`_asyncio.AsyncSession`, ends any transaction in progress and - :term:`releases` any :class:`_asyncio.AsyncConnection` objects which - this :class:`_asyncio.AsyncSession` itself has checked out from - associated :class:`_asyncio.AsyncEngine` objects. The operation then - leaves the :class:`_asyncio.AsyncSession` in a state which it may be - used again. + .. seealso:: + + :meth:`_orm.Session.close` - main documentation for + "close" + + :ref:`session_closing` - detail on the semantics of + :meth:`_asyncio.AsyncSession.close` and + :meth:`_asyncio.AsyncSession.reset`. + + + """ # noqa: E501 + + return await self._proxied.close() + + async def reset(self) -> None: + r"""Close out the transactional resources and ORM objects used by this + :class:`_orm.Session`, resetting the session to its initial state. - .. tip:: + .. container:: class_bases - The :meth:`_asyncio.AsyncSession.close` method **does not prevent - the Session from being used again**. The - :class:`_asyncio.AsyncSession` itself does not actually have a - distinct "closed" state; it merely means the - :class:`_asyncio.AsyncSession` will release all database - connections and ORM objects. + Proxied for the :class:`_asyncio.AsyncSession` class on + behalf of the :class:`_asyncio.scoping.async_scoped_session` class. + .. versionadded:: 2.0.22 .. seealso:: + :meth:`_orm.Session.reset` - main documentation for + "reset" + :ref:`session_closing` - detail on the semantics of - :meth:`_asyncio.AsyncSession.close` + :meth:`_asyncio.AsyncSession.close` and + :meth:`_asyncio.AsyncSession.reset`. """ # noqa: E501 - return await self._proxied.close() + return await self._proxied.reset() async def commit(self) -> None: r"""Commit the current transaction in progress. @@ -483,6 +495,11 @@ class async_scoped_session(Generic[_AS]): Proxied for the :class:`_asyncio.AsyncSession` class on behalf of the :class:`_asyncio.scoping.async_scoped_session` class. + .. seealso:: + + :meth:`_orm.Session.commit` - main documentation for + "commit" + """ # noqa: E501 return await self._proxied.commit() @@ -1014,6 +1031,11 @@ class async_scoped_session(Generic[_AS]): Proxied for the :class:`_asyncio.AsyncSession` class on behalf of the :class:`_asyncio.scoping.async_scoped_session` class. + .. seealso:: + + :meth:`_orm.Session.rollback` - main documentation for + "rollback" + """ # noqa: E501 return await self._proxied.rollback() diff --git a/lib/sqlalchemy/ext/asyncio/session.py b/lib/sqlalchemy/ext/asyncio/session.py index da69c4fb3e..d5b439fcb7 100644 --- a/lib/sqlalchemy/ext/asyncio/session.py +++ b/lib/sqlalchemy/ext/asyncio/session.py @@ -951,42 +951,58 @@ class AsyncSession(ReversibleProxy[Session]): return AsyncSessionTransaction(self, nested=True) async def rollback(self) -> None: - """Rollback the current transaction in progress.""" + """Rollback the current transaction in progress. + + .. seealso:: + + :meth:`_orm.Session.rollback` - main documentation for + "rollback" + """ await greenlet_spawn(self.sync_session.rollback) async def commit(self) -> None: - """Commit the current transaction in progress.""" + """Commit the current transaction in progress. + + .. seealso:: + + :meth:`_orm.Session.commit` - main documentation for + "commit" + """ await greenlet_spawn(self.sync_session.commit) async def close(self) -> None: """Close out the transactional resources and ORM objects used by this :class:`_asyncio.AsyncSession`. - This expunges all ORM objects associated with this - :class:`_asyncio.AsyncSession`, ends any transaction in progress and - :term:`releases` any :class:`_asyncio.AsyncConnection` objects which - this :class:`_asyncio.AsyncSession` itself has checked out from - associated :class:`_asyncio.AsyncEngine` objects. The operation then - leaves the :class:`_asyncio.AsyncSession` in a state which it may be - used again. + .. seealso:: + + :meth:`_orm.Session.close` - main documentation for + "close" - .. tip:: + :ref:`session_closing` - detail on the semantics of + :meth:`_asyncio.AsyncSession.close` and + :meth:`_asyncio.AsyncSession.reset`. - The :meth:`_asyncio.AsyncSession.close` method **does not prevent - the Session from being used again**. The - :class:`_asyncio.AsyncSession` itself does not actually have a - distinct "closed" state; it merely means the - :class:`_asyncio.AsyncSession` will release all database - connections and ORM objects. + """ + await greenlet_spawn(self.sync_session.close) + async def reset(self) -> None: + """Close out the transactional resources and ORM objects used by this + :class:`_orm.Session`, resetting the session to its initial state. + + .. versionadded:: 2.0.22 .. seealso:: + :meth:`_orm.Session.reset` - main documentation for + "reset" + :ref:`session_closing` - detail on the semantics of - :meth:`_asyncio.AsyncSession.close` + :meth:`_asyncio.AsyncSession.close` and + :meth:`_asyncio.AsyncSession.reset`. """ - await greenlet_spawn(self.sync_session.close) + await greenlet_spawn(self.sync_session.reset) async def aclose(self) -> None: """A synonym for :meth:`_asyncio.AsyncSession.close`. diff --git a/lib/sqlalchemy/orm/scoping.py b/lib/sqlalchemy/orm/scoping.py index fc144d98c4..c1ef8871f1 100644 --- a/lib/sqlalchemy/orm/scoping.py +++ b/lib/sqlalchemy/orm/scoping.py @@ -108,6 +108,7 @@ __all__ = ["scoped_session"] "begin", "begin_nested", "close", + "reset", "commit", "connection", "delete", @@ -491,12 +492,17 @@ class scoped_session(Generic[_S]): .. tip:: - The :meth:`_orm.Session.close` method **does not prevent the - Session from being used again**. The :class:`_orm.Session` itself - does not actually have a distinct "closed" state; it merely means + In the default running mode the :meth:`_orm.Session.close` + method **does not prevent the Session from being used again**. + The :class:`_orm.Session` itself does not actually have a + distinct "closed" state; it merely means the :class:`_orm.Session` will release all database connections and ORM objects. + Setting the parameter :paramref:`_orm.Session.close_resets_only` + to ``False`` will instead make the ``close`` final, meaning that + any further action on the session will be forbidden. + .. versionchanged:: 1.4 The :meth:`.Session.close` method does not immediately create a new :class:`.SessionTransaction` object; instead, the new :class:`.SessionTransaction` is created only if @@ -505,13 +511,49 @@ class scoped_session(Generic[_S]): .. seealso:: :ref:`session_closing` - detail on the semantics of - :meth:`_orm.Session.close` + :meth:`_orm.Session.close` and :meth:`_orm.Session.reset`. + + :meth:`_orm.Session.reset` - a similar method that behaves like + ``close()`` with the parameter + :paramref:`_orm.Session.close_resets_only` set to ``True``. """ # noqa: E501 return self._proxied.close() + def reset(self) -> None: + r"""Close out the transactional resources and ORM objects used by this + :class:`_orm.Session`, resetting the session to its initial state. + + .. container:: class_bases + + Proxied for the :class:`_orm.Session` class on + behalf of the :class:`_orm.scoping.scoped_session` class. + + This method provides for same "reset-only" behavior that the + :meth:_orm.Session.close method has provided historically, where the + state of the :class:`_orm.Session` is reset as though the object were + brand new, and ready to be used again. + The method may then be useful for :class:`_orm.Session` objects + which set :paramref:`_orm.Session.close_resets_only` to ``False``, + so that "reset only" behavior is still available from this method. + + .. versionadded:: 2.0.22 + + .. seealso:: + + :ref:`session_closing` - detail on the semantics of + :meth:`_orm.Session.close` and :meth:`_orm.Session.reset`. + + :meth:`_orm.Session.close` - a similar method will additionally + prevent re-use of the Session when the parameter + :paramref:`_orm.Session.close_resets_only` is set to ``False``. + + """ # noqa: E501 + + return self._proxied.reset() + def commit(self) -> None: r"""Flush pending changes and commit the current transaction. diff --git a/lib/sqlalchemy/orm/session.py b/lib/sqlalchemy/orm/session.py index e5eb5036dd..96cd608ac4 100644 --- a/lib/sqlalchemy/orm/session.py +++ b/lib/sqlalchemy/orm/session.py @@ -83,6 +83,7 @@ from ..sql import roles from ..sql import Select from ..sql import TableClause from ..sql import visitors +from ..sql.base import _NoArg from ..sql.base import CompileState from ..sql.schema import Table from ..sql.selectable import ForUpdateArg @@ -881,6 +882,12 @@ class SessionTransaction(_StateChange, TransactionalContext): self.nested = nested = origin is SessionTransactionOrigin.BEGIN_NESTED self.origin = origin + if session._close_state is _SessionCloseState.CLOSED: + raise sa_exc.InvalidRequestError( + "This Session has been permanently closed and is unable " + "to handle any more transaction requests." + ) + if nested: if not parent: raise sa_exc.InvalidRequestError( @@ -1372,6 +1379,12 @@ class SessionTransaction(_StateChange, TransactionalContext): return self._state not in (COMMITTED, CLOSED) +class _SessionCloseState(Enum): + ACTIVE = 1 + CLOSED = 2 + CLOSE_IS_RESET = 3 + + class Session(_SessionClassMethods, EventTarget): """Manages persistence operations for ORM-mapped objects. @@ -1416,6 +1429,7 @@ class Session(_SessionClassMethods, EventTarget): twophase: bool join_transaction_mode: JoinTransactionMode _query_cls: Type[Query[Any]] + _close_state: _SessionCloseState def __init__( self, @@ -1432,6 +1446,7 @@ class Session(_SessionClassMethods, EventTarget): query_cls: Optional[Type[Query[Any]]] = None, autocommit: Literal[False] = False, join_transaction_mode: JoinTransactionMode = "conditional_savepoint", + close_resets_only: Union[bool, _NoArg] = _NoArg.NO_ARG, ): r"""Construct a new :class:`_orm.Session`. @@ -1639,6 +1654,18 @@ class Session(_SessionClassMethods, EventTarget): .. versionadded:: 2.0.0rc1 + :param close_resets_only: Defaults to ``True``. Determines if + the session should reset itself after calling ``.close()`` + or should pass in a no longer usable state, disabling re-use. + + .. versionadded:: 2.0.22 added flag ``close_resets_only``. + A future SQLAlchemy version may change the default value of + this flag to ``False``. + + .. seealso:: + + :ref:`session_closing` - Detail on the semantics of + :meth:`_orm.Session.close` and :meth:`_orm.Session.reset`. """ # noqa @@ -1671,6 +1698,13 @@ class Session(_SessionClassMethods, EventTarget): self.autoflush = autoflush self.expire_on_commit = expire_on_commit self.enable_baked_queries = enable_baked_queries + + # the idea is that at some point NO_ARG will warn that in the future + # the default will switch to close_resets_only=False. + if close_resets_only or close_resets_only is _NoArg.NO_ARG: + self._close_state = _SessionCloseState.CLOSE_IS_RESET + else: + self._close_state = _SessionCloseState.ACTIVE if ( join_transaction_mode and join_transaction_mode @@ -2393,12 +2427,17 @@ class Session(_SessionClassMethods, EventTarget): .. tip:: - The :meth:`_orm.Session.close` method **does not prevent the - Session from being used again**. The :class:`_orm.Session` itself - does not actually have a distinct "closed" state; it merely means + In the default running mode the :meth:`_orm.Session.close` + method **does not prevent the Session from being used again**. + The :class:`_orm.Session` itself does not actually have a + distinct "closed" state; it merely means the :class:`_orm.Session` will release all database connections and ORM objects. + Setting the parameter :paramref:`_orm.Session.close_resets_only` + to ``False`` will instead make the ``close`` final, meaning that + any further action on the session will be forbidden. + .. versionchanged:: 1.4 The :meth:`.Session.close` method does not immediately create a new :class:`.SessionTransaction` object; instead, the new :class:`.SessionTransaction` is created only if @@ -2407,11 +2446,40 @@ class Session(_SessionClassMethods, EventTarget): .. seealso:: :ref:`session_closing` - detail on the semantics of - :meth:`_orm.Session.close` + :meth:`_orm.Session.close` and :meth:`_orm.Session.reset`. + + :meth:`_orm.Session.reset` - a similar method that behaves like + ``close()`` with the parameter + :paramref:`_orm.Session.close_resets_only` set to ``True``. """ self._close_impl(invalidate=False) + def reset(self) -> None: + """Close out the transactional resources and ORM objects used by this + :class:`_orm.Session`, resetting the session to its initial state. + + This method provides for same "reset-only" behavior that the + :meth:_orm.Session.close method has provided historically, where the + state of the :class:`_orm.Session` is reset as though the object were + brand new, and ready to be used again. + The method may then be useful for :class:`_orm.Session` objects + which set :paramref:`_orm.Session.close_resets_only` to ``False``, + so that "reset only" behavior is still available from this method. + + .. versionadded:: 2.0.22 + + .. seealso:: + + :ref:`session_closing` - detail on the semantics of + :meth:`_orm.Session.close` and :meth:`_orm.Session.reset`. + + :meth:`_orm.Session.close` - a similar method will additionally + prevent re-use of the Session when the parameter + :paramref:`_orm.Session.close_resets_only` is set to ``False``. + """ + self._close_impl(invalidate=False, is_reset=True) + def invalidate(self) -> None: """Close this Session, using connection invalidation. @@ -2448,7 +2516,9 @@ class Session(_SessionClassMethods, EventTarget): """ self._close_impl(invalidate=True) - def _close_impl(self, invalidate: bool) -> None: + def _close_impl(self, invalidate: bool, is_reset: bool = False) -> None: + if not is_reset and self._close_state is _SessionCloseState.ACTIVE: + self._close_state = _SessionCloseState.CLOSED self.expunge_all() if self._transaction is not None: for transaction in self._transaction._iterate_self_and_parents(): diff --git a/test/ext/asyncio/test_session_py3k.py b/test/ext/asyncio/test_session_py3k.py index 8fa174eeba..9d17d0b1dd 100644 --- a/test/ext/asyncio/test_session_py3k.py +++ b/test/ext/asyncio/test_session_py3k.py @@ -891,6 +891,11 @@ class AsyncProxyTest(AsyncFixture): is_(async_object_session(u3), None) + await s2.reset() + is_(async_object_session(u2), None) + s2.add(u2) + + is_(async_object_session(u2), s2) await s2.close() is_(async_object_session(u2), None) diff --git a/test/orm/test_session.py b/test/orm/test_session.py index b304ac5745..cbe5eca469 100644 --- a/test/orm/test_session.py +++ b/test/orm/test_session.py @@ -7,6 +7,7 @@ from typing import TYPE_CHECKING import sqlalchemy as sa from sqlalchemy import delete from sqlalchemy import event +from sqlalchemy import exc from sqlalchemy import ForeignKey from sqlalchemy import insert from sqlalchemy import inspect @@ -115,14 +116,14 @@ class ExecutionTest(_fixtures.FixtureTest): def test_no_string_execute(self, connection): with Session(bind=connection) as sess: with expect_raises_message( - sa.exc.ArgumentError, + exc.ArgumentError, r"Textual SQL expression 'select \* from users where.*' " "should be explicitly declared", ): sess.execute("select * from users where id=:id", {"id": 7}) with expect_raises_message( - sa.exc.ArgumentError, + exc.ArgumentError, r"Textual SQL expression 'select id from users .*' " "should be explicitly declared", ): @@ -253,7 +254,7 @@ class TransScopingTest(_fixtures.FixtureTest): orm_trigger = trigger == "lazyload" or trigger == "unitofwork" with expect_raises_message( - sa.exc.InvalidRequestError, + exc.InvalidRequestError, r"Autobegin is disabled on this Session; please call " r"session.begin\(\) to start a new transaction", ): @@ -304,7 +305,7 @@ class TransScopingTest(_fixtures.FixtureTest): s.begin() # OK assert_raises_message( - sa.exc.InvalidRequestError, + exc.InvalidRequestError, "A transaction is already begun on this Session.", s.begin, ) @@ -504,7 +505,7 @@ class SessionUtilTest(_fixtures.FixtureTest): sess.flush() assert u1 not in sess - assert_raises(sa.exc.InvalidRequestError, sess.add, u1) + assert_raises(exc.InvalidRequestError, sess.add, u1) make_transient(u1) sess.add(u1) sess.flush() @@ -550,7 +551,7 @@ class SessionUtilTest(_fixtures.FixtureTest): u1 = User(id=1, name="test") sess.add(u1) assert_raises_message( - sa.exc.InvalidRequestError, + exc.InvalidRequestError, "Given object must be transient", make_transient_to_detached, u1, @@ -566,7 +567,7 @@ class SessionUtilTest(_fixtures.FixtureTest): sess.commit() sess.expunge(u1) assert_raises_message( - sa.exc.InvalidRequestError, + exc.InvalidRequestError, "Given object must be transient", make_transient_to_detached, u1, @@ -640,7 +641,7 @@ class SessionStateTest(_fixtures.FixtureTest): Session(autocommit=False) with expect_raises_message( - sa.exc.ArgumentError, "autocommit=True is no longer supported" + exc.ArgumentError, "autocommit=True is no longer supported" ): Session(autocommit=True) @@ -721,7 +722,7 @@ class SessionStateTest(_fixtures.FixtureTest): # will raise for null email address assert_raises_message( - sa.exc.DBAPIError, + exc.DBAPIError, ".*raised as a result of Query-invoked autoflush; consider using " "a session.no_autoflush block.*", s.query(User).first, @@ -741,7 +742,7 @@ class SessionStateTest(_fixtures.FixtureTest): sess.delete(u1) sess.flush() assert u1 not in sess - assert_raises(sa.exc.InvalidRequestError, sess.add, u1) + assert_raises(exc.InvalidRequestError, sess.add, u1) assert sess.in_transaction() sess.rollback() assert u1 in sess @@ -749,7 +750,7 @@ class SessionStateTest(_fixtures.FixtureTest): sess.delete(u1) sess.commit() assert u1 not in sess - assert_raises(sa.exc.InvalidRequestError, sess.add, u1) + assert_raises(exc.InvalidRequestError, sess.add, u1) make_transient(u1) sess.add(u1) @@ -909,7 +910,7 @@ class SessionStateTest(_fixtures.FixtureTest): user = User(name="u1") assert_raises_message( - sa.exc.InvalidRequestError, "is not persisted", s.delete, user + exc.InvalidRequestError, "is not persisted", s.delete, user ) s.add(user) @@ -937,7 +938,7 @@ class SessionStateTest(_fixtures.FixtureTest): s2 = fixture_session() assert_raises_message( - sa.exc.InvalidRequestError, + exc.InvalidRequestError, "is already attached to session", s2.delete, user, @@ -945,7 +946,7 @@ class SessionStateTest(_fixtures.FixtureTest): u2 = s2.get(User, user.id) s2.expunge(u2) assert_raises_message( - sa.exc.InvalidRequestError, + exc.InvalidRequestError, "another instance .* is already present", s.delete, u2, @@ -973,7 +974,7 @@ class SessionStateTest(_fixtures.FixtureTest): s1.add(u1) assert_raises_message( - sa.exc.InvalidRequestError, + exc.InvalidRequestError, "Object '' is already attached to session", s2.add, u1, @@ -997,7 +998,7 @@ class SessionStateTest(_fixtures.FixtureTest): s.identity_map.add(sa.orm.attributes.instance_state(u1)) assert_raises_message( - sa.exc.InvalidRequestError, + exc.InvalidRequestError, "Can't attach instance ; another instance " "with key .*? is already " "present in this session.", @@ -1057,7 +1058,7 @@ class SessionStateTest(_fixtures.FixtureTest): u1 = User(name="u1") sess1.add(u1) assert_raises_message( - sa.exc.InvalidRequestError, + exc.InvalidRequestError, "already attached to session", sess2.add, u1, @@ -1087,7 +1088,7 @@ class SessionStateTest(_fixtures.FixtureTest): assert u2 in sess assert_raises_message( - sa.exc.InvalidRequestError, + exc.InvalidRequestError, "Can't attach instance ; another instance " "with key .*? is already " "present in this session.", @@ -1176,7 +1177,7 @@ class SessionStateTest(_fixtures.FixtureTest): def test_extra_dirty_state_post_flush_warning(self): s, a1, a2 = self._test_extra_dirty_state() assert_warns_message( - sa.exc.SAWarning, + exc.SAWarning, "Attribute history events accumulated on 1 previously " "clean instances", s.commit, @@ -1264,6 +1265,47 @@ class SessionStateTest(_fixtures.FixtureTest): assert u1 not in sess assert object_session(u1) is None + @testing.combinations(True, False, "default", argnames="close_resets_only") + @testing.variation("method", ["close", "reset"]) + def test_session_close_resets_only(self, close_resets_only, method): + users, User = self.tables.users, self.classes.User + self.mapper_registry.map_imperatively(User, users) + + if close_resets_only is True: + kw = {"close_resets_only": True} + elif close_resets_only is False: + kw = {"close_resets_only": False} + else: + eq_(close_resets_only, "default") + kw = {} + + s = fixture_session(**kw) + u1 = User() + s.add(u1) + assertions.in_(u1, s) + + if method.reset: + s.reset() + elif method.close: + s.close() + else: + method.fail() + + assertions.not_in(u1, s) + + u2 = User() + if method.close and close_resets_only is False: + with expect_raises_message( + exc.InvalidRequestError, + "This Session has been permanently closed and is unable " + "to handle any more transaction requests.", + ): + s.add(u2) + assertions.not_in(u2, s) + else: + s.add(u2) + assertions.in_(u2, s) + class DeferredRelationshipExpressionTest(_fixtures.FixtureTest): run_inserts = None @@ -1345,7 +1387,7 @@ class DeferredRelationshipExpressionTest(_fixtures.FixtureTest): u = User(name="ed", addresses=[Address(email_address="foo")]) assert_raises_message( - sa.exc.InvalidRequestError, + exc.InvalidRequestError, "Can't resolve value for column users.id on object " ".User.*.; no value has been set for this column", (Address.user == u).left.callable, @@ -1353,7 +1395,7 @@ class DeferredRelationshipExpressionTest(_fixtures.FixtureTest): q = sess.query(Address).filter(Address.user == u) assert_raises_message( - sa.exc.StatementError, + exc.StatementError, "Can't resolve value for column users.id on object " ".User.*.; no value has been set for this column", q.one, @@ -1440,7 +1482,7 @@ class DeferredRelationshipExpressionTest(_fixtures.FixtureTest): sess.expunge(u) assert_raises_message( - sa.exc.StatementError, + exc.StatementError, "Can't resolve value for column users.id on object " ".User.*.; the object is detached and the value was expired", q.one, @@ -1690,7 +1732,7 @@ class WeakIdentityMapTest(_fixtures.FixtureTest): # already belongs to u2 s2 = Session(testing.db) assert_raises_message( - sa.exc.InvalidRequestError, + exc.InvalidRequestError, r".*is already attached to session", s2.add, u1, @@ -2035,7 +2077,7 @@ class SessionInterface(fixtures.MappedTest): ) else: assert_raises( - sa.exc.NoInspectionAvailable, callable_, *args, **kw + exc.NoInspectionAvailable, callable_, *args, **kw ) raises_("connection", bind_arguments=dict(mapper=user_arg)) @@ -2062,7 +2104,7 @@ class SessionInterface(fixtures.MappedTest): def test_join_transaction_mode(self): with expect_raises_message( - sa.exc.ArgumentError, + exc.ArgumentError, 'invalid selection for join_transaction_mode: "bogus"', ): Session(join_transaction_mode="bogus") @@ -2116,7 +2158,7 @@ class SessionInterface(fixtures.MappedTest): with mock.patch.object(s, "_validate_persistent"): assert_raises_message( - sa.exc.ArgumentError, + exc.ArgumentError, "with_for_update should be the boolean value True, " "or a dictionary with options", s.refresh, @@ -2168,7 +2210,7 @@ class NewStyleExecutionTest(_fixtures.FixtureTest): getattr(sess, meth)() with expect_raises_message( - sa.exc.InvalidRequestError, + exc.InvalidRequestError, "Object .*User.* cannot be converted to 'persistent' state, " "as this identity map is no longer valid.", ): @@ -2460,7 +2502,7 @@ class FlushWarningsTest(fixtures.MappedTest): object_session(instance).delete(Address(email="x1")) with expect_raises_message( - sa.exc.InvalidRequestError, ".*is not persisted" + exc.InvalidRequestError, ".*is not persisted" ): self._test(evt, r"Session.delete\(\)")