From: Jason Kirtland Date: Tue, 20 May 2008 21:44:43 +0000 (+0000) Subject: - Fleshed out Session.get_bind(), generating a couple todos: [ticket:1053], [ticket... X-Git-Tag: rel_0_5beta1~46 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=938badb2bb1364b1820aec1e5eb4d81cf703ab5d;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git - Fleshed out Session.get_bind(), generating a couple todos: [ticket:1053], [ticket:1054], [ticket:1055] - Trotted out util.pending_deprecation, replacing some 'TODO: deprecate's - Big session docstring content edit fiesta - session.py line length and whitespace non-fiesta --- diff --git a/lib/sqlalchemy/orm/session.py b/lib/sqlalchemy/orm/session.py index c34a695e3b..4e9c2da79b 100644 --- a/lib/sqlalchemy/orm/session.py +++ b/lib/sqlalchemy/orm/session.py @@ -23,15 +23,19 @@ from sqlalchemy.orm import identity __all__ = ['Session', 'SessionTransaction', 'SessionExtension'] -def sessionmaker(bind=None, class_=None, autoflush=True, autocommit=False, autoexpire=True, **kwargs): + +def sessionmaker(bind=None, class_=None, autoflush=True, autocommit=False, + autoexpire=True, **kwargs): """Generate a custom-configured [sqlalchemy.orm.session#Session] class. - The returned object is a subclass of ``Session``, which, when instantiated with no - arguments, uses the keyword arguments configured here as its constructor arguments. + The returned object is a subclass of ``Session``, which, when instantiated + with no arguments, uses the keyword arguments configured here as its + constructor arguments. - It is intended that the `sessionmaker()` function be called within the global scope - of an application, and the returned class be made available to the rest of the - application as the single class used to instantiate sessions. + It is intended that the `sessionmaker()` function be called within the + global scope of an application, and the returned class be made available + to the rest of the application as the single class used to instantiate + sessions. e.g.:: @@ -41,19 +45,19 @@ def sessionmaker(bind=None, class_=None, autoflush=True, autocommit=False, autoe # later, in a local scope, create and use a session: sess = Session() - Any keyword arguments sent to the constructor itself will override the "configured" - keywords:: + Any keyword arguments sent to the constructor itself will override the + "configured" keywords:: Session = sessionmaker() # bind an individual session to a connection sess = Session(bind=connection) - The class also includes a special classmethod ``configure()``, which allows - additional configurational options to take place after the custom ``Session`` - class has been generated. This is useful particularly for defining the - specific ``Engine`` (or engines) to which new instances of ``Session`` - should be bound:: + The class also includes a special classmethod ``configure()``, which + allows additional configurational options to take place after the custom + ``Session`` class has been generated. This is useful particularly for + defining the specific ``Engine`` (or engines) to which new instances of + ``Session`` should be bound:: Session = sessionmaker() Session.configure(bind=create_engine('sqlite:///foo.db')) @@ -61,101 +65,104 @@ def sessionmaker(bind=None, class_=None, autoflush=True, autocommit=False, autoe sess = Session() Options: - - autocommit - Defaults to ``False``. When ``True``, the ``Session`` does not keep a - persistent transaction running, and will acquire connections from the engine - on an as-needed basis, returning them immediately after their use. Flushes - will begin and commit (or possibly rollback) their own transaction if no - transaction is present. When using this mode, the `session.begin()` method - may be used to begin a transaction explicitly. - - Leaving it on its default value of ``False`` means that the ``Session`` will - acquire a connection and begin a transaction the first time it is used, which - it will maintain persistently until ``rollback()``, ``commit()``, or - ``close()`` is called. When the transaction is released by any of these - methods, the ``Session`` is ready for the next usage, which will again acquire - and maintain a new connection/transaction. - - autoexpire - When ``True``, all instances will be fully expired after each ``rollback()`` - and after each ``commit()``, so that all attribute/object access subsequent - to a completed transaction will load from the most recent database state. - - autoflush - When ``True``, all query operations will issue a ``flush()`` call to this - ``Session`` before proceeding. This is a convenience feature so that - ``flush()`` need not be called repeatedly in order for database queries to - retrieve results. It's typical that ``autoflush`` is used in conjunction with - ``autocommit=False``. In this scenario, explicit calls to ``flush()`` are rarely - needed; you usually only need to call ``commit()`` (which flushes) to finalize - changes. - bind - An optional ``Engine`` or ``Connection`` to which this ``Session`` should be - bound. When specified, all SQL operations performed by this session will - execute via this connectable. - - binds - An optional dictionary, which contains more granular "bind" information than - the ``bind`` parameter provides. This dictionary can map individual ``Table`` - instances as well as ``Mapper`` instances to individual ``Engine`` or - ``Connection`` objects. Operations which proceed relative to a particular - ``Mapper`` will consult this dictionary for the direct ``Mapper`` instance as - well as the mapper's ``mapped_table`` attribute in order to locate an - connectable to use. The full resolution is described in the ``get_bind()`` - method of ``Session``. Usage looks like:: - - sess = Session(binds={ - SomeMappedClass : create_engine('postgres://engine1'), - somemapper : create_engine('postgres://engine2'), - some_table : create_engine('postgres://engine3'), - }) - - Also see the ``bind_mapper()`` and ``bind_table()`` methods. - - \class_ - Specify an alternate class other than ``sqlalchemy.orm.session.Session`` - which should be used by the returned class. This is the only argument - that is local to the ``sessionmaker()`` function, and is not sent - directly to the constructor for ``Session``. - - echo_uow - When ``True``, configure Python logging to dump all unit-of-work - transactions. This is the equivalent of - ``logging.getLogger('sqlalchemy.orm.unitofwork').setLevel(logging.DEBUG)``. - - extension - An optional [sqlalchemy.orm.session#SessionExtension] instance, which will receive - pre- and post- commit and flush events, as well as a post-rollback event. User- - defined code may be placed within these hooks using a user-defined subclass - of ``SessionExtension``. - - twophase - When ``True``, all transactions will be started using - [sqlalchemy.engine_TwoPhaseTransaction]. During a ``commit()``, after - ``flush()`` has been issued for all attached databases, the ``prepare()`` - method on each database's ``TwoPhaseTransaction`` will be called. This allows - each database to roll back the entire transaction, before each transaction is - committed. - - weak_identity_map - When set to the default value of ``False``, a weak-referencing map is used; - instances which are not externally referenced will be garbage collected - immediately. For dereferenced instances which have pending changes present, - the attribute management system will create a temporary strong-reference to - the object which lasts until the changes are flushed to the database, at which - point it's again dereferenced. Alternatively, when using the value ``True``, - the identity map uses a regular Python dictionary to store instances. The - session will maintain all instances present until they are removed using - expunge(), clear(), or purge(). - + autocommit + Defaults to ``False``. When ``True``, the ``Session`` does not keep a + persistent transaction running, and will acquire connections from the + engine on an as-needed basis, returning them immediately after their + use. Flushes will begin and commit (or possibly rollback) their own + transaction if no transaction is present. When using this mode, the + `session.begin()` method may be used to begin a transaction explicitly. + + Leaving it on its default value of ``False`` means that the ``Session`` + will acquire a connection and begin a transaction the first time it is + used, which it will maintain persistently until ``rollback()``, + ``commit()``, or ``close()`` is called. When the transaction is released + by any of these methods, the ``Session`` is ready for the next usage, + which will again acquire and maintain a new connection/transaction. + + autoexpire + When ``True``, all instances will be fully expired after each + ``rollback()`` and after each ``commit()``, so that all attribute/object + access subsequent to a completed transaction will load from the most + recent database state. + + autoflush + When ``True``, all query operations will issue a ``flush()`` call to + this ``Session`` before proceeding. This is a convenience feature so that + ``flush()`` need not be called repeatedly in order for database queries + to retrieve results. It's typical that ``autoflush`` is used in + conjunction with ``autocommit=False``. In this scenario, explicit calls + to ``flush()`` are rarely needed; you usually only need to call + ``commit()`` (which flushes) to finalize changes. + + bind + An optional ``Engine`` or ``Connection`` to which this ``Session`` + should be bound. When specified, all SQL operations performed by this + session will execute via this connectable. + + binds + An optional dictionary, which contains more granular "bind" information + than the ``bind`` parameter provides. This dictionary can map individual + ``Table`` instances as well as ``Mapper`` instances to individual + ``Engine`` or ``Connection`` objects. Operations which proceed relative + to a particular ``Mapper`` will consult this dictionary for the direct + ``Mapper`` instance as well as the mapper's ``mapped_table`` attribute + in order to locate an connectable to use. The full resolution is + described in the ``get_bind()`` method of ``Session``. Usage looks + like:: + + sess = Session(binds={ + SomeMappedClass: create_engine('postgres://engine1'), + somemapper: create_engine('postgres://engine2'), + some_table: create_engine('postgres://engine3'), + }) + + Also see the ``bind_mapper()`` and ``bind_table()`` methods. + + \class_ + Specify an alternate class other than ``sqlalchemy.orm.session.Session`` + which should be used by the returned class. This is the only argument + that is local to the ``sessionmaker()`` function, and is not sent + directly to the constructor for ``Session``. + + echo_uow + When ``True``, configure Python logging to dump all unit-of-work + transactions. This is the equivalent of + ``logging.getLogger('sqlalchemy.orm.unitofwork').setLevel(logging.DEBUG)``. + + extension + An optional [sqlalchemy.orm.session#SessionExtension] instance, which + will receive pre- and post- commit and flush events, as well as a + post-rollback event. User- defined code may be placed within these + hooks using a user-defined subclass of ``SessionExtension``. + + twophase + When ``True``, all transactions will be started using + [sqlalchemy.engine_TwoPhaseTransaction]. During a ``commit()``, after + ``flush()`` has been issued for all attached databases, the + ``prepare()`` method on each database's ``TwoPhaseTransaction`` will be + called. This allows each database to roll back the entire transaction, + before each transaction is committed. + + weak_identity_map + When set to the default value of ``False``, a weak-referencing map is + used; instances which are not externally referenced will be garbage + collected immediately. For dereferenced instances which have pending + changes present, the attribute management system will create a temporary + strong-reference to the object which lasts until the changes are flushed + to the database, at which point it's again dereferenced. Alternatively, + when using the value ``True``, the identity map uses a regular Python + dictionary to store instances. The session will maintain all instances + present until they are removed using expunge(), clear(), or purge(). + """ - if 'transactional' in kwargs: - util.warn_deprecated("The 'transactional' argument to sessionmaker() is deprecated; use autocommit=True|False instead.") + util.warn_deprecated( + "The 'transactional' argument to sessionmaker() is deprecated; " + "use autocommit=True|False instead.") autocommit = not kwargs.pop('transactional') - + kwargs['bind'] = bind kwargs['autoflush'] = autoflush kwargs['autocommit'] = autocommit @@ -171,13 +178,14 @@ def sessionmaker(bind=None, class_=None, autoflush=True, autocommit=False, autoe super(Sess, self).__init__(**local_kwargs) def configure(self, **new_kwargs): - """(re)configure the arguments for this sessionmaker. + """(Re)configure the arguments for this sessionmaker. + + e.g.:: - e.g. Session = sessionmaker() + Session.configure(bind=create_engine('sqlite://')) """ - kwargs.update(new_kwargs) configure = classmethod(configure) s = type.__new__(type, "Session", (Sess, class_), {}) @@ -185,17 +193,17 @@ def sessionmaker(bind=None, class_=None, autoflush=True, autocommit=False, autoe class SessionTransaction(object): - """Represents a Session-level Transaction. + """A Session-level transaction. This corresponds to one or more [sqlalchemy.engine#Transaction] instances behind the scenes, with one ``Transaction`` per ``Engine`` in use. - Direct usage of ``SessionTransaction`` is not necessary as of - SQLAlchemy 0.4; use the ``begin()`` and ``commit()`` methods on - ``Session`` itself. + Direct usage of ``SessionTransaction`` is not necessary as of SQLAlchemy + 0.4; use the ``begin()`` and ``commit()`` methods on ``Session`` itself. The ``SessionTransaction`` object is **not** threadsafe. + """ def __init__(self, session, parent=None, autoflush=True, nested=False): @@ -207,26 +215,30 @@ class SessionTransaction(object): self._active = True self._prepared = False if not parent and nested: - raise sa_exc.InvalidRequestError("Can't start a SAVEPOINT transaction when no existing transaction is in progress") + raise sa_exc.InvalidRequestError( + "Can't start a SAVEPOINT transaction when no existing " + "transaction is in progress") self._take_snapshot() def is_active(self): return self.session is not None and self._active is_active = property(is_active) - + def _assert_is_active(self): self._assert_is_open() if not self._active: - raise sa_exc.InvalidRequestError("The transaction is inactive due to a rollback in a subtransaction. Issue rollback() to cancel the transaction.") + raise sa_exc.InvalidRequestError( + "The transaction is inactive due to a rollback in a " + "subtransaction. Issue rollback() to cancel the transaction.") def _assert_is_open(self): if self.session is None: raise sa_exc.InvalidRequestError("The transaction is closed") - + def _is_transaction_boundary(self): return self.nested or not self._parent _is_transaction_boundary = property(_is_transaction_boundary) - + def connection(self, bindkey, **kwargs): self._assert_is_active() engine = self.session.get_bind(bindkey, **kwargs) @@ -234,55 +246,58 @@ class SessionTransaction(object): def _begin(self, autoflush=True, nested=False): self._assert_is_active() - return SessionTransaction(self.session, self, autoflush=autoflush, nested=nested) + return SessionTransaction( + self.session, self, autoflush=autoflush, nested=nested) def _iterate_parents(self, upto=None): if self._parent is upto: return (self,) else: if self._parent is None: - raise sa_exc.InvalidRequestError("Transaction %s is not on the active transaction list" % upto) + raise sa_exc.InvalidRequestError( + "Transaction %s is not on the active transaction list" % ( + upto)) return (self,) + self._parent._iterate_parents(upto) - + def _take_snapshot(self): if not self._is_transaction_boundary: self._new = self._parent._new self._deleted = self._parent._deleted return - + if self.autoflush: self.session.flush() - + self._new = weakref.WeakKeyDictionary() self._deleted = weakref.WeakKeyDictionary() - + def _restore_snapshot(self): assert self._is_transaction_boundary - + for s in util.Set(self._deleted).union(self.session._deleted): self.session._update_impl(s) - + assert not self.session._deleted - + for s in util.Set(self._new).union(self.session._new): self.session._expunge_state(s) - + for s in self.session.identity_map.all_states(): _expire_state(s, None) - + def _remove_snapshot(self): assert self._is_transaction_boundary if not self.nested and self.session.autoexpire: for s in self.session.identity_map.all_states(): _expire_state(s, None) - + def _connection_for_bind(self, bind): self._assert_is_active() - + if bind in self._connections: return self._connections[bind][0] - + if self._parent: conn = self._parent._connection_for_bind(bind) if not self.nested: @@ -291,7 +306,9 @@ class SessionTransaction(object): if isinstance(bind, engine.Connection): conn = bind if conn.engine in self._connections: - raise sa_exc.InvalidRequestError("Session already has a Connection associated for the given Connection's Engine") + raise sa_exc.InvalidRequestError( + "Session already has a Connection associated for the " + "given Connection's Engine") else: conn = bind.contextual_connect() @@ -301,29 +318,33 @@ class SessionTransaction(object): transaction = conn.begin_nested() else: transaction = conn.begin() - - self._connections[conn] = self._connections[conn.engine] = (conn, transaction, conn is not bind) + + self._connections[conn] = self._connections[conn.engine] = \ + (conn, transaction, conn is not bind) if self.session.extension is not None: self.session.extension.after_begin(self.session, self, conn) return conn def prepare(self): if self._parent is not None or not self.session.twophase: - raise sa_exc.InvalidRequestError("Only root two phase transactions of can be prepared") + raise sa_exc.InvalidRequestError( + "Only root two phase transactions of can be prepared") self._prepare_impl() - + def _prepare_impl(self): self._assert_is_active() - if self.session.extension is not None and (self._parent is None or self.nested): + if (self.session.extension is not None and + (self._parent is None or self.nested)): self.session.extension.before_commit(self.session) - - if self.session.transaction is not self: - for subtransaction in self.session.transaction._iterate_parents(upto=self): + + stx = self.session.transaction + if stx is not self: + for subtransaction in stx._iterate_parents(upto=self): subtransaction.commit() - + if self.autoflush: self.session.flush() - + if self._parent is None and self.session.twophase: try: for t in util.Set(self._connections.values()): @@ -331,34 +352,35 @@ class SessionTransaction(object): except: self.rollback() raise - + self._deactivate() self._prepared = True - + def commit(self): self._assert_is_open() if not self._prepared: self._prepare_impl() - + if self._parent is None or self.nested: for t in util.Set(self._connections.values()): t[1].commit() if self.session.extension is not None: self.session.extension.after_commit(self.session) - + self._remove_snapshot() - + self.close() return self._parent - + def rollback(self): self._assert_is_open() - - if self.session.transaction is not self: - for subtransaction in self.session.transaction._iterate_parents(upto=self): + + stx = self.session.transaction + if stx is not self: + for subtransaction in stx._iterate_parents(upto=self): subtransaction.close() - + if self.is_active or self._prepared: for transaction in self._iterate_parents(): if transaction._parent is None or transaction.nested: @@ -370,7 +392,7 @@ class SessionTransaction(object): self.close() return self._parent - + def _rollback_impl(self): for t in util.Set(self._connections.values()): t[1].rollback() @@ -394,7 +416,7 @@ class SessionTransaction(object): self._deactivate() self.session = None self._connections = None - + def __enter__(self): return self @@ -411,73 +433,90 @@ class SessionTransaction(object): self.rollback() class Session(object): - """Encapsulates a set of objects being operated upon within an object-relational operation. + """Manages persistence operations for ORM-mapped objects. - The Session is the front end to SQLAlchemy's **Unit of Work** implementation. The concept - behind Unit of Work is to track modifications to a field of objects, and then be able to - flush those changes to the database in a single operation. + The Session is the front end to SQLAlchemy's **Unit of Work** + implementation. The concept behind Unit of Work is to track modifications + to a field of objects, and then be able to flush those changes to the + database in a single operation. SQLAlchemy's unit of work includes these functions: - * The ability to track in-memory changes on scalar- and collection-based object - attributes, such that database persistence operations can be assembled based on those - changes. - - * The ability to organize individual SQL queries and population of newly generated - primary and foreign key-holding attributes during a persist operation such that - referential integrity is maintained at all times. - - * The ability to maintain insert ordering against the order in which new instances were - added to the session. - - * an Identity Map, which is a dictionary keying instances to their unique primary key - identity. This ensures that only one copy of a particular entity is ever present - within the session, even if repeated load operations for the same entity occur. This - allows many parts of an application to get a handle to a particular object without - any chance of modifications going to two different places. - - When dealing with instances of mapped classes, an instance may be *attached* to a - particular Session, else it is *unattached* . An instance also may or may not correspond - to an actual row in the database. These conditions break up into four distinct states: - - * *Transient* - an instance that's not in a session, and is not saved to the database; - i.e. it has no database identity. The only relationship such an object has to the ORM - is that its class has a ``mapper()`` associated with it. - - * *Pending* - when you ``add()`` a transient instance, it becomes pending. It still - wasn't actually flushed to the database yet, but it will be when the next flush - occurs. - - * *Persistent* - An instance which is present in the session and has a record in the - database. You get persistent instances by either flushing so that the pending - instances become persistent, or by querying the database for existing instances (or - moving persistent instances from other sessions into your local session). - - * *Detached* - an instance which has a record in the database, but is not in any - session. Theres nothing wrong with this, and you can use objects normally when - they're detached, **except** they will not be able to issue any SQL in order to load - collections or attributes which are not yet loaded, or were marked as "expired". - - The session methods which control instance state include ``add()``, ``delete()``, - ``merge()``, and ``expunge()``. - - The Session object is generally **not** threadsafe. A session which is set to ``autocommit`` - and is only read from may be used by concurrent threads if it's acceptable that some object - instances may be loaded twice. - - The typical pattern to managing Sessions in a multi-threaded environment is either to use - mutexes to limit concurrent access to one thread at a time, or more commonly to establish - a unique session for every thread, using a threadlocal variable. SQLAlchemy provides - a thread-managed Session adapter, provided by the [sqlalchemy.orm#scoped_session()] function. - + * The ability to track in-memory changes on scalar- and collection-based + object attributes, such that database persistence operations can be + assembled based on those changes. + + * The ability to organize individual SQL queries and population of newly + generated primary and foreign key-holding attributes during a persist + operation such that referential integrity is maintained at all times. + + * The ability to maintain insert ordering against the order in which new + instances were added to the session. + + * An Identity Map, which is a dictionary keying instances to their unique + primary key identity. This ensures that only one copy of a particular + entity is ever present within the session, even if repeated load + operations for the same entity occur. This allows many parts of an + application to get a handle to a particular object without any chance of + modifications going to two different places. + + When dealing with instances of mapped classes, an instance may be + *attached* to a particular Session, else it is *unattached* . An instance + also may or may not correspond to an actual row in the database. These + conditions break up into four distinct states: + + * *Transient* - an instance that's not in a session, and is not saved to + the database; i.e. it has no database identity. The only relationship + such an object has to the ORM is that its class has a ``mapper()`` + associated with it. + + * *Pending* - when you ``add()`` a transient instance, it becomes + pending. It still wasn't actually flushed to the database yet, but it + will be when the next flush occurs. + + * *Persistent* - An instance which is present in the session and has a + record in the database. You get persistent instances by either flushing + so that the pending instances become persistent, or by querying the + database for existing instances (or moving persistent instances from + other sessions into your local session). + + * *Detached* - an instance which has a record in the database, but is not + in any session. Theres nothing wrong with this, and you can use objects + normally when they're detached, **except** they will not be able to + issue any SQL in order to load collections or attributes which are not + yet loaded, or were marked as "expired". + + The session methods which control instance state include ``add()``, + ``delete()``, ``merge()``, and ``expunge()``. + + The Session object is generally **not** threadsafe. A session which is + set to ``autocommit`` and is only read from may be used by concurrent + threads if it's acceptable that some object instances may be loaded twice. + + The typical pattern to managing Sessions in a multi-threaded environment + is either to use mutexes to limit concurrent access to one thread at a + time, or more commonly to establish a unique session for every thread, + using a threadlocal variable. SQLAlchemy provides a thread-managed + Session adapter, provided by the [sqlalchemy.orm#scoped_session()] + function. + """ - public_methods = ('get', 'load', 'close', 'add', 'add_all', 'expire_all', 'save', 'commit', 'update', 'save_or_update', 'flush', 'query', 'delete', 'merge', 'clear', 'refresh', 'expire', 'expunge', 'rollback', 'begin', 'begin_nested', 'connection', 'execute', 'scalar', 'get_bind', 'is_modified', '__contains__', '__iter__') - - def __init__(self, bind=None, autoflush=True, autoexpire=True, autocommit=False, twophase=False, echo_uow=False, weak_identity_map=True, binds=None, extension=None): + + public_methods = ( + '__contains__', '__iter__', 'add', 'add_all', 'begin', 'begin_nested', + 'clear', 'close', 'commit', 'connection', 'delete', 'execute', 'expire', + 'expire_all', 'expunge', 'flush', 'get', 'get_bind', 'is_modified', + 'load', 'merge', 'query', 'refresh', 'rollback', 'save', + 'save_or_update', 'scalar', 'update') + + def __init__(self, bind=None, autoflush=True, autoexpire=True, + autocommit=False, twophase=False, echo_uow=False, + weak_identity_map=True, binds=None, extension=None): """Construct a new Session. - - Arguments to ``Session`` are described using the [sqlalchemy.orm#sessionmaker()] function. - + + Arguments to ``Session`` are described using the + [sqlalchemy.orm#sessionmaker()] function. + """ self.echo_uow = echo_uow if weak_identity_map: @@ -515,67 +554,64 @@ class Session(object): def begin(self, subtransactions=False, nested=False, _autoflush=True): """Begin a transaction on this Session. - - If this Session is already within a transaction, - either a plain transaction or nested transaction, - an error is raised, unless ``subtransactions=True`` - or ``nested=True`` is specified. - - The ``subtransactions=True`` flag indicates that - this ``begin()`` can create a subtransaction if a - transaction is already in progress. A subtransaction - is a non-transactional, delimiting construct that - allows matching begin()/commit() pairs to be nested - together, with only the outermost begin/commit pair - actually affecting transactional state. When a rollback - is issued, the subtransaction will directly roll back - the innermost real transaction, however each subtransaction - still must be explicitly rolled back to maintain proper - stacking of subtransactions. - - If no transaction is in progress, - then a real transaction is begun. - - The ``nested`` flag begins a SAVEPOINT transaction - and is equivalent to calling ``begin_nested()``. - + + If this Session is already within a transaction, either a plain + transaction or nested transaction, an error is raised, unless + ``subtransactions=True`` or ``nested=True`` is specified. + + The ``subtransactions=True`` flag indicates that this ``begin()`` can + create a subtransaction if a transaction is already in progress. A + subtransaction is a non-transactional, delimiting construct that + allows matching begin()/commit() pairs to be nested together, with + only the outermost begin/commit pair actually affecting transactional + state. When a rollback is issued, the subtransaction will directly + roll back the innermost real transaction, however each subtransaction + still must be explicitly rolled back to maintain proper stacking of + subtransactions. + + If no transaction is in progress, then a real transaction is begun. + + The ``nested`` flag begins a SAVEPOINT transaction and is equivalent + to calling ``begin_nested()``. + """ if self.transaction is not None: if subtransactions or nested: - self.transaction = self.transaction._begin(nested=nested, autoflush=_autoflush) + self.transaction = self.transaction._begin( + nested=nested, autoflush=_autoflush) else: - raise sa_exc.InvalidRequestError("A transaction is already begun. Use subtransactions=True to allow subtransactions.") + raise sa_exc.InvalidRequestError( + "A transaction is already begun. Use subtransactions=True " + "to allow subtransactions.") else: - self.transaction = SessionTransaction(self, nested=nested, autoflush=_autoflush) + self.transaction = SessionTransaction( + self, nested=nested, autoflush=_autoflush) return self.transaction # needed for __enter__/__exit__ hook def begin_nested(self): """Begin a `nested` transaction on this Session. - This utilizes a ``SAVEPOINT`` transaction for databases - which support this feature. + The target database(s) must support SQL SAVEPOINTs or a + SQLAlchemy-supported vendor implementation of the idea. - The nested transaction is a real transation, unlike - a "subtransaction" which corresponds to multiple - ``begin()`` calls. The next ``rollback()`` or - ``commit()`` call will operate upon this nested + The nested transaction is a real transation, unlike a "subtransaction" + which corresponds to multiple ``begin()`` calls. The next + ``rollback()`` or ``commit()`` call will operate upon this nested transaction. - + """ return self.begin(nested=True) def rollback(self): """Rollback the current transaction in progress. - If no transaction is in progress, this method is a - pass-thru. - - This method rolls back the current transaction - or nested transaction regardless of subtransactions - being in effect. All subtrasactions up to the - first real transaction are closed. Subtransactions - occur when begin() is called mulitple times. - + If no transaction is in progress, this method is a pass-through. + + This method rolls back the current transaction or nested transaction + regardless of subtransactions being in effect. All subtrasactions up + to the first real transaction are closed. Subtransactions occur when + begin() is called mulitple times. + """ if self.transaction is None: pass @@ -585,22 +621,19 @@ class Session(object): self.begin() def commit(self): - """Flush any pending changes, and commit the current transaction - in progress, assuming no subtransactions are in effect. - - If no transaction is in progress, this method raises - an InvalidRequestError. - - If a subtransaction is in effect (which occurs when - begin() is called multiple times), the subtransaction - will be closed, and the next call to ``commit()`` - will operate on the enclosing transaction. - - For a session configured with autocommit=False, a new - transaction will be begun immediately after the commit, - but note that the newly begun transaction does *not* - use any connection resources until the first SQL is - actually emitted. + """Flush pending changes and commit the current transaction. + + If no transaction is in progress, this method raises an + InvalidRequestError. + + If a subtransaction is in effect (which occurs when begin() is called + multiple times), the subtransaction will be closed, and the next call + to ``commit()`` will operate on the enclosing transaction. + + For a session configured with autocommit=False, a new transaction will + be begun immediately after the commit, but note that the newly begun + transaction does *not* use any connection resources until the first + SQL is actually emitted. """ if self.transaction is None: @@ -612,15 +645,16 @@ class Session(object): self.transaction.commit() if self.transaction is None and not self.autocommit: self.begin() - + def prepare(self): """Prepare the current transaction in progress for two phase commit. - If no transaction is in progress, this method raises - an InvalidRequestError. + If no transaction is in progress, this method raises an + InvalidRequestError. + + Only root transactions of two phase sessions can be prepared. If the + current transaction is not such, an InvalidRequestError is raised. - Only root transactions of two phase sessions can be prepared. If the current transaction is - not such, an InvalidRequestError is raised. """ if self.transaction is None: if not self.autocommit: @@ -631,23 +665,30 @@ class Session(object): self.transaction.prepare() def connection(self, mapper=None, clause=None, instance=None): - """Return a ``Connection`` corresponding to this session's - transactional context, if any. + """Return the active Connection. - If this ``Session`` is transactional, the connection will be in - the context of this session's transaction. Otherwise, the - connection is returned by the ``contextual_connect()`` method - on the engine. + Retrieves the ``Connection`` managing the current transaction. Any + operations executed on the Connection will take place in the same + transactional context as ``Session`` operations. - The `mapper` argument is a class or mapper to which a bound engine - will be located; use this when the Session itself is either bound - to multiple engines or connections, or is not bound to any connectable. + For ``autocommit`` Sessions with no active manual transaction, + ``connection()`` is a passthrough to ``contextual_connect()`` on the + underlying engine. - \**kwargs are additional arguments which will be passed to get_bind(). - See the get_bind() method for details. Note that the ``ShardedSession`` - subclass takes a different get_bind() argument signature. - """ + Ambiguity in multi-bind or unbound Sessions can be resolved through + any of the optional keyword arguments. See ``get_bind()`` for more + information. + mapper + Optional, a ``mapper`` or mapped class + + clause + Optional, any ``ClauseElement`` + + instance + Optional, an instance of a mapped class + + """ return self.__connection(self.get_bind(mapper, clause, instance)) def __connection(self, engine, **kwargs): @@ -657,51 +698,55 @@ class Session(object): return engine.contextual_connect(**kwargs) def execute(self, clause, params=None, mapper=None, instance=None): - """Execute the given clause, using the current transaction (if any). + """Execute a clause within the current transaction. + + Returns a ``ResultProxy`` of execution results. `autocommit` Sessions + will create a transaction on the fly. + + Connection ambiguity in multi-bind or unbound Sessions will be + resolved by inspecting the clause for binds. The 'mapper' and + 'instance' keyword arguments may be used if this is insufficient, See + ``get_bind()`` for more information. - Returns a ``ResultProxy`` corresponding to the execution's results. - clause - a ClauseElement (i.e. select(), text(), etc.) or + A ClauseElement (i.e. select(), text(), etc.) or string SQL statement to be executed - - params - a dictionary of bind parameters. - + + params + Optional, a dictionary of bind parameters. + mapper - a mapped class or Mapper instance which may be needed - in order to locate the proper bind. This is typically - if the Session is not directly bound to a single engine. - + Optional, a ``mapper`` or mapped class + instance - used by some Query operations to further identify - the proper bind, in the case of ShardedSession. - + Optional, an instance of a mapped class + """ clause = expression._literal_as_text(clause) - + engine = self.get_bind(mapper, clause=clause, instance=instance) - return self.__connection(engine, close_with_result=True).execute(clause, params or {}) + return self.__connection(engine, close_with_result=True).execute( + clause, params or {}) def scalar(self, clause, params=None, mapper=None, instance=None): """Like execute() but return a scalar result.""" engine = self.get_bind(mapper, clause=clause, instance=instance) - return self.__connection(engine, close_with_result=True).scalar(clause, params or {}) + return self.__connection(engine, close_with_result=True).scalar( + clause, params or {}) def close(self): """Close this Session. This clears all items and ends any transaction in progress. - If this session were created with ``transactional=True``, a - new transaction is immediately begun. Note that this new - transaction does not use any connection resources until they - are first needed. - """ + If this session were created with ``transactional=True``, a new + transaction is immediately begun. Note that this new transaction does + not use any connection resources until they are first needed. + """ self.clear() if self.transaction is not None: for transaction in self.transaction._iterate_parents(): @@ -720,10 +765,10 @@ class Session(object): def expunge_all(self): """Remove all object instances from this ``Session``. - This is equivalent to calling ``expunge()`` for all objects in - this ``Session``. + This is equivalent to calling ``expunge(obj)`` on all objects in this + ``Session``. + """ - for state in self.identity_map.all_states() + list(self._new): del state.session_id @@ -731,19 +776,29 @@ class Session(object): self._new = {} self._deleted = {} clear = expunge_all - + # TODO: deprecate #clear = util.deprecated()(expunge_all) - + # TODO: need much more test coverage for bind_mapper() and similar ! + # TODO: + crystalize + document resolution order vis. bind_mapper/bind_table def bind_mapper(self, mapper, bind, entity_name=None): - """Bind the given `mapper` or `class` to the given ``Engine`` or ``Connection``. + """Bind operations for a mapper to a Connectable. - All subsequent operations involving this ``Mapper`` will use the - given `bind`. - """ + mapper + A mapper instance or mapped class + bind + Any Connectable: a ``Engine`` or ``Connection``. + + entity_name + Defaults to None. + + All subsequent operations involving this mapper will use the given + `bind`. + + """ if isinstance(mapper, type): mapper = _class_mapper(mapper, entity_name=entity_name) @@ -752,151 +807,175 @@ class Session(object): self.__binds[t] = bind def bind_table(self, table, bind): - """Bind the given `table` to the given ``Engine`` or ``Connection``. + """Bind operations on a Table to a Connectable. + + table + A ``Table`` instance + + bind + Any Connectable: a ``Engine`` or ``Connection``. All subsequent operations involving this ``Table`` will use the given `bind`. - """ + """ self.__binds[table] = bind - def get_bind(self, mapper, clause=None, instance=None): - """Return an engine corresponding to the given arguments. + def get_bind(self, mapper=None, clause=None, instance=None, state=None): + """Resolve and return a configured database bind or raise. + + All arguments are optional. mapper - mapper relative to the desired operation. + Optional, a ``Mapper`` or mapped class clause - a ClauseElement which is to be executed. if - mapper is not present, this may be used to locate - Table objects, which are then associated with mappers - which have associated binds. - + Optional, A ClauseElement (i.e. select(), text(), etc.) + instance - an ORM mapped instance which may be used to further - locate the correct bind. This is currently used by - the ShardedSession subclass. - + Optional, an instance of a mapped class + + state + Optional, SA internal representation of a mapped instance + """ - if mapper is None and clause is None: + if mapper is clause is instance is state is None: if self.bind: return self.bind else: - raise sa_exc.UnboundExecutionError("This session is not bound to any Engine or Connection; specify a mapper to get_bind()") - - elif self.__binds: - if mapper: - mapper = _class_to_mapper(mapper) - if mapper.base_mapper in self.__binds: - return self.__binds[mapper.base_mapper] - elif mapper.mapped_table in self.__binds: - return self.__binds[mapper.mapped_table] + raise sa_exc.UnboundExecutionError( + "This session is not bound to a single Engine or " + "Connection, and no context was provided to locate " + "a binding.") + + # fixme: fix internal callers, we're bork3n here + if isinstance(instance, attributes.InstanceState): + state, instance = instance, None + + mappers = [] + if state is not None: + mappers.append(_state_mapper(state)) + if instance is not None: + mappers.append(_object_mapper(instance)) + if mapper is not None: + mappers.append(_class_to_mapper(mapper)) + + # manually bound? + if self.__binds: + for m in mappers: + if m.base_mapper in self.__binds: + return self.__binds[m.base_mapper] + elif m.mapped_table in self.__binds: + return self.__binds[m.mapped_table] if clause: for t in sql_util.find_tables(clause): if t in self.__binds: return self.__binds[t] - - if self.bind: + elif self.bind: return self.bind - elif isinstance(clause, sql.expression.ClauseElement) and clause.bind: + + if isinstance(clause, sql.expression.ClauseElement) and clause.bind: return clause.bind - elif not mapper: - raise sa_exc.UnboundExecutionError("Could not locate any mapper associated with SQL expression") - else: - mapper = _class_to_mapper(mapper) - e = mapper.mapped_table.bind - if e is None: - raise sa_exc.UnboundExecutionError("Could not locate any Engine or Connection bound to mapper '%s'" % str(mapper)) - return e + + for m in mappers: + if m.mapped_table.bind: + return m.mapped_table.bind + + context = [] + if mapper is not None: + context.append('mapper %s' % c_mapper) + if clause is not None: + context.append('SQL expression') + if instance is not None: + context.append('instance %s' % mapperutil.instance_str(instance)) + if state is not None: + context.append('state %r' % state) + + raise sa_exc.UnboundExecutionError( + "Could not locate a bind configured on %s or this Session" % ( + ', '.join(context))) def query(self, *entities, **kwargs): """Return a new ``Query`` object corresponding to this ``Session``.""" - + return self._query_cls(entities, self, **kwargs) def _autoflush(self): if self.autoflush and (self.transaction is None or self.transaction.autoflush): self.flush() - + def _finalize_loaded(self, states): for state in states: state.commit_all() def get(self, class_, ident, entity_name=None): - """Return an instance of the object based on the given - identifier, or ``None`` if not found. + """Return the instance of class with ident or None. - The `ident` argument is a scalar or tuple of primary key - column values in the order of the table def's primary key - columns. + The `ident` argument is a scalar or tuple of primary key column values + in the order of the table def's primary key columns. - The `entity_name` keyword argument may also be specified which - further qualifies the underlying Mapper used to perform the - query. - """ + The `entity_name` keyword argument may also be specified which further + qualifies the underlying Mapper used to perform the query. + """ return self.query(class_, entity_name=entity_name).get(ident) def load(self, class_, ident, entity_name=None): - """Return an instance of the object based on the given - identifier. - - If not found, raises an exception. The method will **remove - all pending changes** to the object already existing in the - ``Session``. The `ident` argument is a scalar or tuple of primary - key columns in the order of the table def's primary key - columns. - - The `entity_name` keyword argument may also be specified which - further qualifies the underlying ``Mapper`` used to perform the - query. - """ + """Reset and return the instance of class with ident or raise. + + If not found, raises an exception. The method will **remove all + pending changes** to the object already existing in the ``Session``. + The `ident` argument is a scalar or tuple of primary key columns in + the order of the table def's primary key columns. + + The `entity_name` keyword argument may also be specified which further + qualifies the underlying ``Mapper`` used to perform the query. + """ return self.query(class_, entity_name=entity_name).load(ident) def refresh(self, instance, attribute_names=None): """Refresh the attributes on the given instance. - When called, a query will be issued - to the database which will refresh all attributes with their - current value. + A query will be issued to the database and all attributes will be + refreshed with their current database value. Lazy-loaded relational attributes will remain lazily loaded, so that - the instance-wide refresh operation will be followed - immediately by the lazy load of that attribute. + the instance-wide refresh operation will be followed immediately by + the lazy load of that attribute. Eagerly-loaded relational attributes will eagerly load within the single refresh operation. - The ``attribute_names`` argument is an iterable collection - of attribute names indicating a subset of attributes to be - refreshed. - """ + The ``attribute_names`` argument is an iterable collection of + attribute names indicating a subset of attributes to be refreshed. + """ state = attributes.instance_state(instance) self._validate_persistent(state) if self.query(_object_mapper(instance))._get( state.key, refresh_instance=state, only_load_props=attribute_names) is None: - raise sa_exc.InvalidRequestError("Could not refresh instance '%s'" % mapperutil.instance_str(instance)) + raise sa_exc.InvalidRequestError( + "Could not refresh instance '%s'" % + mapperutil.instance_str(instance)) def expire_all(self): - """Expires all persistent instances within this Session. - - """ + """Expires all persistent instances within this Session.""" for state in self.identity_map.all_states(): _expire_state(state, None) - + def expire(self, instance, attribute_names=None): - """Expire the attributes on the given instance. + """Expire the attributes on an instance. - The instance's attributes are instrumented such that - when an attribute is next accessed, a query will be issued - to the database which will refresh all attributes with their - current value. + Marks the attributes of an instance as out of date. When an expired + attribute is next accessed, query will be issued to the database and + the attributes will be refreshed with their current database value. + ``expire()`` is a lazy variant of ``refresh()``. The ``attribute_names`` argument is an iterable collection of attribute names indicating a subset of attributes to be expired. + """ state = attributes.instance_state(instance) self._validate_persistent(state) @@ -913,30 +992,31 @@ class Session(object): def prune(self): """Remove unreferenced instances cached in the identity map. - Note that this method is only meaningful if "weak_identity_map" - is set to False. + Note that this method is only meaningful if "weak_identity_map" is set + to False. The default weak identity map is self-pruning. Removes any object in this Session's identity map that is not referenced in user code, modified, new or scheduled for deletion. Returns the number of objects pruned. - """ + """ return self.identity_map.prune() def expunge(self, instance): - """Remove the given `instance` from this ``Session``. + """Remove the `instance` from this ``Session``. + + This will free all internal references to the instance. Cascading + will be applied according to the *expunge* cascade rule. - This will free all internal references to the instance. - Cascading will be applied according to the *expunge* cascade - rule. """ - state = attributes.instance_state(instance) if state.session_id is not self.hash_key: - raise sa_exc.InvalidRequestError("Instance %s is not present in this Session" % mapperutil.state_str(state)) + raise sa_exc.InvalidRequestError( + "Instance %s is not present in this Session" % + mapperutil.state_str(state)) for s, m in [(state, None)] + list(_cascade_state_iterator('expunge', state)): self._expunge_state(s) - + def _expunge_state(self, state): if state in self._new: self._new.pop(state) @@ -974,11 +1054,11 @@ class Session(object): if self.transaction: self.transaction._new[state] = True self._new.pop(state) - + def _remove_newly_deleted(self, state): if self.transaction: self.transaction._deleted[state] = True - + self.identity_map.discard(state) self._deleted.pop(state, None) del state.session_id @@ -986,104 +1066,99 @@ class Session(object): def save(self, instance, entity_name=None): """Add a transient (unsaved) instance to this ``Session``. - This operation cascades the `save_or_update` method to - associated instances if the relation is mapped with - ``cascade="save-update"``. + This operation cascades the `save_or_update` method to associated + instances if the relation is mapped with ``cascade="save-update"``. + + The `entity_name` keyword argument will further qualify the specific + ``Mapper`` used to handle this instance. - The `entity_name` keyword argument will further qualify the - specific ``Mapper`` used to handle this instance. - """ state = _state_for_unsaved_instance(instance, entity_name) self._save_impl(state) self._cascade_save_or_update(state, entity_name) - - # TODO - #save = util.deprecated("Use the add() method.")(save) - + save = util.pending_deprecation('0.5.x', "Use session.add()")(save) + def _save_without_cascade(self, instance, entity_name=None): - """used by scoping.py to save on init without cascade.""" - + """Used by scoping.py to save on init without cascade.""" + state = _state_for_unsaved_instance(instance, entity_name) self._save_impl(state) - + def update(self, instance, entity_name=None): - """Bring the given detached (saved) instance into this - ``Session``. + """Bring a detached (saved) instance into this ``Session``. If there is a persistent instance with the same instance key, but different identity already associated with this ``Session``, an InvalidRequestError exception is thrown. - This operation cascades the `save_or_update` method to - associated instances if the relation is mapped with - ``cascade="save-update"``. - + This operation cascades the `save_or_update` method to associated + instances if the relation is mapped with ``cascade="save-update"``. + """ state = attributes.instance_state(instance) self._update_impl(state) self._cascade_save_or_update(state, entity_name) - - # TODO - #update = util.deprecated("Use the add() method.")(update) - + update = util.pending_deprecation('0.5.x', "Use session.add()")(update) + def add(self, instance, entity_name=None): """Add the given instance into this ``Session``. + TODO: rephrase the below in user terms; possibly tie into future + function that downgrades persistent to transient. [ticket:1052] + The non-None state `key` on the instance's state determines whether to ``save()`` or ``update()`` the instance. """ state = _state_for_unknown_persistence_instance(instance, entity_name) self._save_or_update_state(state, entity_name) - + def add_all(self, instances): """Add the given collection of instances to this ``Session``.""" - + for instance in instances: self.add(instance) - - # TODO - # save_or_update = util.deprecated("Use the add() method.")(add) - save_or_update = add - + def _save_or_update_state(self, state, entity_name): self._save_or_update_impl(state) self._cascade_save_or_update(state, entity_name) - + + save_or_update = ( + util.pending_deprecation('0.5.x', "Use session.add()")(add)) + def _cascade_save_or_update(self, state, entity_name): for state, mapper in _cascade_unknown_state_iterator('save-update', state, halt_on=lambda c:c in self): self._save_or_update_impl(state) def delete(self, instance): - """Mark the given instance as deleted. + """Mark an instance as deleted. - The delete operation occurs upon ``flush()``. - """ + The database delete operation occurs upon ``flush()``. + """ state = attributes.instance_state(instance) self._delete_impl(state) for state, m in _cascade_state_iterator('delete', state): self._delete_impl(state, ignore_transient=True) + def merge(self, instance, entity_name=None, dont_load=False, + _recursive=None): + """Copy the state an instance onto the persistent instance with the same identifier. - def merge(self, instance, entity_name=None, dont_load=False, _recursive=None): - """Copy the state of the given `instance` onto the persistent - instance with the same identifier. + If there is no persistent instance currently associated with the + session, it will be loaded. Return the persistent instance. If the + given instance is unsaved, save a copy of and return it as a newly + persistent instance. The given instance does not become associated + with the session. - If there is no persistent instance currently associated with - the session, it will be loaded. Return the persistent - instance. If the given instance is unsaved, save a copy of and - return it as a newly persistent instance. The given instance - does not become associated with the session. + This operation cascades to associated instances if the association is + mapped with ``cascade="merge"``. - This operation cascades to associated instances if the - association is mapped with ``cascade="merge"``. """ - if _recursive is None: - _recursive = {} # TODO: this should be an IdentityDict for instances, but will need a separate - # dict for PropertyLoader tuples + # TODO: this should be an IdentityDict for instances, but will + # need a separate dict for PropertyLoader tuples + _recursive = {} if entity_name is not None: mapper = _class_mapper(instance.__class__, entity_name=entity_name) else: @@ -1096,7 +1171,11 @@ class Session(object): key = state.key if key is None: if dont_load: - raise sa_exc.InvalidRequestError("merge() with dont_load=True option does not support objects transient (i.e. unpersisted) objects. flush() all changes on mapped instances before merging with dont_load=True.") + raise sa_exc.InvalidRequestError( + "merge() with dont_load=True option does not support " + "objects transient (i.e. unpersisted) objects. flush() " + "all changes on mapped instances before merging with " + "dont_load=True.") key = mapper._identity_key_from_state(state) merged = None @@ -1105,8 +1184,10 @@ class Session(object): merged = self.identity_map[key] elif dont_load: if state.modified: - raise sa_exc.InvalidRequestError("merge() with dont_load=True option does not support objects marked as 'dirty'. flush() all changes on mapped instances before merging with dont_load=True.") - + raise sa_exc.InvalidRequestError( + "merge() with dont_load=True option does not support " + "objects marked as 'dirty'. flush() all changes on " + "mapped instances before merging with dont_load=True.") merged = mapper.class_manager.new_instance() merged_state = attributes.instance_state(merged) merged_state.key = key @@ -1139,14 +1220,16 @@ class Session(object): identity_key = classmethod(identity_key) def object_session(cls, instance): - """Return the ``Session`` to which the given object belongs.""" + """Return the ``Session`` to which an object belongs.""" return object_session(instance) object_session = classmethod(object_session) def _validate_persistent(self, state): if not self.identity_map.contains_state(state): - raise sa_exc.InvalidRequestError("Instance '%s' is not persistent within this Session" % mapperutil.state_str(state)) + raise sa_exc.InvalidRequestError( + "Instance '%s' is not persistent within this Session" % + mapperutil.state_str(state)) def _save_impl(self, state): if state.key is not None: @@ -1159,24 +1242,25 @@ class Session(object): state.insert_order = len(self._new) def _update_impl(self, state): - if self.identity_map.contains_state(state) and state not in self._deleted: + if (self.identity_map.contains_state(state) and + state not in self._deleted): return - if state.key is None: raise sa_exc.InvalidRequestError( "Instance '%s' is not persisted" % mapperutil.state_str(state)) - - if state.key in self.identity_map and not self.identity_map.contains_state(state): + + if (state.key in self.identity_map and + not self.identity_map.contains_state(state)): raise sa_exc.InvalidRequestError( "Could not update instance '%s', identity key %s; a different " "instance with the same identity key already exists in this " "session." % (mapperutil.state_str(state), state.key)) - + self._attach(state) self._deleted.pop(state, None) self.identity_map.add(state) - + def _save_or_update_impl(self, state): if state.key is None: self._save_impl(state) @@ -1186,13 +1270,16 @@ class Session(object): def _delete_impl(self, state, ignore_transient=False): if self.identity_map.contains_state(state) and state in self._deleted: return - + if state.key is None: if ignore_transient: return else: - raise sa_exc.InvalidRequestError("Instance '%s' is not persisted" % mapperutil.state_str(state)) - if state.key in self.identity_map and not self.identity_map.contains_state(state): + raise sa_exc.InvalidRequestError( + "Instance '%s' is not persisted" % + mapperutil.state_str(state)) + if (state.key in self.identity_map and + not self.identity_map.contains_state(state)): raise sa_exc.InvalidRequestError( "Instance '%s' is with key %s already persisted with a " "different identity" % (mapperutil.state_str(state), @@ -1211,16 +1298,16 @@ class Session(object): state.session_id = self.hash_key def __contains__(self, instance): - """Return True if the given instance is associated with this session. + """Return True if the instance is associated with this session. The instance may be pending or persistent within the Session for a result of True. """ return self._contains_state(attributes.instance_state(instance)) - + def __iter__(self): - """Return an iterator of all instances which are pending or persistent within this Session.""" + """Iterate over all pending or persistent instances within this Session.""" return iter(list(self._new.values()) + self.identity_map.values()) @@ -1229,16 +1316,31 @@ class Session(object): def flush(self, objects=None): - """Flush all the object modifications present in this session - to the database. + """Flush all the object changes to the database. + + Writes out all pending object creations, deletions and modifications + to the database as INSERTs, DELETEs, UPDATEs, etc. Operations are + automatically ordered by the Session's unit of work dependency + solver.. + + Database operations will be issued in the current transactional + context and do not affect the state of the transaction. You may + flush() as often as you like within a transaction to move changes from + Python to the database's transaction buffer. - `objects` is a list or tuple of objects specifically to be - flushed; if ``None``, all new and modified objects are flushed. + For ``autocommit`` Sessions with no active manual transaction, flush() + will create a transaction on the fly that surrounds the entire set of + operations int the flush. + + objects + Optional; a list or tuple collection. Restricts the flush operation + to only these objects, rather than all pending changes. """ - if not self.identity_map.check_modified() and not self._deleted and not self._new: + if (not self.identity_map.check_modified() and + not self._deleted and not self._new): return - + dirty = self._dirty_states if not dirty and not self._deleted and not self._new: self.identity_map.modified = False @@ -1265,15 +1367,20 @@ class Session(object): # store objects whose fate has been decided processed = util.Set() - # put all saves/updates into the flush context. detect top-level orphans and throw them into deleted. + # put all saves/updates into the flush context. detect top-level + # orphans and throw them into deleted. for state in new.union(dirty).intersection(objset).difference(deleted): is_orphan = _state_mapper(state)._is_orphan(state) if is_orphan and not _state_has_identity(state): - raise exc.FlushError("instance %s is an unsaved, pending instance and is an orphan (is not attached to %s)" % - ( - mapperutil.state_str(state), - ", nor ".join(["any parent '%s' instance via that classes' '%s' attribute" % (klass.__name__, key) for (key,klass) in _state_mapper(state).delete_orphans]) - )) + path = ", nor ".join( + ["any parent '%s' instance " + "via that classes' '%s' attribute" % + (cls.__name__, key) + for (key, cls) in _state_mapper(state).delete_orphans]) + raise exc.FlushError( + "Instance %s is an unsaved, pending instance and is an " + "orphan (is not attached to %s)" % ( + mapperutil.state_str(state), path)) flush_context.register_object(state, isdelete=is_orphan) processed.add(state) @@ -1283,8 +1390,9 @@ class Session(object): if len(flush_context.tasks) == 0: return - - flush_context.transaction = transaction = self.begin(subtransactions=True, _autoflush=False) + + flush_context.transaction = transaction = self.begin( + subtransactions=True, _autoflush=False) try: flush_context.execute() @@ -1304,22 +1412,24 @@ class Session(object): self.extension.after_flush_postexec(self, flush_context) def is_modified(self, instance, include_collections=True, passive=False): - """Return True if the given instance has modified attributes. + """Return True if instance has modified attributes. - This method retrieves a history instance for each instrumented attribute - on the instance and performs a comparison of the current value to its - previously committed value. Note that instances present in the 'dirty' - collection may result in a value of ``False`` when tested with this method. + This method retrieves a history instance for each instrumented + attribute on the instance and performs a comparison of the current + value to its previously committed value. Note that instances present + in the 'dirty' collection may result in a value of ``False`` when + tested with this method. - `include_collections` indicates if multivalued collections should be included - in the operation. Setting this to False is a way to detect only local-column - based properties (i.e. scalar columns or many-to-one foreign keys) that would - result in an UPDATE for this instance upon flush. + `include_collections` indicates if multivalued collections should be + included in the operation. Setting this to False is a way to detect + only local-column based properties (i.e. scalar columns or many-to-one + foreign keys) that would result in an UPDATE for this instance upon + flush. - The `passive` flag indicates if unloaded attributes and collections should - not be loaded in the course of performing this test. - """ + The `passive` flag indicates if unloaded attributes and collections + should not be loaded in the course of performing this test. + """ for attr in attributes.manager_of_class(instance.__class__).attributes: if not include_collections and hasattr(attr.impl, 'get_collection'): continue @@ -1336,8 +1446,9 @@ class Session(object): """ return util.IdentitySet( - [state for state in self.identity_map.all_states() if state.check_modified()] - ) + [state + for state in self.identity_map.all_states() + if state.check_modified()]) _dirty_states = property(_dirty_states) def dirty(self): @@ -1346,46 +1457,46 @@ class Session(object): Instances are considered dirty when they were modified but not deleted. - Note that the 'dirty' state here is 'optimistic'; most attribute-setting or collection - modification operations will mark an instance as 'dirty' and place it in this set, - even if there is no net change to the attribute's value. At flush time, the value - of each attribute is compared to its previously saved value, - and if there's no net change, no SQL operation will occur (this is a more expensive + Note that this 'dirty' calculation is 'optimistic'; most + attribute-setting or collection modification operations will mark an + instance as 'dirty' and place it in this set, even if there is no net + change to the attribute's value. At flush time, the value of each + attribute is compared to its previously saved value, and if there's no + net change, no SQL operation will occur (this is a more expensive operation so it's only done at flush time). - To check if an instance has actionable net changes to its attributes, use the - is_modified() method. + To check if an instance has actionable net changes to its attributes, + use the is_modified() method. """ - return util.IdentitySet( - [state.obj() for state in self._dirty_states if state not in self._deleted] - ) - + [state.obj() + for state in self._dirty_states + if state not in self._deleted]) dirty = property(dirty) def deleted(self): - "Return a ``Set`` of all instances marked as 'deleted' within this ``Session``" - + "Return a set of all instances marked as 'deleted' within this ``Session``" + return util.IdentitySet(self._deleted.values()) deleted = property(deleted) def new(self): - "Return a ``Set`` of all instances marked as 'new' within this ``Session``." - + "Return a set of all instances marked as 'new' within this ``Session``." + return util.IdentitySet(self._new.values()) new = property(new) def _expire_state(state, attribute_names): - """Standalone expire instance function. + """Stand-alone expire instance function. - Installs a callable with the given instance's _state - which will fire off when any of the named attributes are accessed; - their existing value is removed. + Installs a callable with the given instance's _state which will fire off + when any of the named attributes are accessed; their existing value is + removed. If the list is None or blank, the entire instance is expired. - """ + """ state.expire_attributes(attribute_names) register_attribute = unitofwork.register_attribute @@ -1423,10 +1534,9 @@ def _state_for_unknown_persistence_instance(instance, entity_name): return state def object_session(instance): - """Return the ``Session`` to which the given instance is bound, or ``None`` if none.""" - + """Return the ``Session`` to which instance belongs, or None.""" return _state_session(attributes.instance_state(instance)) - + def _state_session(state): if state.session_id: try: diff --git a/lib/sqlalchemy/orm/util.py b/lib/sqlalchemy/orm/util.py index 2f2b4c08bc..485d60cd1b 100644 --- a/lib/sqlalchemy/orm/util.py +++ b/lib/sqlalchemy/orm/util.py @@ -452,7 +452,14 @@ def object_mapper(object, entity_name=None, raiseerror=True): be located. If False, return None. """ - state = attributes.instance_state(object) + try: + state = attributes.instance_state(object) + except (KeyError, AttributeError): + if not raiseerror: + return None + raise sa_exc.InvalidRequestError( + "FIXME Instance %r with entity name '%s' has no mapper associated with it" % + (object, entity_name)) if state.entity_name is not attributes.NO_ENTITY_NAME: # Override the given entity name if the object is not transient. entity_name = state.entity_name diff --git a/test/testlib/testing.py b/test/testlib/testing.py index eda83a55df..e09899e0c4 100644 --- a/test/testlib/testing.py +++ b/test/testlib/testing.py @@ -302,14 +302,17 @@ def emits_warning(*messages): if sa_exc is None: import sqlalchemy.exc as sa_exc + # todo: should probably be strict about this, too + filters = [dict(action='ignore', + category=sa_exc.SAPendingDeprecationWarning)] if not messages: - filters = [dict(action='ignore', - category=sa_exc.SAWarning)] + filters.append([dict(action='ignore', + category=sa_exc.SAWarning)]) else: - filters = [dict(action='ignore', - message=message, - category=sa_exc.SAWarning) - for message in messages ] + filters.extend([dict(action='ignore', + message=message, + category=sa_exc.SAWarning) + for message in messages]) for f in filters: warnings.filterwarnings(**f) try: @@ -337,17 +340,21 @@ def uses_deprecated(*messages): if sa_exc is None: import sqlalchemy.exc as sa_exc + # todo: should probably be strict about this, too + filters = [dict(action='ignore', + category=sa_exc.SAPendingDeprecationWarning)] if not messages: - filters = [dict(action='ignore', - category=sa_exc.SADeprecationWarning)] + filters.append(dict(action='ignore', + category=sa_exc.SADeprecationWarning)) else: - filters = [dict(action='ignore', - message=message, - category=sa_exc.SADeprecationWarning) - for message in - [ (m.startswith('//') and - ('Call to deprecated function ' + m[2:]) or m) - for m in messages] ] + filters.extend( + [dict(action='ignore', + message=message, + category=sa_exc.SADeprecationWarning) + for message in + [ (m.startswith('//') and + ('Call to deprecated function ' + m[2:]) or m) + for m in messages] ]) for f in filters: warnings.filterwarnings(**f) @@ -365,7 +372,8 @@ def resetwarnings(): if sa_exc is None: import sqlalchemy.exc as sa_exc - warnings.resetwarnings() + warnings.filterwarnings('ignore', + category=sa_exc.SAPendingDeprecationWarning) warnings.filterwarnings('error', category=sa_exc.SADeprecationWarning) warnings.filterwarnings('error', category=sa_exc.SAWarning)