summaryrefslogtreecommitdiff
path: root/lib/sqlalchemy/orm/session.py
diff options
context:
space:
mode:
Diffstat (limited to 'lib/sqlalchemy/orm/session.py')
-rw-r--r--lib/sqlalchemy/orm/session.py152
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()