diff options
| author | Mike Bayer <mike_mp@zzzcomputing.com> | 2010-09-14 17:37:27 -0400 |
|---|---|---|
| committer | Mike Bayer <mike_mp@zzzcomputing.com> | 2010-09-14 17:37:27 -0400 |
| commit | 60b82d6e13246a3d88e7288e863a5231b7572c6a (patch) | |
| tree | 30fb5710dd763fd4fbfa9966efe47a4c9e7eace9 /lib/sqlalchemy | |
| parent | b6fe3a83e9127996cb903ae176701ddfbcca5b12 (diff) | |
| parent | 46196ea723484f354ac17204ccd489004baaac95 (diff) | |
| download | sqlalchemy-60b82d6e13246a3d88e7288e863a5231b7572c6a.tar.gz | |
- merge tip, 0.6.4 + 0.6.5
Diffstat (limited to 'lib/sqlalchemy')
35 files changed, 805 insertions, 362 deletions
diff --git a/lib/sqlalchemy/__init__.py b/lib/sqlalchemy/__init__.py index 2d809e339..cb4e8e10b 100644 --- a/lib/sqlalchemy/__init__.py +++ b/lib/sqlalchemy/__init__.py @@ -114,6 +114,6 @@ from sqlalchemy.engine import create_engine, engine_from_config __all__ = sorted(name for name, obj in locals().items() if not (name.startswith('_') or inspect.ismodule(obj))) -__version__ = '0.6.4' +__version__ = '0.6.5' del inspect, sys diff --git a/lib/sqlalchemy/dialects/firebird/base.py b/lib/sqlalchemy/dialects/firebird/base.py index da8bef8c0..bb4cc2b09 100644 --- a/lib/sqlalchemy/dialects/firebird/base.py +++ b/lib/sqlalchemy/dialects/firebird/base.py @@ -571,9 +571,10 @@ class FBDialect(default.DefaultDialect): if row['fdefault'] is not None: # the value comes down as "DEFAULT 'value'": there may be # more than one whitespace around the "DEFAULT" keyword + # and it may also be lower case # (see also http://tracker.firebirdsql.org/browse/CORE-356) defexpr = row['fdefault'].lstrip() - assert defexpr[:8].rstrip() == \ + assert defexpr[:8].rstrip().upper() == \ 'DEFAULT', "Unrecognized default value: %s" % \ defexpr defvalue = defexpr[8:].strip() diff --git a/lib/sqlalchemy/dialects/informix/base.py b/lib/sqlalchemy/dialects/informix/base.py index bc7b6c3e7..242b8a328 100644 --- a/lib/sqlalchemy/dialects/informix/base.py +++ b/lib/sqlalchemy/dialects/informix/base.py @@ -105,29 +105,20 @@ class InfoSQLCompiler(compiler.SQLCompiler): s += "" return s - def visit_select(self, select): - # the column in order by clause must in select too - - def __label(c): - try: - return c._label.lower() - except: - return '' - - # TODO: dont modify the original select, generate a new one - a = [__label(c) for c in select._raw_columns] - for c in select._order_by_clause.clauses: - if __label(c) not in a: - select.append_column(c) - - return compiler.SQLCompiler.visit_select(self, select) + def visit_select(self, select, asfrom=False, parens=True, **kw): + text = compiler.SQLCompiler.visit_select(self, select, asfrom, parens, **kw) + if asfrom and parens and self.dialect.server_version_info < (11,): + #assuming that 11 version doesn't need this, not tested + return "table(multiset" + text + ")" + else: + return text def limit_clause(self, select): if select._offset is not None and select._offset > 0: raise NotImplementedError("Informix does not support OFFSET") return "" - def visit_function(self, func): + def visit_function(self, func, **kw): if func.name.lower() == 'current_date': return "today" elif func.name.lower() == 'current_time': @@ -135,7 +126,7 @@ class InfoSQLCompiler(compiler.SQLCompiler): elif func.name.lower() in ('current_timestamp', 'now'): return "CURRENT YEAR TO SECOND" else: - return compiler.SQLCompiler.visit_function(self, func) + return compiler.SQLCompiler.visit_function(self, func, **kw) class InfoDDLCompiler(compiler.DDLCompiler): diff --git a/lib/sqlalchemy/dialects/informix/informixdb.py b/lib/sqlalchemy/dialects/informix/informixdb.py index 54e5a994a..8edcc953b 100644 --- a/lib/sqlalchemy/dialects/informix/informixdb.py +++ b/lib/sqlalchemy/dialects/informix/informixdb.py @@ -31,10 +31,13 @@ class InformixDialect_informixdb(InformixDialect): def _get_server_version_info(self, connection): # http://informixdb.sourceforge.net/manual.html#inspecting-version-numbers - vers = connection.dbms_version - - # TODO: not tested - return tuple([int(x) for x in vers.split('.')]) + version = [] + for n in connection.connection.dbms_version.split('.'): + try: + version.append(int(n)) + except ValueError: + version.append(n) + return tuple(version) def is_disconnect(self, e): if isinstance(e, self.dbapi.OperationalError): diff --git a/lib/sqlalchemy/dialects/mssql/base.py b/lib/sqlalchemy/dialects/mssql/base.py index 88ca36dbd..95a5bf4c4 100644 --- a/lib/sqlalchemy/dialects/mssql/base.py +++ b/lib/sqlalchemy/dialects/mssql/base.py @@ -104,7 +104,7 @@ Compatibility Levels MSSQL supports the notion of setting compatibility levels at the database level. This allows, for instance, to run a database that is compatibile with SQL2000 while running on a SQL2005 database -server. ``server_version_info`` will always retrun the database +server. ``server_version_info`` will always return the database server version information (in this case SQL2005) and not the compatibiility level information. Because of this, if running under a backwards compatibility mode SQAlchemy may attempt to use T-SQL diff --git a/lib/sqlalchemy/engine/__init__.py b/lib/sqlalchemy/engine/__init__.py index b4894250f..43d3dd038 100644 --- a/lib/sqlalchemy/engine/__init__.py +++ b/lib/sqlalchemy/engine/__init__.py @@ -132,12 +132,19 @@ def create_engine(*args, **kwargs): additional keyword arguments. :param convert_unicode=False: if set to True, all - String/character based types will convert Unicode values to raw - byte values going into the database, and all raw byte values to + String/character based types will convert Python Unicode values to raw + byte values sent to the DBAPI as bind parameters, and all raw byte values to Python Unicode coming out in result sets. This is an - engine-wide method to provide unicode conversion across the - board. For unicode conversion on a column-by-column level, use - the ``Unicode`` column type instead, described in `types`. + engine-wide method to provide Unicode conversion across the + board for those DBAPIs that do not accept Python Unicode objects + as input. For Unicode conversion on a column-by-column level, use + the ``Unicode`` column type instead, described in :ref:`types_toplevel`. Note that + many DBAPIs have the ability to return Python Unicode objects in + result sets directly - SQLAlchemy will use these modes of operation + if possible and will also attempt to detect "Unicode returns" + behavior by the DBAPI upon first connect by the + :class:`.Engine`. When this is detected, string values in + result sets are passed through without further processing. :param creator: a callable which returns a DBAPI connection. This creation function will be passed to the underlying @@ -188,10 +195,13 @@ def create_engine(*args, **kwargs): opened above and beyond the pool_size setting, which defaults to five. this is only used with :class:`~sqlalchemy.pool.QueuePool`. - :param module=None: used by database implementations which - support multiple DBAPI modules, this is a reference to a DBAPI2 - module to be used instead of the engine's default module. For - PostgreSQL, the default is psycopg2. For Oracle, it's cx_Oracle. + :param module=None: reference to a Python module object (the module itself, not + its string name). Specifies an alternate DBAPI module to be used + by the engine's dialect. Each sub-dialect references a specific DBAPI which + will be imported before first connect. This parameter causes the + import to be bypassed, and the given module to be used instead. + Can be used for testing of DBAPIs as well as to inject "mock" + DBAPI implementations into the :class:`.Engine`. :param pool=None: an already-constructed instance of :class:`~sqlalchemy.pool.Pool`, such as a @@ -199,7 +209,7 @@ def create_engine(*args, **kwargs): pool will be used directly as the underlying connection pool for the engine, bypassing whatever connection parameters are present in the URL argument. For information on constructing - connection pools manually, see `pooling`. + connection pools manually, see :ref:`pooling_toplevel`. :param poolclass=None: a :class:`~sqlalchemy.pool.Pool` subclass, which will be used to create a connection pool @@ -224,7 +234,7 @@ def create_engine(*args, **kwargs): connections after the given number of seconds has passed. It defaults to -1, or no timeout. For example, setting to 3600 means connections will be recycled after one hour. Note that - MySQL in particular will ``disconnect automatically`` if no + MySQL in particular will disconnect automatically if no activity is detected on a connection for eight hours (although this is configurable with the MySQLDB connection itself and the server configuration as well). @@ -233,8 +243,8 @@ def create_engine(*args, **kwargs): up on getting a connection from the pool. This is only used with :class:`~sqlalchemy.pool.QueuePool`. - :param strategy='plain': used to invoke alternate :class:`~sqlalchemy.engine.base.Engine.` - implementations. Currently available is the ``threadlocal`` + :param strategy='plain': selects alternate engine implementations. + Currently available is the ``threadlocal`` strategy, which is described in :ref:`threadlocal_strategy`. """ diff --git a/lib/sqlalchemy/engine/base.py b/lib/sqlalchemy/engine/base.py index a02e90aba..6c81f8b74 100644 --- a/lib/sqlalchemy/engine/base.py +++ b/lib/sqlalchemy/engine/base.py @@ -797,11 +797,22 @@ class Connectable(object): class Connection(Connectable): """Provides high-level functionality for a wrapped DB-API connection. - Provides execution support for string-based SQL statements as well - as ClauseElement, Compiled and DefaultGenerator objects. Provides - a :meth:`begin` method to return Transaction objects. - - The Connection object is **not** thread-safe. + Provides execution support for string-based SQL statements as well as + :class:`.ClauseElement`, :class:`.Compiled` and :class:`.DefaultGenerator` + objects. Provides a :meth:`begin` method to return :class:`.Transaction` + objects. + + The Connection object is **not** thread-safe. While a Connection can be + shared among threads using properly synchronized access, it is still + possible that the underlying DBAPI connection may not support shared + access between threads. Check the DBAPI documentation for details. + + The Connection object represents a single dbapi connection checked out + from the connection pool. In this state, the connection pool has no affect + upon the connection, including its expiration or timeout state. For the + connection pool to properly manage connections, connections should be + returned to the connection pool (i.e. ``connection.close()``) whenever the + connection is not in use. .. index:: single: thread safety; Connection @@ -812,9 +823,9 @@ class Connection(Connectable): _branch=False, _execution_options=None): """Construct a new Connection. - Connection objects are typically constructed by an - :class:`~sqlalchemy.engine.Engine`, see the ``connect()`` and - ``contextual_connect()`` methods of Engine. + The constructor here is not public and is only called only by an + :class:`.Engine`. See :meth:`.Engine.connect` and + :meth:`.Engine.contextual_connect` methods. """ self.engine = engine @@ -1154,7 +1165,22 @@ class Connection(Connectable): return self.execute(object, *multiparams, **params).scalar() def execute(self, object, *multiparams, **params): - """Executes and returns a ResultProxy.""" + """Executes the given construct and returns a :class:`.ResultProxy`. + + The construct can be one of: + + * a textual SQL string + * any :class:`.ClauseElement` construct that is also + a subclass of :class:`.Executable`, such as a + :func:`.select` construct + * a :class:`.FunctionElement`, such as that generated + by :attr:`.func`, will be automatically wrapped in + a SELECT statement, which is then executed. + * a :class:`.DDLElement` object + * a :class:`.DefaultGenerator` object + * a :class:`.Compiled` object + + """ for c in type(object).__mro__: if c in Connection.executors: @@ -1451,6 +1477,12 @@ class Connection(Connectable): class Transaction(object): """Represent a Transaction in progress. + The object provides :meth:`.rollback` and :meth:`.commit` + methods in order to control transaction boundaries. It + also implements a context manager interface so that + the Python ``with`` statement can be used with the + :meth:`.Connection.begin` method. + The Transaction object is **not** threadsafe. .. index:: @@ -1458,12 +1490,17 @@ class Transaction(object): """ def __init__(self, connection, parent): + """The constructor for :class:`.Transaction` is private + and is called from within the :class:`.Connection.begin` + implementation. + + """ self.connection = connection self._parent = parent or self self.is_active = True def close(self): - """Close this transaction. + """Close this :class:`.Transaction`. If this transaction is the base transaction in a begin/commit nesting, the transaction will rollback(). Otherwise, the @@ -1471,6 +1508,7 @@ class Transaction(object): This is used to cancel a Transaction without affecting the scope of an enclosing transaction. + """ if not self._parent.is_active: return @@ -1478,6 +1516,9 @@ class Transaction(object): self.rollback() def rollback(self): + """Roll back this :class:`.Transaction`. + + """ if not self._parent.is_active: return self._do_rollback() @@ -1487,6 +1528,8 @@ class Transaction(object): self._parent.rollback() def commit(self): + """Commit this :class:`.Transaction`.""" + if not self._parent.is_active: raise exc.InvalidRequestError("This transaction is inactive") self._do_commit() @@ -1783,7 +1826,20 @@ class Engine(Connectable, log.Identified): conn.close() def execute(self, statement, *multiparams, **params): - """Executes and returns a ResultProxy.""" + """Executes the given construct and returns a :class:`.ResultProxy`. + + The arguments are the same as those used by + :meth:`.Connection.execute`. + + Here, a :class:`.Connection` is acquired using the + :meth:`~.Engine.contextual_connect` method, and the statement executed + with that connection. The returned :class:`.ResultProxy` is flagged + such that when the :class:`.ResultProxy` is exhausted and its + underlying cursor is closed, the :class:`.Connection` created here + will also be closed, which allows its associated DBAPI connection + resource to be returned to the connection pool. + + """ connection = self.contextual_connect(close_with_result=True) return connection.execute(statement, *multiparams, **params) @@ -1800,16 +1856,30 @@ class Engine(Connectable, log.Identified): return connection._execute_compiled(compiled, multiparams, params) def connect(self, **kwargs): - """Return a newly allocated Connection object.""" + """Return a new :class:`.Connection` object. + + The :class:`.Connection`, upon construction, will procure a DBAPI connection + from the :class:`.Pool` referenced by this :class:`.Engine`, + returning it back to the :class:`.Pool` after the :meth:`.Connection.close` + method is called. + + """ return self.Connection(self, **kwargs) def contextual_connect(self, close_with_result=False, **kwargs): - """Return a Connection object which may be newly allocated, - or may be part of some ongoing context. - - This Connection is meant to be used by the various - "auto-connecting" operations. + """Return a :class:`.Connection` object which may be part of some ongoing context. + + By default, this method does the same thing as :meth:`.Engine.connect`. + Subclasses of :class:`.Engine` may override this method + to provide contextual behavior. + + :param close_with_result: When True, the first :class:`.ResultProxy` created + by the :class:`.Connection` will call the :meth:`.Connection.close` method + of that connection as soon as any pending result rows are exhausted. + This is used to supply the "connectionless execution" behavior provided + by the :meth:`.Engine.execute` method. + """ return self.Connection(self, diff --git a/lib/sqlalchemy/engine/reflection.py b/lib/sqlalchemy/engine/reflection.py index 4a34ef1c6..def889e6d 100644 --- a/lib/sqlalchemy/engine/reflection.py +++ b/lib/sqlalchemy/engine/reflection.py @@ -50,27 +50,27 @@ class Inspector(object): consistent interface as well as caching support for previously fetched metadata. - The preferred method to construct an :class:`Inspector` is via the + The preferred method to construct an :class:`.Inspector` is via the :meth:`Inspector.from_engine` method. I.e.:: engine = create_engine('...') insp = Inspector.from_engine(engine) Where above, the :class:`~sqlalchemy.engine.base.Dialect` may opt - to return an :class:`Inspector` subclass that provides additional + to return an :class:`.Inspector` subclass that provides additional methods specific to the dialect's target database. """ def __init__(self, bind): - """Initialize a new :class:`Inspector`. + """Initialize a new :class:`.Inspector`. :param bind: a :class:`~sqlalchemy.engine.base.Connectable`, which is typically an instance of :class:`~sqlalchemy.engine.base.Engine` or :class:`~sqlalchemy.engine.base.Connection`. - For a dialect-specific instance of :class:`Inspector`, see + For a dialect-specific instance of :class:`.Inspector`, see :meth:`Inspector.from_engine` """ @@ -98,12 +98,12 @@ class Inspector(object): :class:`~sqlalchemy.engine.base.Engine` or :class:`~sqlalchemy.engine.base.Connection`. - This method differs from direct a direct constructor call of :class:`Inspector` + This method differs from direct a direct constructor call of :class:`.Inspector` in that the :class:`~sqlalchemy.engine.base.Dialect` is given a chance to provide - a dialect-specific :class:`Inspector` instance, which may provide additional + a dialect-specific :class:`.Inspector` instance, which may provide additional methods. - See the example at :class:`Inspector`. + See the example at :class:`.Inspector`. """ if hasattr(bind.dialect, 'inspector'): diff --git a/lib/sqlalchemy/exc.py b/lib/sqlalchemy/exc.py index 1c412824c..003969f56 100644 --- a/lib/sqlalchemy/exc.py +++ b/lib/sqlalchemy/exc.py @@ -5,9 +5,9 @@ """Exceptions used with SQLAlchemy. -The base exception class is SQLAlchemyError. Exceptions which are raised as a +The base exception class is :class:`.SQLAlchemyError`. Exceptions which are raised as a result of DBAPI exceptions are all subclasses of -:class:`~sqlalchemy.exc.DBAPIError`. +:class:`.DBAPIError`. """ diff --git a/lib/sqlalchemy/ext/compiler.py b/lib/sqlalchemy/ext/compiler.py index 12f1e443d..1bf4b1447 100644 --- a/lib/sqlalchemy/ext/compiler.py +++ b/lib/sqlalchemy/ext/compiler.py @@ -119,6 +119,8 @@ overriding routine and cause an endless loop. Such as, to add "prefix" to all The above compiler will prefix all INSERT statements with "some prefix" when compiled. +.. _type_compilation_extension: + Changing Compilation of Types ============================= diff --git a/lib/sqlalchemy/ext/declarative.py b/lib/sqlalchemy/ext/declarative.py index b1b083e80..6d4bdda43 100755 --- a/lib/sqlalchemy/ext/declarative.py +++ b/lib/sqlalchemy/ext/declarative.py @@ -392,6 +392,8 @@ class declaration:: 'version_id_generator': lambda v:datetime.now() } +.. _declarative_inheritance: + Inheritance Configuration ========================= diff --git a/lib/sqlalchemy/ext/sqlsoup.py b/lib/sqlalchemy/ext/sqlsoup.py index e54cde3ed..e8234e7c7 100644 --- a/lib/sqlalchemy/ext/sqlsoup.py +++ b/lib/sqlalchemy/ext/sqlsoup.py @@ -3,7 +3,15 @@ Introduction ============ SqlSoup provides a convenient way to access existing database tables without -having to declare table or mapper classes ahead of time. It is built on top of the SQLAlchemy ORM and provides a super-minimalistic interface to an existing database. +having to declare table or mapper classes ahead of time. It is built on top of +the SQLAlchemy ORM and provides a super-minimalistic interface to an existing +database. + +SqlSoup effectively provides a coarse grained, alternative interface to +working with the SQLAlchemy ORM, providing a "self configuring" interface +for extremely rudimental operations. It's somewhat akin to a +"super novice mode" version of the ORM. While SqlSoup can be very handy, +users are strongly encouraged to use the full ORM for non-trivial applications. Suppose we have a database with users, books, and loans tables (corresponding to the PyWebOff dataset, if you're curious). diff --git a/lib/sqlalchemy/orm/__init__.py b/lib/sqlalchemy/orm/__init__.py index 17d967db4..39c68f0aa 100644 --- a/lib/sqlalchemy/orm/__init__.py +++ b/lib/sqlalchemy/orm/__init__.py @@ -110,17 +110,19 @@ __all__ = ( def scoped_session(session_factory, scopefunc=None): - """Provides thread-local management of Sessions. + """Provides thread-local or scoped management of :class:`.Session` objects. This is a front-end function to - :class:`~sqlalchemy.orm.scoping.ScopedSession`. + :class:`.ScopedSession`. :param session_factory: a callable function that produces :class:`Session` instances, such as :func:`sessionmaker`. - :param scopefunc: optional, TODO + :param scopefunc: Optional "scope" function which would be + passed to the :class:`.ScopedRegistry`. If None, the + :class:`.ThreadLocalRegistry` is used by default. - :returns: an :class:`~sqlalchemy.orm.scoping.ScopedSession` instance + :returns: an :class:`.ScopedSession` instance Usage:: @@ -179,12 +181,8 @@ def create_session(bind=None, **kwargs): def relationship(argument, secondary=None, **kwargs): """Provide a relationship of a primary Mapper to a secondary Mapper. - .. note:: This function is known as :func:`relation` in all versions - of SQLAlchemy prior to version 0.6beta2, including the 0.5 and 0.4 - series. :func:`~sqlalchemy.orm.relationship()` is only available - starting with SQLAlchemy 0.6beta2. The :func:`relation` name will - remain available for the foreseeable future in order to enable - cross-compatibility. + .. note:: :func:`relationship` is historically known as + :func:`relation` prior to version 0.6. This corresponds to a parent-child or associative table relationship. The constructed class is an instance of :class:`RelationshipProperty`. @@ -261,6 +259,8 @@ def relationship(argument, secondary=None, **kwargs): :param collection_class: a class or callable that returns a new list-holding object. will be used in place of a plain list for storing elements. + Behavior of this attribute is described in detail at + :ref:`custom_collections`. :param comparator_factory: a class which extends :class:`RelationshipProperty.Comparator` which @@ -318,25 +318,28 @@ def relationship(argument, secondary=None, **kwargs): which is already higher up in the chain. This option applies both to joined- and subquery- eager loaders. - :param lazy=('select'|'joined'|'subquery'|'noload'|'dynamic'): specifies - how the related items should be loaded. Values include: + :param lazy='select': specifies + how the related items should be loaded. Default value is + ``select``. Values include: - * 'select' - items should be loaded lazily when the property is first - accessed. + * ``select`` - items should be loaded lazily when the property is first + accessed, using a separate SELECT statement. - * 'joined' - items should be loaded "eagerly" in the same query as - that of the parent, using a JOIN or LEFT OUTER JOIN. + * ``joined`` - items should be loaded "eagerly" in the same query as + that of the parent, using a JOIN or LEFT OUTER JOIN. Whether + the join is "outer" or not is determined by the ``innerjoin`` + parameter. - * 'subquery' - items should be loaded "eagerly" within the same + * ``subquery`` - items should be loaded "eagerly" within the same query as that of the parent, using a second SQL statement which issues a JOIN to a subquery of the original statement. - * 'noload' - no loading should occur at any time. This is to + * ``noload`` - no loading should occur at any time. This is to support "write-only" attributes, or attributes which are populated in some manner specific to the application. - * 'dynamic' - the attribute will return a pre-configured + * ``dynamic`` - the attribute will return a pre-configured :class:`~sqlalchemy.orm.query.Query` object for all read operations, onto which further filtering operations can be applied before iterating the results. The dynamic @@ -351,6 +354,33 @@ def relationship(argument, secondary=None, **kwargs): * None - a synonym for 'noload' + Detailed discussion of loader strategies is at :ref:`loading_toplevel`. + + :param load_on_pending=False: + Indicates loading behavior for transient or pending parent objects. + + When set to ``True``, causes the lazy-loader to + issue a query for a parent object that is not persistent, meaning it has + never been flushed. This may take effect for a pending object when + autoflush is disabled, or for a transient object that has been + "attached" to a :class:`.Session` but is not part of its pending + collection. Attachment of transient objects to the session without + moving to the "pending" state is not a supported behavior at this time. + + Note that the load of related objects on a pending or transient object + also does not trigger any attribute change events - no user-defined + events will be emitted for these attributes, and if and when the + object is ultimately flushed, only the user-specific foreign key + attributes will be part of the modified state. + + The load_on_pending flag does not improve behavior + when the ORM is used normally - object references should be constructed + at the object level, not at the foreign key level, so that they + are present in an ordinary way before flush() proceeds. This flag + is not not intended for general use. + + New in 0.6.5. + :param order_by: indicates the ordering that should be applied when loading these items. @@ -629,7 +659,7 @@ def deferred(*columns, **kwargs): return ColumnProperty(deferred=True, *columns, **kwargs) def mapper(class_, local_table=None, *args, **params): - """Return a new :class:`~sqlalchemy.orm.Mapper` object. + """Return a new :class:`~.Mapper` object. :param class\_: The class to be mapped. @@ -720,7 +750,7 @@ def mapper(class_, local_table=None, *args, **params): when a primary key changes on a joined-table inheritance or other joined table mapping. - When True, it is assumed that ON UPDATE CASCADE is configured on + When True, it is assumed that ON UPDATE CASCADE is configured on the foreign key in the database, and that the database will handle propagation of an UPDATE from a source column to dependent rows. Note that with databases which enforce referential integrity (i.e. @@ -729,20 +759,20 @@ def mapper(class_, local_table=None, *args, **params): value of the attribute on related items which are locally present in the session during a flush. - When False, it is assumed that the database does not enforce + When False, it is assumed that the database does not enforce referential integrity and will not be issuing its own CASCADE operation for an update. The relationship() will issue the appropriate UPDATE statements to the database in response to the change of a referenced key, and items locally present in the session during a flush will also be refreshed. - This flag should probably be set to False if primary key changes + This flag should probably be set to False if primary key changes are expected and the database in use doesn't support CASCADE (i.e. SQLite, MySQL MyISAM tables). Also see the passive_updates flag on :func:`relationship()`. - A future SQLAlchemy release will provide a "detect" feature for + A future SQLAlchemy release will provide a "detect" feature for this flag. :param polymorphic_on: Used with mappers in an inheritance @@ -858,7 +888,8 @@ def synonym(name, map_column=False, descriptor=None, doc=doc) def comparable_property(comparator_factory, descriptor=None): - """Provide query semantics for an unmanaged attribute. + """Provides a method of applying a :class:`.PropComparator` + to any Python descriptor attribute. Allows a regular Python @property (descriptor) to be used in Queries and SQL constructs like a managed attribute. comparable_property wraps a @@ -1041,8 +1072,6 @@ def subqueryload(*keys): """Return a ``MapperOption`` that will convert the property of the given name into an subquery eager load. - .. note:: This function is new as of SQLAlchemy version 0.6beta3. - Used with :meth:`~sqlalchemy.orm.query.Query.options`. examples:: @@ -1067,8 +1096,6 @@ def subqueryload_all(*keys): """Return a ``MapperOption`` that will convert all properties along the given dot-separated path into a subquery eager load. - .. note:: This function is new as of SQLAlchemy version 0.6beta3. - Used with :meth:`~sqlalchemy.orm.query.Query.options`. For example:: @@ -1153,6 +1180,9 @@ def contains_eager(*keys, **kwargs): See also :func:`eagerload` for the "automatic" version of this functionality. + For additional examples of :func:`contains_eager` see + :ref:`contains_eager`. + """ alias = kwargs.pop('alias', None) if kwargs: diff --git a/lib/sqlalchemy/orm/attributes.py b/lib/sqlalchemy/orm/attributes.py index ddfdb8655..7cc8d2161 100644 --- a/lib/sqlalchemy/orm/attributes.py +++ b/lib/sqlalchemy/orm/attributes.py @@ -35,18 +35,25 @@ NEVER_SET = util.symbol('NEVER_SET') # "passive" get settings # TODO: the True/False values need to be factored out -# of the rest of ORM code -# don't fire off any callables, and don't initialize the attribute to -# an empty value PASSIVE_NO_INITIALIZE = True #util.symbol('PASSIVE_NO_INITIALIZE') +"""Symbol indicating that loader callables should + not be fired off, and a non-initialized attribute + should remain that way.""" -# don't fire off any callables, but if no callables present -# then initialize to an empty value/collection # this is used by backrefs. PASSIVE_NO_FETCH = util.symbol('PASSIVE_NO_FETCH') +"""Symbol indicating that loader callables should not be fired off. + Non-initialized attributes should be initialized to an empty value.""" + +PASSIVE_ONLY_PERSISTENT = util.symbol('PASSIVE_ONLY_PERSISTENT') +"""Symbol indicating that loader callables should only fire off for +persistent objects. + +Loads of "previous" values during change events use this flag. +""" -# fire callables/initialize as needed PASSIVE_OFF = False #util.symbol('PASSIVE_OFF') +"""Symbol indicating that loader callables should be executed.""" INSTRUMENTATION_MANAGER = '__sa_instrumentation_manager__' """Attribute, elects custom instrumentation when present on a mapped class. @@ -512,7 +519,7 @@ class ScalarAttributeImpl(AttributeImpl): self, state, dict_.get(self.key, NO_VALUE)) def set(self, state, dict_, value, initiator, passive=PASSIVE_OFF): - if initiator is self: + if initiator and initiator.parent_token is self.parent_token: return if self.dispatch.active_history: @@ -591,7 +598,7 @@ class MutableScalarAttributeImpl(ScalarAttributeImpl): state.mutable_dict.pop(self.key) def set(self, state, dict_, value, initiator, passive=PASSIVE_OFF): - if initiator is self: + if initiator and initiator.parent_token is self.parent_token: return if self.dispatch.on_set: @@ -653,14 +660,14 @@ class ScalarObjectAttributeImpl(ScalarAttributeImpl): setter operation. """ - if initiator is self: + if initiator and initiator.parent_token is self.parent_token: return if self.dispatch.active_history: - old = self.get(state, dict_) + old = self.get(state, dict_, passive=PASSIVE_ONLY_PERSISTENT) else: old = self.get(state, dict_, passive=PASSIVE_NO_FETCH) - + value = self.fire_replace_event(state, dict_, value, old, initiator) dict_[self.key] = value @@ -783,7 +790,7 @@ class CollectionAttributeImpl(AttributeImpl): self.key, state, self.collection_factory) def append(self, state, dict_, value, initiator, passive=PASSIVE_OFF): - if initiator is self: + if initiator and initiator.parent_token is self.parent_token: return collection = self.get_collection(state, dict_, passive=passive) @@ -796,7 +803,7 @@ class CollectionAttributeImpl(AttributeImpl): collection.append_with_event(value, initiator) def remove(self, state, dict_, value, initiator, passive=PASSIVE_OFF): - if initiator is self: + if initiator and initiator.parent_token is self.parent_token: return collection = self.get_collection(state, state.dict, passive=passive) @@ -816,7 +823,7 @@ class CollectionAttributeImpl(AttributeImpl): setter operation. """ - if initiator is self: + if initiator and initiator.parent_token is self.parent_token: return self._set_iterable( @@ -841,15 +848,16 @@ class CollectionAttributeImpl(AttributeImpl): else: new_values = list(iterable) - old = self.get(state, dict_) - - # ignore re-assignment of the current collection, as happens - # implicitly with in-place operators (foo.collection |= other) - if old is iterable: + old = self.get(state, dict_, passive=PASSIVE_ONLY_PERSISTENT) + if old is PASSIVE_NO_RESULT: + old = self.initialize(state, dict_) + elif old is iterable: + # ignore re-assignment of the current collection, as happens + # implicitly with in-place operators (foo.collection |= other) return state.modified_event(dict_, self, True, old) - + old_collection = self.get_collection(state, dict_, old) dict_[self.key] = user_data @@ -919,7 +927,7 @@ class GenericBackrefExtension(interfaces.AttributeExtension): def set(self, state, child, oldchild, initiator): if oldchild is child: return child - + if oldchild is not None and oldchild is not PASSIVE_NO_RESULT: # With lazy=None, there's no guarantee that the full collection is # present when updating via a backref. @@ -1344,8 +1352,10 @@ class _ClassInstrumentationAdapter(ClassManager): return self._get_dict class History(tuple): - """A 3-tuple of added, unchanged and deleted values. - + """A 3-tuple of added, unchanged and deleted values, + representing the changes which have occured on an instrumented + attribute. + Each tuple member is an iterable sequence. """ @@ -1353,9 +1363,18 @@ class History(tuple): __slots__ = () added = property(itemgetter(0)) + """Return the collection of items added to the attribute (the first tuple + element).""" + unchanged = property(itemgetter(1)) + """Return the collection of items that have not changed on the attribute + (the second tuple element).""" + + deleted = property(itemgetter(2)) - + """Return the collection of items that have been removed from the + attribute (the third tuple element).""" + def __new__(cls, added, unchanged, deleted): return tuple.__new__(cls, (added, unchanged, deleted)) @@ -1363,25 +1382,38 @@ class History(tuple): return self != HISTORY_BLANK def empty(self): + """Return True if this :class:`History` has no changes + and no existing, unchanged state. + + """ + return not bool( (self.added or self.deleted) or self.unchanged and self.unchanged != [None] ) def sum(self): + """Return a collection of added + unchanged + deleted.""" + return (self.added or []) +\ (self.unchanged or []) +\ (self.deleted or []) def non_deleted(self): + """Return a collection of added + unchanged.""" + return (self.added or []) +\ (self.unchanged or []) def non_added(self): + """Return a collection of unchanged + deleted.""" + return (self.unchanged or []) +\ (self.deleted or []) def has_changes(self): + """Return True if this :class:`History` has changes.""" + return bool(self.added or self.deleted) def as_state(self): @@ -1442,11 +1474,19 @@ class History(tuple): HISTORY_BLANK = History(None, None, None) def get_history(obj, key, **kwargs): - """Return a History record for the given object and attribute key. + """Return a :class:`.History` record for the given object + and attribute key. - obj is an instrumented object instance. An InstanceState - is accepted directly for backwards compatibility but - this usage is deprecated. + :param obj: an object whose class is instrumented by the + attributes package. + + :param key: string attribute name. + + :param kwargs: Optional keyword arguments currently + include the ``passive`` flag, which indicates if the attribute should be + loaded from the database if not already present (:attr:`PASSIVE_NO_FETCH`), and + if the attribute should be not initialized to a blank value otherwise + (:attr:`PASSIVE_NO_INITIALIZE`). Default is :attr:`PASSIVE_OFF`. """ return get_state_history(instance_state(obj), key, **kwargs) diff --git a/lib/sqlalchemy/orm/collections.py b/lib/sqlalchemy/orm/collections.py index a9ad34239..b52329523 100644 --- a/lib/sqlalchemy/orm/collections.py +++ b/lib/sqlalchemy/orm/collections.py @@ -189,7 +189,7 @@ class collection(object): The recipe decorators all require parens, even those that take no arguments:: - @collection.adds('entity'): + @collection.adds('entity') def insert(self, position, entity): ... @collection.removes_return() @@ -253,7 +253,7 @@ class collection(object): The remover method is called with one positional argument: the value to remove. The method will be automatically decorated with - 'removes_return()' if not already decorated:: + :meth:`removes_return` if not already decorated:: @collection.remover def zap(self, entity): ... @@ -293,7 +293,7 @@ class collection(object): """Tag the method as instrumented. This tag will prevent any decoration from being applied to the method. - Use this if you are orchestrating your own calls to collection_adapter + Use this if you are orchestrating your own calls to :func:`.collection_adapter` in one of the basic SQLAlchemy interface methods, or to prevent an automatic ABC method decoration from wrapping your implementation:: @@ -339,7 +339,7 @@ class collection(object): The default converter implementation will use duck-typing to do the conversion. A dict-like collection will be convert into an iterable - of dictionary values, and other types will simply be iterated. + of dictionary values, and other types will simply be iterated:: @collection.converter def convert(self, other): ... @@ -442,7 +442,8 @@ class collection(object): # public instrumentation interface for 'internally instrumented' # implementations def collection_adapter(collection): - """Fetch the CollectionAdapter for a collection.""" + """Fetch the :class:`.CollectionAdapter` for a collection.""" + return getattr(collection, '_sa_adapter', None) def collection_iter(collection): @@ -545,6 +546,7 @@ class CollectionAdapter(object): def append_with_event(self, item, initiator=None): """Add an entity to the collection, firing mutation events.""" + getattr(self._data(), '_sa_appender')(item, _sa_initiator=initiator) def append_without_event(self, item): @@ -585,7 +587,7 @@ class CollectionAdapter(object): def fire_append_event(self, item, initiator=None): """Notify that a entity has entered the collection. - Initiator is the InstrumentedAttribute that initiated the membership + Initiator is a token owned by the InstrumentedAttribute that initiated the membership mutation, and should be left as None unless you are passing along an initiator value from a chained operation. diff --git a/lib/sqlalchemy/orm/dependency.py b/lib/sqlalchemy/orm/dependency.py index 376afd88d..662cfc67b 100644 --- a/lib/sqlalchemy/orm/dependency.py +++ b/lib/sqlalchemy/orm/dependency.py @@ -734,7 +734,12 @@ class DetectKeySwitch(DependencyProcessor): def per_property_preprocessors(self, uow): if self.prop._reverse_property: - return + if self.passive_updates: + return + else: + if False in (prop.passive_updates for \ + prop in self.prop._reverse_property): + return uow.register_preprocessor(self, False) @@ -797,14 +802,12 @@ class DetectKeySwitch(DependencyProcessor): if switchers: # if primary key values have actually changed somewhere, perform # a linear search through the UOW in search of a parent. - # note that this handler isn't used if the many-to-one - # relationship has a backref. for state in uowcommit.session.identity_map.all_states(): if not issubclass(state.class_, self.parent.class_): continue dict_ = state.dict - related = dict_.get(self.key) - if related is not None: + related = state.get_impl(self.key).get(state, dict_, passive=self.passive_updates) + if related is not attributes.PASSIVE_NO_RESULT and related is not None: related_state = attributes.instance_state(dict_[self.key]) if related_state in switchers: uowcommit.register_object(state, diff --git a/lib/sqlalchemy/orm/dynamic.py b/lib/sqlalchemy/orm/dynamic.py index 918a0aabd..2b0883cfb 100644 --- a/lib/sqlalchemy/orm/dynamic.py +++ b/lib/sqlalchemy/orm/dynamic.py @@ -113,7 +113,7 @@ class DynamicAttributeImpl(attributes.AttributeImpl): def set(self, state, dict_, value, initiator, passive=attributes.PASSIVE_OFF): - if initiator is self: + if initiator and initiator.parent_token is self.parent_token: return self._set_iterable(state, dict_, value) diff --git a/lib/sqlalchemy/orm/exc.py b/lib/sqlalchemy/orm/exc.py index 3f28a3dd3..8f257bdd5 100644 --- a/lib/sqlalchemy/orm/exc.py +++ b/lib/sqlalchemy/orm/exc.py @@ -38,7 +38,7 @@ class FlushError(sa.exc.SQLAlchemyError): class UnmappedError(sa.exc.InvalidRequestError): - """TODO""" + """Base for exceptions that involve expected mappings not present.""" class DetachedInstanceError(sa.exc.SQLAlchemyError): """An attempt to access unloaded attributes on a diff --git a/lib/sqlalchemy/orm/identity.py b/lib/sqlalchemy/orm/identity.py index 4650b066f..30c3a06b7 100644 --- a/lib/sqlalchemy/orm/identity.py +++ b/lib/sqlalchemy/orm/identity.py @@ -15,7 +15,7 @@ class IdentityMap(dict): self._mutable_attrs = set() self._modified = set() self._wr = weakref.ref(self) - + def replace(self, state): raise NotImplementedError() @@ -61,7 +61,7 @@ class IdentityMap(dict): def has_key(self, key): return key in self - + def popitem(self): raise NotImplementedError("IdentityMap uses remove() to remove data") @@ -81,6 +81,9 @@ class IdentityMap(dict): raise NotImplementedError("IdentityMap uses remove() to remove data") class WeakInstanceDict(IdentityMap): + def __init__(self): + IdentityMap.__init__(self) + self._remove_mutex = base_util.threading.Lock() def __getitem__(self, key): state = dict.__getitem__(self, key) @@ -134,8 +137,13 @@ class WeakInstanceDict(IdentityMap): self.remove(state) def remove(self, state): - if dict.pop(self, state.key) is not state: - raise AssertionError("State %s is not present in this identity map" % state) + self._remove_mutex.acquire() + try: + if dict.pop(self, state.key) is not state: + raise AssertionError("State %s is not present in this identity map" % state) + finally: + self._remove_mutex.release() + self._manage_removed_state(state) def discard(self, state): @@ -153,43 +161,56 @@ class WeakInstanceDict(IdentityMap): if o is None: return default return o - - # Py2K + + def items(self): + # Py2K return list(self.iteritems()) - + def iteritems(self): - for state in dict.itervalues(self): # end Py2K - # Py3K - #def items(self): - # for state in dict.values(self): - value = state.obj() - if value is not None: - yield state.key, value + self._remove_mutex.acquire() + try: + result = [] + for state in dict.values(self): + value = state.obj() + if value is not None: + result.append((state.key, value)) - # Py2K + return iter(result) + finally: + self._remove_mutex.release() + def values(self): + # Py2K return list(self.itervalues()) def itervalues(self): - for state in dict.itervalues(self): # end Py2K - # Py3K - #def values(self): - # for state in dict.values(self): - instance = state.obj() - if instance is not None: - yield instance + self._remove_mutex.acquire() + try: + result = [] + for state in dict.values(self): + value = state.obj() + if value is not None: + result.append(value) + return iter(result) + finally: + self._remove_mutex.release() + def all_states(self): - # Py3K - # return list(dict.values(self)) + self._remove_mutex.acquire() + try: + # Py3K + # return list(dict.values(self)) - # Py2K - return dict.values(self) - # end Py2K - + # Py2K + return dict.values(self) + # end Py2K + finally: + self._remove_mutex.release() + def prune(self): return 0 diff --git a/lib/sqlalchemy/orm/interfaces.py b/lib/sqlalchemy/orm/interfaces.py index ececf4a69..8ccbb6b23 100644 --- a/lib/sqlalchemy/orm/interfaces.py +++ b/lib/sqlalchemy/orm/interfaces.py @@ -51,7 +51,7 @@ MANYTOMANY = util.symbol('MANYTOMANY') class MapperExtension(object): """Base implementation for customizing ``Mapper`` behavior. - + New extension classes subclass ``MapperExtension`` and are specified using the ``extension`` mapper() argument, which is a single ``MapperExtension`` or a list of such. A single mapper @@ -74,8 +74,9 @@ class MapperExtension(object): when this symbol is returned. Like EXT_CONTINUE, it also has additional significance in some cases that a default mapper activity will not be performed. - + """ + def instrument_class(self, mapper, class_): """Receive a class when the mapper is first constructed, and has applied instrumentation to the mapped class. @@ -185,7 +186,7 @@ class MapperExtension(object): \**flags extra information about the row, same as criterion in ``create_row_processor()`` method of - :class:`~sqlalchemy.orm.interfaces.MapperProperty` + :class:`~sqlalchemy.orm.interfaces.MapperProperty` """ return EXT_CONTINUE @@ -324,10 +325,10 @@ class MapperExtension(object): def after_delete(self, mapper, connection, instance): """Receive an object instance after that instance is deleted. - - The return value is only significant within the ``MapperExtension`` + + The return value is only significant within the ``MapperExtension`` chain; the parent mapper's behavior isn't modified by this method. - + """ return EXT_CONTINUE @@ -552,11 +553,29 @@ class MapperProperty(object): return operator(self.comparator, value) class PropComparator(expression.ColumnOperators): - """defines comparison operations for MapperProperty objects. + """Defines comparison operations for MapperProperty objects. + + User-defined subclasses of :class:`.PropComparator` may be created. The + built-in Python comparison and math operator methods, such as + ``__eq__()``, ``__lt__()``, ``__add__()``, can be overridden to provide + new operator behaivor. The custom :class:`.PropComparator` is passed to + the mapper property via the ``comparator_factory`` argument. In each case, + the appropriate subclass of :class:`.PropComparator` should be used:: + + from sqlalchemy.orm.properties import \\ + ColumnProperty,\\ + CompositeProperty,\\ + RelationshipProperty - PropComparator instances should also define an accessor 'property' - which returns the MapperProperty associated with this - PropComparator. + class MyColumnComparator(ColumnProperty.Comparator): + pass + + class MyCompositeComparator(CompositeProperty.Comparator): + pass + + class MyRelationshipComparator(RelationshipProperty.Comparator): + pass + """ def __init__(self, prop, mapper, adapter=None): diff --git a/lib/sqlalchemy/orm/mapper.py b/lib/sqlalchemy/orm/mapper.py index 7c64f20db..915c2c922 100644 --- a/lib/sqlalchemy/orm/mapper.py +++ b/lib/sqlalchemy/orm/mapper.py @@ -103,7 +103,7 @@ class Mapper(object): """Construct a new mapper. Mappers are normally constructed via the - :func:`~sqlalchemy.orm.mapper` function. See for details. + :func:`~sqlalchemy.orm.mapper` function. See for details. """ @@ -930,7 +930,19 @@ class Mapper(object): "Mapper '%s' has no property '%s'" % (self, key)) else: return None + + @util.deprecated('0.6.4', + 'Call to deprecated function mapper._get_col_to_pr' + 'op(). Use mapper.get_property_by_column()') + def _get_col_to_prop(self, col): + return self._columntoproperty[col] + + def get_property_by_column(self, column): + """Given a :class:`.Column` object, return the + :class:`.MapperProperty` which maps this column.""" + return self._columntoproperty[column] + @property def iterate_properties(self): """return an iterator of all MapperProperty objects.""" @@ -1282,8 +1294,8 @@ class Mapper(object): column in self.primary_key] # TODO: improve names? - def _get_state_attr_by_column(self, state, dict_, column): - return self._columntoproperty[column]._getattr(state, dict_, column) + def _get_state_attr_by_column(self, state, dict_, column, passive=False): + return self._columntoproperty[column]._getattr(state, dict_, column, passive=passive) def _set_state_attr_by_column(self, state, dict_, column, value): return self._columntoproperty[column]._setattr(state, dict_, value, column) @@ -1373,11 +1385,11 @@ class Mapper(object): """Iterate each element and its mapper in an object graph, for all relationships that meet the given cascade rule. - ``type\_``: + :param type_: The name of the cascade rule (i.e. save-update, delete, etc.) - ``state``: + :param state: The lead InstanceState. child items will be processed per the relationships defined for this object's mapper. diff --git a/lib/sqlalchemy/orm/properties.py b/lib/sqlalchemy/orm/properties.py index 263b611a5..80443a7f3 100644 --- a/lib/sqlalchemy/orm/properties.py +++ b/lib/sqlalchemy/orm/properties.py @@ -116,8 +116,8 @@ class ColumnProperty(StrategizedProperty): group=self.group, *self.columns) - def _getattr(self, state, dict_, column): - return state.get_impl(self.key).get(state, dict_) + def _getattr(self, state, dict_, column, passive=False): + return state.get_impl(self.key).get(state, dict_, passive=passive) def _getcommitted(self, state, dict_, column, passive=False): return state.get_impl(self.key).\ @@ -191,8 +191,8 @@ class CompositeProperty(ColumnProperty): # which issues assertions that do not apply to CompositeColumnProperty super(ColumnProperty, self).do_init() - def _getattr(self, state, dict_, column): - obj = state.get_impl(self.key).get(state, dict_) + def _getattr(self, state, dict_, column, passive=False): + obj = state.get_impl(self.key).get(state, dict_, passive=passive) return self.get_col_value(column, obj) def _getcommitted(self, state, dict_, column, passive=False): @@ -444,6 +444,7 @@ class RelationshipProperty(StrategizedProperty): comparator_factory=None, single_parent=False, innerjoin=False, doc=None, + load_on_pending=False, strategy_class=None, _local_remote_pairs=None, query_class=None): self.uselist = uselist @@ -468,6 +469,7 @@ class RelationshipProperty(StrategizedProperty): self.join_depth = join_depth self.local_remote_pairs = _local_remote_pairs self.extension = extension + self.load_on_pending = load_on_pending self.comparator_factory = comparator_factory or \ RelationshipProperty.Comparator self.comparator = self.comparator_factory(self, None) @@ -720,7 +722,9 @@ class RelationshipProperty(StrategizedProperty): self.prop.parent.compile() return self.prop - def compare(self, op, value, value_is_parent=False, alias_secondary=True): + def compare(self, op, value, + value_is_parent=False, + alias_secondary=True): if op == operators.eq: if value is None: if self.uselist: @@ -730,14 +734,15 @@ class RelationshipProperty(StrategizedProperty): value_is_parent=value_is_parent, alias_secondary=alias_secondary) else: - return self._optimized_compare(value, - value_is_parent=value_is_parent, - alias_secondary=alias_secondary) + return self._optimized_compare(value, + value_is_parent=value_is_parent, + alias_secondary=alias_secondary) else: return op(self.comparator, value) def _optimized_compare(self, value, value_is_parent=False, - adapt_source=None, alias_secondary=True): + adapt_source=None, + alias_secondary=True): if value is not None: value = attributes.instance_state(value) return self._get_strategy(strategies.LazyLoader).lazy_clause(value, @@ -1207,6 +1212,10 @@ class RelationshipProperty(StrategizedProperty): 'when single_parent is not set. Set ' 'single_parent=True on the relationship().' % self) + if self.direction is MANYTOONE and self.passive_deletes: + util.warn("On %s, 'passive_deletes' is normally configured " + "on one-to-many, one-to-one, many-to-many relationships only." + % self) def _determine_local_remote_pairs(self): if not self.local_remote_pairs: diff --git a/lib/sqlalchemy/orm/query.py b/lib/sqlalchemy/orm/query.py index fdc426a07..b22a10b55 100644 --- a/lib/sqlalchemy/orm/query.py +++ b/lib/sqlalchemy/orm/query.py @@ -32,7 +32,7 @@ from sqlalchemy.orm import ( from sqlalchemy.orm.util import ( AliasedClass, ORMAdapter, _entity_descriptor, _entity_info, _is_aliased_class, _is_mapped_class, _orm_columns, _orm_selectable, - join as orm_join, + join as orm_join,with_parent ) @@ -98,6 +98,7 @@ class Query(object): _attributes = util.frozendict() _with_options = () _with_hints = () + _enable_single_crit = True def __init__(self, entities, session=None): self.session = session @@ -633,44 +634,41 @@ class Query(object): @_generative() def populate_existing(self): - """Return a Query that will refresh all instances loaded. - - This includes all entities accessed from the database, including - secondary entities, eagerly-loaded collection items. - - All changes present on entities which are already present in the - session will be reset and the entities will all be marked "clean". - - An alternative to populate_existing() is to expire the Session - fully using session.expire_all(). + """Return a :class:`Query` that will expire and refresh all instances + as they are loaded, or reused from the current :class:`.Session`. + + :meth:`.populate_existing` does not improve behavior when + the ORM is used normally - the :class:`.Session` object's usual + behavior of maintaining a transaction and expiring all attributes + after rollback or commit handles object state automatically. + This method is not intended for general use. """ self._populate_existing = True def with_parent(self, instance, property=None): - """Add a join criterion corresponding to a relationship to the given - parent instance. - - instance - a persistent or detached instance which is related to class - represented by this query. - - property - string name of the property which relates this query's class to the - instance. if None, the method will attempt to find a suitable - property. - - Currently, this method only works with immediate parent relationships, - but in the future may be enhanced to work across a chain of parent - mappers. - + """Add filtering criterion that relates the given instance + to a child object or collection, using its attribute state + as well as an established :func:`.relationship()` + configuration. + + The method uses the :func:`.with_parent` function to generate + the clause, the result of which is passed to :meth:`.Query.filter`. + + Parameters are the same as :func:`.with_parent`, with the exception + that the given property can be None, in which case a search is + performed against this :class:`.Query` object's target mapper. + """ - from sqlalchemy.orm import properties - mapper = object_mapper(instance) + if property is None: + from sqlalchemy.orm import properties + mapper = object_mapper(instance) + for prop in mapper.iterate_properties: if isinstance(prop, properties.PropertyLoader) and \ prop.mapper is self._mapper_zero(): + property = prop break else: raise sa_exc.InvalidRequestError( @@ -680,11 +678,8 @@ class Query(object): self._mapper_zero().class_.__name__, instance.__class__.__name__) ) - else: - prop = mapper.get_property(property, resolve_synonyms=True) - return self.filter(prop.compare( - operators.eq, - instance, value_is_parent=True)) + + return self.filter(with_parent(instance, property)) @_generative() def add_entity(self, entity, alias=None): @@ -707,12 +702,17 @@ class Query(object): """ fromclause = self.with_labels().enable_eagerloads(False).\ + _enable_single_crit(False).\ statement.correlate(None) q = self._from_selectable(fromclause) if entities: q._set_entities(entities) return q - + + @_generative() + def _enable_single_crit(self, val): + self._enable_single_crit = val + @_generative() def _from_selectable(self, fromclause): for attr in ('_statement', '_criterion', '_order_by', '_group_by', @@ -779,8 +779,13 @@ class Query(object): def options(self, *args): """Return a new Query object, applying the given list of - MapperOptions. - + mapper options. + + Most supplied options regard changing how column- and + relationship-mapped attributes are loaded. See the sections + :ref:`deferred` and :ref:`loading_toplevel` for reference + documentation. + """ return self._options(False, *args) @@ -1937,7 +1942,8 @@ class Query(object): else: from_obj = context.froms - self._adjust_for_single_inheritance(context) + if self._enable_single_crit: + self._adjust_for_single_inheritance(context) whereclause = context.whereclause @@ -2274,7 +2280,8 @@ class Query(object): # i.e. when each _MappedEntity has its own FROM froms = context.froms - self._adjust_for_single_inheritance(context) + if self._enable_single_crit: + self._adjust_for_single_inheritance(context) if not context.primary_columns: if self._only_load_props: @@ -2406,6 +2413,7 @@ class Query(object): selected from the total results. """ + for entity, (mapper, adapter, s, i, w) in \ self._mapper_adapter_map.iteritems(): single_crit = mapper._single_table_criterion diff --git a/lib/sqlalchemy/orm/scoping.py b/lib/sqlalchemy/orm/scoping.py index af518e407..c1a5fd577 100644 --- a/lib/sqlalchemy/orm/scoping.py +++ b/lib/sqlalchemy/orm/scoping.py @@ -22,9 +22,13 @@ class ScopedSession(object): Usage:: - Session = scoped_session(sessionmaker(autoflush=True)) + Session = scoped_session(sessionmaker()) - ... use session normally. + ... use Session normally. + + The internal registry is accessible as well, + and by default is an instance of :class:`.ThreadLocalRegistry`. + """ @@ -89,6 +93,7 @@ class ScopedSession(object): class when called. e.g.:: + Session = scoped_session(sessionmaker()) class MyClass(object): diff --git a/lib/sqlalchemy/orm/session.py b/lib/sqlalchemy/orm/session.py index 4727de218..54b41fcc6 100644 --- a/lib/sqlalchemy/orm/session.py +++ b/lib/sqlalchemy/orm/session.py @@ -446,71 +446,8 @@ class SessionTransaction(object): class Session(object): """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. - - 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 :meth:`.Session.add`, - :meth:`.Session.delete`, :meth:`.Session.merge`, and :meth:`.Session.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 :func:`~sqlalchemy.orm.scoped_session` - function. - + The Session's usage paradigm is described at :ref:`session_toplevel`. + """ public_methods = ( @@ -529,8 +466,9 @@ class Session(object): query_cls=query.Query): """Construct a new Session. - Arguments to ``Session`` are described using the - :func:`~sqlalchemy.orm.sessionmaker` function. + Arguments to :class:`.Session` are described using the + :func:`.sessionmaker` function, which is the + typical point of entry. """ @@ -1080,6 +1018,14 @@ class Session(object): if obj is not None: instance_key = mapper._identity_key_from_state(state) + + if _none_set.issubset(instance_key[1]) and \ + not mapper.allow_partial_pks or \ + _none_set.issuperset(instance_key[1]): + raise exc.FlushError('Instance %s has a NULL identity ' + 'key. Check if this flush is occuring at an ' + 'inappropriate time, such as during a load ' + 'operation.' % mapperutil.state_str(state)) if state.key is None: state.key = instance_key diff --git a/lib/sqlalchemy/orm/state.py b/lib/sqlalchemy/orm/state.py index a5f1ed340..107dda814 100644 --- a/lib/sqlalchemy/orm/state.py +++ b/lib/sqlalchemy/orm/state.py @@ -331,7 +331,7 @@ class InstanceState(object): previous = dict_[attr.key] else: previous = attr.get(self, dict_) - + if should_copy and previous not in (None, NO_VALUE, NEVER_SET): previous = attr.copy(previous) diff --git a/lib/sqlalchemy/orm/strategies.py b/lib/sqlalchemy/orm/strategies.py index 3778bb2f9..1696d1456 100644 --- a/lib/sqlalchemy/orm/strategies.py +++ b/lib/sqlalchemy/orm/strategies.py @@ -257,8 +257,8 @@ class LoadDeferredColumns(object): def __init__(self, state, key): self.state, self.key = state, key - def __call__(self, **kw): - if kw.get('passive') is attributes.PASSIVE_NO_FETCH: + def __call__(self, passive=False): + if passive is attributes.PASSIVE_NO_FETCH: return attributes.PASSIVE_NO_RESULT state = self.state @@ -405,7 +405,8 @@ class LazyLoader(AbstractRelationshipLoader): ) def lazy_clause(self, state, reverse_direction=False, - alias_secondary=False, adapt_source=None): + alias_secondary=False, + adapt_source=None): if state is None: return self._lazy_none_clause( reverse_direction, @@ -427,18 +428,26 @@ class LazyLoader(AbstractRelationshipLoader): else: mapper = self.parent_property.parent - def visit_bindparam(bindparam): - if bindparam.key in bind_to_col: - # use the "committed" (database) version to get - # query column values - # also its a deferred value; so that when used - # by Query, the committed value is used - # after an autoflush occurs - o = state.obj() # strong ref - bindparam.value = \ - lambda: mapper._get_committed_attr_by_column( - o, bind_to_col[bindparam.key]) - + o = state.obj() # strong ref + dict_ = attributes.instance_dict(o) + + # use the "committed state" only if we're in a flush + # for this state. + + sess = sessionlib._state_session(state) + if sess is not None and sess._flushing: + def visit_bindparam(bindparam): + if bindparam.key in bind_to_col: + bindparam.value = \ + lambda: mapper._get_committed_state_attr_by_column( + state, dict_, bind_to_col[bindparam.key]) + else: + def visit_bindparam(bindparam): + if bindparam.key in bind_to_col: + bindparam.value = lambda: mapper._get_state_attr_by_column( + state, dict_, bind_to_col[bindparam.key]) + + if self.parent_property.secondary is not None and alias_secondary: criterion = sql_util.ClauseAdapter( self.parent_property.secondary.alias()).\ @@ -446,6 +455,7 @@ class LazyLoader(AbstractRelationshipLoader): criterion = visitors.cloned_traverse( criterion, {}, {'bindparam':visit_bindparam}) + if adapt_source: criterion = adapt_source(criterion) return criterion @@ -469,7 +479,8 @@ class LazyLoader(AbstractRelationshipLoader): return criterion def _class_level_loader(self, state): - if not state.has_identity: + if not state.has_identity and \ + (not self.parent_property.load_on_pending or not state.session_id): return None return LoadLazyAttribute(state, self.key) @@ -559,16 +570,22 @@ class LoadLazyAttribute(object): def __setstate__(self, state): self.state, self.key = state - def __call__(self, **kw): + def __call__(self, passive=False): state = self.state instance_mapper = mapper._state_mapper(state) prop = instance_mapper.get_property(self.key) strategy = prop._get_strategy(LazyLoader) - - if kw.get('passive') is attributes.PASSIVE_NO_FETCH and \ - not strategy.use_get: + pending = not state.key + + if ( + passive is attributes.PASSIVE_NO_FETCH and + not strategy.use_get + ) or ( + passive is attributes.PASSIVE_ONLY_PERSISTENT and + pending + ): return attributes.PASSIVE_NO_RESULT - + if strategy._should_log_debug(): strategy.logger.debug("loading %s", mapperutil.state_attribute_str( @@ -584,21 +601,35 @@ class LoadLazyAttribute(object): q = session.query(prop.mapper)._adapt_all_clauses() + # don't autoflush on pending + # this would be something that's prominent in the + # docs and such + if pending: + q = q.autoflush(False) + if state.load_path: q = q._with_current_path(state.load_path + (self.key,)) - + # if we have a simple primary key load, use mapper.get() # to possibly save a DB round trip if strategy.use_get: ident = [] allnulls = True + if session._flushing: + get_attr = instance_mapper._get_committed_state_attr_by_column + else: + get_attr = instance_mapper._get_state_attr_by_column + + # The many-to-one get is intended to be very fast. Note + # that we don't want to autoflush() if the get() doesn't + # actually have to hit the DB. It is now not necessary + # now that we use the pending attribute state. for primary_key in prop.mapper.primary_key: - val = instance_mapper.\ - _get_committed_state_attr_by_column( + val = get_attr( state, state.dict, strategy._equated_columns[primary_key], - **kw) + passive=passive) if val is attributes.PASSIVE_NO_RESULT: return val allnulls = allnulls and val is None @@ -611,7 +642,7 @@ class LoadLazyAttribute(object): q = q._conditional_options(*state.load_options) key = prop.mapper.identity_key_from_primary_key(ident) - return q._get(key, ident, **kw) + return q._get(key, ident, passive=passive) if prop.order_by: @@ -627,8 +658,15 @@ class LoadLazyAttribute(object): if state.load_options: q = q._conditional_options(*state.load_options) + + lazy_clause = strategy.lazy_clause(state) + + if pending: + bind_values = sql_util.bind_values(lazy_clause) + if None in bind_values: + return None - q = q.filter(strategy.lazy_clause(state)) + q = q.filter(lazy_clause) result = q.all() if strategy.uselist: @@ -728,6 +766,7 @@ class SubqueryLoader(AbstractRelationshipLoader): ("orig_query", SubqueryLoader): orig_query, ('subquery_path', None) : subq_path } + q = q._enable_single_crit(False) # figure out what's being joined. a.k.a. the fun part to_join = [ diff --git a/lib/sqlalchemy/orm/util.py b/lib/sqlalchemy/orm/util.py index 0f4adec00..d68ff4473 100644 --- a/lib/sqlalchemy/orm/util.py +++ b/lib/sqlalchemy/orm/util.py @@ -482,18 +482,30 @@ def outerjoin(left, right, onclause=None, join_to_left=True): return _ORMJoin(left, right, onclause, True, join_to_left) def with_parent(instance, prop): - """Return criterion which selects instances with a given parent. - - :param instance: a parent instance, which should be persistent - or detached. - - :param property: a class-attached descriptor, MapperProperty or - string property name - attached to the parent instance. - - :param \**kwargs: all extra keyword arguments are propagated - to the constructor of Query. - + """Create filtering criterion that relates this query's primary entity + to the given related instance, using established :func:`.relationship()` + configuration. + + The SQL rendered is the same as that rendered when a lazy loader + would fire off from the given parent on that attribute, meaning + that the appropriate state is taken from the parent object in + Python without the need to render joins to the parent table + in the rendered statement. + + As of 0.6.4, this method accepts parent instances in all + persistence states, including transient, persistent, and detached. + Only the requisite primary key/foreign key attributes need to + be populated. Previous versions didn't work with transient + instances. + + :param instance: + An instance which has some :func:`.relationship`. + + :param property: + String property name, or class-bound attribute, which indicates + what relationship from the instance should be used to reconcile the + parent/child relationship. + """ if isinstance(prop, basestring): mapper = object_mapper(instance) @@ -501,7 +513,9 @@ def with_parent(instance, prop): elif isinstance(prop, attributes.QueryableAttribute): prop = prop.property - return prop.compare(operators.eq, instance, value_is_parent=True) + return prop.compare(operators.eq, + instance, + value_is_parent=True) def _entity_info(entity, compile=True): diff --git a/lib/sqlalchemy/pool.py b/lib/sqlalchemy/pool.py index 5d14c1789..9ed5c8067 100644 --- a/lib/sqlalchemy/pool.py +++ b/lib/sqlalchemy/pool.py @@ -223,13 +223,30 @@ class Pool(log.Identified): interfaces.PoolListener._adapt_listener(self, listener) def unique_connection(self): + """Produce a DBAPI connection that is not referenced by any + thread-local context. + + This method is different from :meth:`.Pool.connect` only if the + ``use_threadlocal`` flag has been set to ``True``. + + """ + return _ConnectionFairy(self).checkout() def create_connection(self): + """Called by subclasses to create a new ConnectionRecord.""" + return _ConnectionRecord(self) def recreate(self): - """Return a new instance with identical creation arguments.""" + """Return a new :class:`.Pool`, of the same class as this one + and configured with identical creation arguments. + + This method is used in conjunection with :meth:`dispose` + to close out an entire :class:`.Pool` and create a new one in + its place. + + """ raise NotImplementedError() @@ -240,11 +257,19 @@ class Pool(log.Identified): remaining open, It is advised to not reuse the pool once dispose() is called, and to instead use a new pool constructed by the recreate() method. + """ raise NotImplementedError() def connect(self): + """Return a DBAPI connection from the pool. + + The connection is instrumented such that when its + ``close()`` method is called, the connection will be returned to + the pool. + + """ if not self._use_threadlocal: return _ConnectionFairy(self).checkout() @@ -260,17 +285,33 @@ class Pool(log.Identified): return agent.checkout() def return_conn(self, record): + """Given a _ConnectionRecord, return it to the :class:`.Pool`. + + This method is called when an instrumented DBAPI connection + has its ``close()`` method called. + + """ if self._use_threadlocal and hasattr(self._threadconns, "current"): del self._threadconns.current self.do_return_conn(record) def get(self): + """Return a non-instrumented DBAPI connection from this :class:`.Pool`. + + This is called by ConnectionRecord in order to get its DBAPI + resource. + + """ return self.do_get() def do_get(self): + """Implementation for :meth:`get`, supplied by subclasses.""" + raise NotImplementedError() def do_return_conn(self, conn): + """Implementation for :meth:`return_conn`, supplied by subclasses.""" + raise NotImplementedError() def status(self): diff --git a/lib/sqlalchemy/schema.py b/lib/sqlalchemy/schema.py index 3e7cad70e..cf00f2b96 100644 --- a/lib/sqlalchemy/schema.py +++ b/lib/sqlalchemy/schema.py @@ -142,7 +142,7 @@ class Table(SchemaItem, expression.TableClause): :param \*args: Additional positional arguments are used primarily to add the list of :class:`Column` objects contained within this table. Similar to the style of a CREATE TABLE statement, other - :class:`SchemaItem` constructs may be added here, including + :class:`.SchemaItem` constructs may be added here, including :class:`PrimaryKeyConstraint`, and :class:`ForeignKeyConstraint`. :param autoload: Defaults to False: the Columns for this table should @@ -453,8 +453,22 @@ class Table(SchemaItem, expression.TableClause): def tometadata(self, metadata, schema=RETAIN_SCHEMA): - """Return a copy of this ``Table`` associated with a different - ``MetaData``.""" + """Return a copy of this :class:`Table` associated with a different + :class:`MetaData`. + + E.g.:: + + # create two metadata + meta1 = MetaData('sqlite:///querytest.db') + meta2 = MetaData() + + # load 'users' from the sqlite engine + users_table = Table('users', meta1, autoload=True) + + # create the same Table object for the plain metadata + users_table_2 = users_table.tometadata(meta2) + + """ try: if schema is RETAIN_SCHEMA: @@ -515,7 +529,7 @@ class Column(SchemaItem, expression.ColumnClause): may not function in all cases. :param \*args: Additional positional arguments include various - :class:`SchemaItem` derived constructs which will be applied + :class:`.SchemaItem` derived constructs which will be applied as options to the column. These include instances of :class:`Constraint`, :class:`ForeignKey`, :class:`ColumnDefault`, and :class:`Sequence`. In some cases an equivalent keyword @@ -1246,7 +1260,23 @@ class DefaultGenerator(SchemaItem): class ColumnDefault(DefaultGenerator): """A plain default value on a column. - This could correspond to a constant, a callable function, or a SQL clause. + This could correspond to a constant, a callable function, + or a SQL clause. + + :class:`.ColumnDefault` is generated automatically + whenever the ``default``, ``onupdate`` arguments of + :class:`.Column` are used. A :class:`.ColumnDefault` + can be passed positionally as well. + + For example, the following:: + + Column('foo', Integer, default=50) + + Is equivalent to:: + + Column('foo', Integer, ColumnDefault(50)) + + """ def __init__(self, arg, **kwargs): @@ -1377,7 +1407,20 @@ class Sequence(DefaultGenerator): class FetchedValue(object): - """A default that takes effect on the database side.""" + """A marker for a transparent database-side default. + + Use :class:`.FetchedValue` when the database is configured + to provide some automatic default for a column. + + E.g.:: + + Column('foo', Integer, FetchedValue()) + + Would indicate that some trigger or default generator + will create a new value for the ``foo`` column during an + INSERT. + + """ def __init__(self, for_update=False): self.for_update = for_update @@ -1394,7 +1437,26 @@ class FetchedValue(object): class DefaultClause(FetchedValue): - """A DDL-specified DEFAULT column value.""" + """A DDL-specified DEFAULT column value. + + :class:`.DefaultClause` is a :class:`.FetchedValue` + that also generates a "DEFAULT" clause when + "CREATE TABLE" is emitted. + + :class:`.DefaultClause` is generated automatically + whenever the ``server_default``, ``server_onupdate`` arguments of + :class:`.Column` are used. A :class:`.DefaultClause` + can be passed positionally as well. + + For example, the following:: + + Column('foo', Integer, server_default="50") + + Is equivalent to:: + + Column('foo', Integer, DefaultClause("50")) + + """ def __init__(self, arg, for_update=False): util.assert_arg_type(arg, (basestring, diff --git a/lib/sqlalchemy/sql/expression.py b/lib/sqlalchemy/sql/expression.py index d184816ab..1b1cfee8a 100644 --- a/lib/sqlalchemy/sql/expression.py +++ b/lib/sqlalchemy/sql/expression.py @@ -1269,10 +1269,15 @@ class ClauseElement(Visitable): return engine else: return None - + + @util.pending_deprecation('0.7', + 'Only SQL expressions which subclass ' + ':class:`.Executable` may provide the ' + ':func:`.execute` method.') def execute(self, *multiparams, **params): - """Compile and execute this :class:`ClauseElement`.""" - + """Compile and execute this :class:`ClauseElement`. + + """ e = self.bind if e is None: label = getattr(self, 'description', self.__class__.__name__) @@ -1284,9 +1289,13 @@ class ClauseElement(Visitable): raise exc.UnboundExecutionError(msg) return e._execute_clauseelement(self, multiparams, params) + @util.pending_deprecation('0.7', + 'Only SQL expressions which subclass ' + ':class:`.Executable` may provide the ' + ':func:`.scalar` method.') def scalar(self, *multiparams, **params): - """Compile and execute this :class:`ClauseElement`, returning the - result's scalar representation. + """Compile and execute this :class:`ClauseElement`, returning + the result's scalar representation. """ return self.execute(*multiparams, **params).scalar() @@ -2401,7 +2410,7 @@ class Executable(_Generative): COMMIT will be invoked in order to provide its "autocommit" feature. Typically, all INSERT/UPDATE/DELETE statements as well as CREATE/DROP statements have autocommit behavior enabled; SELECT - constructs do not. Use this option when invokving a SELECT or other + constructs do not. Use this option when invoking a SELECT or other specific SQL construct where COMMIT is desired (typically when calling stored procedures and such). @@ -2436,6 +2445,27 @@ class Executable(_Generative): """ self._execution_options = self._execution_options.union(kw) + def execute(self, *multiparams, **params): + """Compile and execute this :class:`.Executable`.""" + + e = self.bind + if e is None: + label = getattr(self, 'description', self.__class__.__name__) + msg = ('This %s is not bound and does not support direct ' + 'execution. Supply this statement to a Connection or ' + 'Engine for execution. Or, assign a bind to the statement ' + 'or the Metadata of its underlying tables to enable ' + 'implicit execution via this method.' % label) + raise exc.UnboundExecutionError(msg) + return e._execute_clauseelement(self, multiparams, params) + + def scalar(self, *multiparams, **params): + """Compile and execute this :class:`.Executable`, returning the + result's scalar representation. + + """ + return self.execute(*multiparams, **params).scalar() + # legacy, some outside users may be calling this _Executable = Executable diff --git a/lib/sqlalchemy/sql/util.py b/lib/sqlalchemy/sql/util.py index c999ab786..bd4f70247 100644 --- a/lib/sqlalchemy/sql/util.py +++ b/lib/sqlalchemy/sql/util.py @@ -92,6 +92,31 @@ def find_columns(clause): visitors.traverse(clause, {}, {'column':cols.add}) return cols +def bind_values(clause): + """Return an ordered list of "bound" values in the given clause. + + E.g.:: + + >>> expr = and_( + ... table.c.foo==5, table.c.foo==7 + ... ) + >>> bind_values(expr) + [5, 7] + """ + + v = [] + def visit_bindparam(bind): + value = bind.value + + # evaluate callables + if callable(value): + value = value() + + v.append(value) + + visitors.traverse(clause, {}, {'bindparam':visit_bindparam}) + return v + def _quote_ddl_expr(element): if isinstance(element, basestring): element = element.replace("'", "''") diff --git a/lib/sqlalchemy/test/testing.py b/lib/sqlalchemy/test/testing.py index 78cd74d22..41ba3038f 100644 --- a/lib/sqlalchemy/test/testing.py +++ b/lib/sqlalchemy/test/testing.py @@ -398,6 +398,7 @@ def uses_deprecated(*messages): verbiage emitted by the sqlalchemy.util.deprecated decorator. """ + def decorate(fn): def safe(*args, **kw): # todo: should probably be strict about this, too @@ -435,8 +436,6 @@ def resetwarnings(): # warnings.simplefilter('error') - if sys.version_info < (2, 4): - warnings.filterwarnings('ignore', category=FutureWarning) def global_cleanup_assertions(): """Check things that have to be finalized at the end of a test suite. diff --git a/lib/sqlalchemy/types.py b/lib/sqlalchemy/types.py index f00cc2e3c..af7ef22e6 100644 --- a/lib/sqlalchemy/types.py +++ b/lib/sqlalchemy/types.py @@ -75,12 +75,14 @@ class AbstractType(Visitable): This allows systems like the ORM to know if a column value can be considered 'not changed' by comparing the identity of - objects alone. - - Use the :class:`MutableType` mixin or override this method to - return True in custom types that hold mutable values such as - ``dict``, ``list`` and custom objects. - + objects alone. Values such as dicts, lists which + are serialized into strings are examples of "mutable" + column structures. + + When this method is overridden, :meth:`copy_value` should + also be supplied. The :class:`.MutableType` mixin + is recommended as a helper. + """ return False @@ -485,6 +487,19 @@ class TypeDecorator(AbstractType): return self.impl.compare_values(x, y) def is_mutable(self): + """Return True if the target Python type is 'mutable'. + + This allows systems like the ORM to know if a column value can + be considered 'not changed' by comparing the identity of + objects alone. Values such as dicts, lists which + are serialized into strings are examples of "mutable" + column structures. + + When this method is overridden, :meth:`copy_value` should + also be supplied. The :class:`.MutableType` mixin + is recommended as a helper. + + """ return self.impl.is_mutable() def _adapt_expression(self, op, othertype): @@ -562,7 +577,12 @@ class MutableType(object): """ def is_mutable(self): - """Return True, mutable.""" + """Return True if the target Python type is 'mutable'. + + For :class:`.MutableType`, this method is set to + return ``True``. + + """ return True def copy_value(self, value): @@ -1600,6 +1620,13 @@ class PickleType(MutableType, TypeDecorator): return x == y def is_mutable(self): + """Return True if the target Python type is 'mutable'. + + When this method is overridden, :meth:`copy_value` should + also be supplied. The :class:`.MutableType` mixin + is recommended as a helper. + + """ return self.mutable diff --git a/lib/sqlalchemy/util.py b/lib/sqlalchemy/util.py index d5227b447..3f1b89cfc 100644 --- a/lib/sqlalchemy/util.py +++ b/lib/sqlalchemy/util.py @@ -1271,16 +1271,30 @@ class UniqueAppender(object): class ScopedRegistry(object): """A Registry that can store one or multiple instances of a single - class on a per-thread scoped basis, or on a customized scope. + class on the basis of a "scope" function. + + The object implements ``__call__`` as the "getter", so by + calling ``myregistry()`` the contained object is returned + for the current scope. - createfunc + :param createfunc: a callable that returns a new object to be placed in the registry - scopefunc + :param scopefunc: a callable that will return a key to store/retrieve an object. """ def __init__(self, createfunc, scopefunc): + """Construct a new :class:`.ScopedRegistry`. + + :param createfunc: A creation function that will generate + a new value for the current scope, if none is present. + + :param scopefunc: A function that returns a hashable + token representing the current scope (such as, current + thread identifier). + + """ self.createfunc = createfunc self.scopefunc = scopefunc self.registry = {} @@ -1293,18 +1307,28 @@ class ScopedRegistry(object): return self.registry.setdefault(key, self.createfunc()) def has(self): + """Return True if an object is present in the current scope.""" + return self.scopefunc() in self.registry def set(self, obj): + """Set the value forthe current scope.""" + self.registry[self.scopefunc()] = obj def clear(self): + """Clear the current scope, if any.""" + try: del self.registry[self.scopefunc()] except KeyError: pass class ThreadLocalRegistry(ScopedRegistry): + """A :class:`.ScopedRegistry` that uses a ``threading.local()`` + variable for storage. + + """ def __init__(self, createfunc): self.createfunc = createfunc self.registry = threading.local() |
