diff options
Diffstat (limited to 'lib/sqlalchemy/orm/session.py')
-rw-r--r-- | lib/sqlalchemy/orm/session.py | 152 |
1 files changed, 110 insertions, 42 deletions
diff --git a/lib/sqlalchemy/orm/session.py b/lib/sqlalchemy/orm/session.py index 5a4486eef..c10a0efc9 100644 --- a/lib/sqlalchemy/orm/session.py +++ b/lib/sqlalchemy/orm/session.py @@ -1,5 +1,5 @@ # orm/session.py -# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors <see AUTHORS file> +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors <see AUTHORS file> # # This module is part of SQLAlchemy and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php @@ -8,25 +8,40 @@ import weakref -from .. import util, sql, engine, exc as sa_exc, event +from .. import util, sql, engine, exc as sa_exc from ..sql import util as sql_util, expression from . import ( - SessionExtension, attributes, exc, query, util as orm_util, + SessionExtension, attributes, exc, query, loading, identity ) -from .util import ( +from ..inspection import inspect +from .base import ( object_mapper, class_mapper, _class_to_mapper, _state_mapper, object_state, - _none_set + _none_set, state_str, instance_str ) from .unitofwork import UOWTransaction -from .mapper import Mapper -from .events import SessionEvents -statelib = util.importlater("sqlalchemy.orm", "state") +from . import state as statelib import sys __all__ = ['Session', 'SessionTransaction', 'SessionExtension', 'sessionmaker'] +_sessions = weakref.WeakValueDictionary() +"""Weak-referencing dictionary of :class:`.Session` objects. +""" + +def _state_session(state): + """Given an :class:`.InstanceState`, return the :class:`.Session` + associated, if any. + """ + if state.session_id: + try: + return _sessions[state.session_id] + except KeyError: + pass + return None + + class _SessionClassMethods(object): """Class-level methods for :class:`.Session`, :class:`.sessionmaker`.""" @@ -39,7 +54,8 @@ class _SessionClassMethods(object): sess.close() @classmethod - def identity_key(cls, *args, **kwargs): + @util.dependencies("sqlalchemy.orm.util") + def identity_key(cls, orm_util, *args, **kwargs): """Return an identity key. This is an alias of :func:`.util.identity_key`. @@ -469,6 +485,7 @@ class Session(_SessionClassMethods): _enable_transaction_accounting=True, autocommit=False, twophase=False, weak_identity_map=True, binds=None, extension=None, + info=None, query_cls=query.Query): """Construct a new Session. @@ -557,6 +574,14 @@ class Session(_SessionClassMethods): flush events, as well as a post-rollback event. **Deprecated.** Please see :class:`.SessionEvents`. + :param info: optional dictionary of arbitrary data to be associated + with this :class:`.Session`. Is available via the :attr:`.Session.info` + attribute. Note the dictionary is copied at construction time so + that modifications to the per-:class:`.Session` dictionary will be local + to that :class:`.Session`. + + .. versionadded:: 0.9.0 + :param query_cls: Class which should be used to create new Query objects, as returned by the ``query()`` method. Defaults to :class:`~sqlalchemy.orm.query.Query`. @@ -599,6 +624,8 @@ class Session(_SessionClassMethods): self._enable_transaction_accounting = _enable_transaction_accounting self.twophase = twophase self._query_cls = query_cls + if info: + self.info.update(info) if extension: for ext in util.to_list(extension): @@ -606,22 +633,39 @@ class Session(_SessionClassMethods): if binds is not None: for mapperortable, bind in binds.items(): - if isinstance(mapperortable, (type, Mapper)): + insp = inspect(mapperortable) + if insp.is_selectable: + self.bind_table(mapperortable, bind) + elif insp.is_mapper: self.bind_mapper(mapperortable, bind) else: - self.bind_table(mapperortable, bind) + assert False + if not self.autocommit: self.begin() _sessions[self.hash_key] = self - dispatch = event.dispatcher(SessionEvents) - connection_callable = None transaction = None """The current active or inactive :class:`.SessionTransaction`.""" + @util.memoized_property + def info(self): + """A user-modifiable dictionary. + + The initial value of this dictioanry can be populated using the + ``info`` argument to the :class:`.Session` constructor or + :class:`.sessionmaker` constructor or factory methods. The dictionary + here is always local to this :class:`.Session` and can be modified + independently of all other :class:`.Session` objects. + + .. versionadded:: 0.9.0 + + """ + return {} + def begin(self, subtransactions=False, nested=False): """Begin a transaction on this Session. @@ -779,7 +823,7 @@ class Session(_SessionClassMethods): etc.) which will be used to locate a bind, if a bind cannot otherwise be identified. - :param close_with_result: Passed to :meth:`Engine.connect`, indicating + :param close_with_result: Passed to :meth:`.Engine.connect`, indicating the :class:`.Connection` should be considered "single use", automatically closing when the first result set is closed. This flag only has an effect if this :class:`.Session` is configured with @@ -1136,7 +1180,18 @@ class Session(_SessionClassMethods): def _autoflush(self): if self.autoflush and not self._flushing: - self.flush() + try: + self.flush() + except sa_exc.StatementError as e: + # note we are reraising StatementError as opposed to + # raising FlushError with "chaining" to remain compatible + # with code that catches StatementError, IntegrityError, + # etc. + e.add_detail( + "raised as a result of Query-invoked autoflush; " + "consider using a session.no_autoflush block if this " + "flush is occuring prematurely") + util.raise_from_cause(e) def refresh(self, instance, attribute_names=None, lockmode=None): """Expire and refresh the attributes on the given instance. @@ -1180,7 +1235,7 @@ class Session(_SessionClassMethods): only_load_props=attribute_names) is None: raise sa_exc.InvalidRequestError( "Could not refresh instance '%s'" % - orm_util.instance_str(instance)) + instance_str(instance)) def expire_all(self): """Expires all persistent instances within this Session. @@ -1291,7 +1346,7 @@ class Session(_SessionClassMethods): if state.session_id is not self.hash_key: raise sa_exc.InvalidRequestError( "Instance %s is not present in this Session" % - orm_util.state_str(state)) + state_str(state)) cascaded = list(state.manager.mapper.cascade_iterator( 'expunge', state)) @@ -1331,7 +1386,7 @@ class Session(_SessionClassMethods): "expect these generated values. Ensure also that " "this flush() is not occurring at an inappropriate " "time, such aswithin a load() event." - % orm_util.state_str(state) + % state_str(state) ) if state.key is None: @@ -1434,7 +1489,7 @@ class Session(_SessionClassMethods): if state.key is None: raise sa_exc.InvalidRequestError( "Instance '%s' is not persisted" % - orm_util.state_str(state)) + state_str(state)) if state in self._deleted: return @@ -1598,7 +1653,7 @@ class Session(_SessionClassMethods): "merging to update the most recent version." % ( existing_version, - orm_util.state_str(merged_state), + state_str(merged_state), merged_version )) @@ -1622,13 +1677,13 @@ class Session(_SessionClassMethods): if not self.identity_map.contains_state(state): raise sa_exc.InvalidRequestError( "Instance '%s' is not persistent within this Session" % - orm_util.state_str(state)) + state_str(state)) def _save_impl(self, state): if state.key is not None: raise sa_exc.InvalidRequestError( "Object '%s' already has an identity - it can't be registered " - "as pending" % orm_util.state_str(state)) + "as pending" % state_str(state)) self._before_attach(state) if state not in self._new: @@ -1644,13 +1699,13 @@ class Session(_SessionClassMethods): if state.key is None: raise sa_exc.InvalidRequestError( "Instance '%s' is not persisted" % - orm_util.state_str(state)) + state_str(state)) if state.deleted: raise sa_exc.InvalidRequestError( "Instance '%s' has been deleted. Use the make_transient() " "function to send this object back to the transient state." % - orm_util.state_str(state) + state_str(state) ) self._before_attach(state) self._deleted.pop(state, None) @@ -1703,8 +1758,8 @@ class Session(_SessionClassMethods): may not fire off a backref event, if the effective value is what was already loaded from a foreign-key-holding value. - The :meth:`.Session.enable_relationship_loading` method supersedes - the ``load_on_pending`` flag on :func:`.relationship`. Unlike + The :meth:`.Session.enable_relationship_loading` method is + similar to the ``load_on_pending`` flag on :func:`.relationship`. Unlike that flag, :meth:`.Session.enable_relationship_loading` allows an object to remain transient while still being able to load related items. @@ -1721,6 +1776,12 @@ class Session(_SessionClassMethods): .. versionadded:: 0.8 + .. seealso:: + + ``load_on_pending`` at :func:`.relationship` - this flag + allows per-relationship loading of many-to-ones on items that + are pending. + """ state = attributes.instance_state(obj) self._attach(state, include_before=True) @@ -1738,14 +1799,14 @@ class Session(_SessionClassMethods): raise sa_exc.InvalidRequestError("Can't attach instance " "%s; another instance with key %s is already " "present in this session." - % (orm_util.state_str(state), state.key)) + % (state_str(state), state.key)) if state.session_id and \ state.session_id is not self.hash_key and \ state.session_id in _sessions: raise sa_exc.InvalidRequestError( "Object '%s' is already attached to session '%s' " - "(this is '%s')" % (orm_util.state_str(state), + "(this is '%s')" % (state_str(state), state.session_id, self.hash_key)) if state.session_id != self.hash_key: @@ -2090,9 +2151,10 @@ class Session(_SessionClassMethods): access to the full set of persistent objects (i.e., those that have row identity) currently in the session. - See also: + .. seealso:: - :func:`.identity_key` - operations involving identity keys. + :func:`.identity_key` - helper function to produce the keys used + in this dictionary. """ @@ -2196,7 +2258,8 @@ class sessionmaker(_SessionClassMethods): def __init__(self, bind=None, class_=Session, autoflush=True, autocommit=False, - expire_on_commit=True, **kw): + expire_on_commit=True, + info=None, **kw): """Construct a new :class:`.sessionmaker`. All arguments here except for ``class_`` correspond to arguments @@ -2213,6 +2276,13 @@ class sessionmaker(_SessionClassMethods): :class:`.Session` objects. :param expire_on_commit=True: the expire_on_commit setting to use with newly created :class:`.Session` objects. + :param info: optional dictionary of information that will be available + via :attr:`.Session.info`. Note this dictionary is *updated*, not + replaced, when the ``info`` parameter is specified to the specific + :class:`.Session` construction operation. + + .. versionadded:: 0.9.0 + :param \**kw: all other keyword arguments are passed to the constructor of newly created :class:`.Session` objects. @@ -2221,6 +2291,8 @@ class sessionmaker(_SessionClassMethods): kw['autoflush'] = autoflush kw['autocommit'] = autocommit kw['expire_on_commit'] = expire_on_commit + if info is not None: + kw['info'] = info self.kw = kw # make our own subclass of the given class, so that # events can be associated with it specifically. @@ -2238,7 +2310,12 @@ class sessionmaker(_SessionClassMethods): """ for k, v in self.kw.items(): - local_kw.setdefault(k, v) + if k == 'info' and 'info' in local_kw: + d = v.copy() + d.update(local_kw['info']) + local_kw['info'] = d + else: + local_kw.setdefault(k, v) return self.class_(**local_kw) def configure(self, **new_kw): @@ -2253,13 +2330,12 @@ class sessionmaker(_SessionClassMethods): self.kw.update(new_kw) def __repr__(self): - return "%s(class_=%r%s)" % ( + return "%s(class_=%r,%s)" % ( self.__class__.__name__, self.class_.__name__, ", ".join("%s=%r" % (k, v) for k, v in self.kw.items()) ) -_sessions = weakref.WeakValueDictionary() def make_transient(instance): @@ -2304,12 +2380,4 @@ def object_session(instance): raise exc.UnmappedInstanceError(instance) -def _state_session(state): - if state.session_id: - try: - return _sessions[state.session_id] - except KeyError: - pass - return None - _new_sessionid = util.counter() |