From: Mike Bayer Date: Fri, 24 Aug 2012 22:48:42 +0000 (-0400) Subject: - [feature] Added support for .info dictionary argument to X-Git-Tag: rel_0_8_0b1~210 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=393b29477648e9b2db9597916a0e53602610ac44;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git - [feature] Added support for .info dictionary argument to column_property(), relationship(), composite(). All MapperProperty classes have an auto-creating .info dict available overall. --- diff --git a/CHANGES b/CHANGES index 77d29ab2b2..39102110c7 100644 --- a/CHANGES +++ b/CHANGES @@ -123,6 +123,11 @@ underneath "0.7.xx". Both features should be avoided, however. [ticket:2372] + - [feature] Added support for .info dictionary argument to + column_property(), relationship(), composite(). + All MapperProperty classes have an auto-creating .info + dict available overall. + - [feature] Adding/removing None from a mapped collection now generates attribute events. Previously, a None append would be ignored in some cases. Related diff --git a/doc/build/core/schema.rst b/doc/build/core/schema.rst index 709366f2b0..8e3c77eede 100644 --- a/doc/build/core/schema.rst +++ b/doc/build/core/schema.rst @@ -257,8 +257,8 @@ While SQLAlchemy directly supports emitting CREATE and DROP statements for schem constructs, the ability to alter those constructs, usually via the ALTER statement as well as other database-specific constructs, is outside of the scope of SQLAlchemy itself. While it's easy enough to emit ALTER statements and similar by hand, -such as by passing a string to :meth:`.Connection.execute` or by using the -:class:`.DDL` construct, it's a common practice to automate the maintenance of +such as by passing a string to :meth:`.Connection.execute` or by using the +:class:`.DDL` construct, it's a common practice to automate the maintenance of database schemas in relation to application code using schema migration tools. There are two major migration tools available for SQLAlchemy: @@ -266,12 +266,12 @@ There are two major migration tools available for SQLAlchemy: * `Alembic `_ - Written by the author of SQLAlchemy, Alembic features a highly customizable environment and a minimalistic usage pattern, supporting such features as transactional DDL, automatic generation of "candidate" - migrations, an "offline" mode which generates SQL scripts, and support for branch + migrations, an "offline" mode which generates SQL scripts, and support for branch resolution. * `SQLAlchemy-Migrate `_ - The original migration tool for SQLAlchemy, SQLAlchemy-Migrate is widely used and continues - under active development. SQLAlchemy-Migrate includes features such as - SQL script generation, ORM class generation, ORM model comparison, and extensive + under active development. SQLAlchemy-Migrate includes features such as + SQL script generation, ORM class generation, ORM model comparison, and extensive support for SQLite migrations. .. _metadata_binding: @@ -417,6 +417,7 @@ Column, Table, MetaData API .. autoclass:: SchemaItem :show-inheritance: + :members: .. autoclass:: Table :members: @@ -1092,11 +1093,11 @@ Setting up Constraints when using the Declarative ORM Extension The :class:`.Table` is the SQLAlchemy Core construct that allows one to define table metadata, which among other things can be used by the SQLAlchemy ORM as a target to map a class. The :ref:`Declarative ` -extension allows the :class:`.Table` object to be created automatically, given +extension allows the :class:`.Table` object to be created automatically, given the contents of the table primarily as a mapping of :class:`.Column` objects. To apply table-level constraint objects such as :class:`.ForeignKeyConstraint` -to a table defined using Declarative, use the ``__table_args__`` attribute, +to a table defined using Declarative, use the ``__table_args__`` attribute, described at :ref:`declarative_table_args`. Constraints API @@ -1179,9 +1180,9 @@ INDEX" is issued right after the create statements for the table: CREATE INDEX idx_col34 ON mytable (col3, col4){stop} Note in the example above, the :class:`.Index` construct is created -externally to the table which it corresponds, using :class:`.Column` +externally to the table which it corresponds, using :class:`.Column` objects directly. :class:`.Index` also supports -"inline" definition inside the :class:`.Table`, using string names to +"inline" definition inside the :class:`.Table`, using string names to identify columns:: meta = MetaData() @@ -1308,7 +1309,7 @@ constraint will be added via ALTER: event.listen( users, - "after_create", + "after_create", AddConstraint(constraint) ) event.listen( @@ -1331,11 +1332,11 @@ constraint will be added via ALTER: DROP TABLE users{stop} The real usefulness of the above becomes clearer once we illustrate the :meth:`.DDLEvent.execute_if` -method. This method returns a modified form of the DDL callable which will +method. This method returns a modified form of the DDL callable which will filter on criteria before responding to a received event. It accepts a parameter ``dialect``, which is the string name of a dialect or a tuple of such, which will limit the execution of the item to just those dialects. It also -accepts a ``callable_`` parameter which may reference a Python callable which will +accepts a ``callable_`` parameter which may reference a Python callable which will be invoked upon event reception, returning ``True`` or ``False`` indicating if the event should proceed. diff --git a/lib/sqlalchemy/engine/base.py b/lib/sqlalchemy/engine/base.py index bf31319946..2894f2c219 100644 --- a/lib/sqlalchemy/engine/base.py +++ b/lib/sqlalchemy/engine/base.py @@ -253,7 +253,15 @@ class Connection(Connectable): @property def info(self): - """A collection of per-DB-API connection instance properties.""" + """Info dictionary associated with the underlying DBAPI connection + referred to by this :class:`.Connection`, allowing user-defined + data to be associated with the connection. + + The data here will follow along with the DBAPI connection including + after it is returned to the connection pool and used again + in subsequent instances of :class:`.Connection`. + + """ return self.connection.info diff --git a/lib/sqlalchemy/orm/__init__.py b/lib/sqlalchemy/orm/__init__.py index 2078b23961..f31e8b0237 100644 --- a/lib/sqlalchemy/orm/__init__.py +++ b/lib/sqlalchemy/orm/__init__.py @@ -417,6 +417,11 @@ def relationship(argument, secondary=None, **kwargs): more specific system of describing which columns in a particular ``primaryjoin`` should be considered "foreign". + :param info: Optional data dictionary which will be populated into the + :attr:`.MapperProperty.info` attribute of this object. + + .. versionadded:: 0.8 + :param innerjoin=False: when ``True``, joined eager loads will use an inner join to join against related tables instead of an outer join. The purpose @@ -742,6 +747,11 @@ def column_property(*cols, **kw): .. versionadded:: 0.7.3 + :param info: Optional data dictionary which will be populated into the + :attr:`.MapperProperty.info` attribute of this object. + + .. versionadded:: 0.8 + :param extension: an :class:`.AttributeExtension` @@ -794,6 +804,11 @@ def composite(class_, *cols, **kwargs): optional string that will be applied as the doc on the class-bound descriptor. + :param info: Optional data dictionary which will be populated into the + :attr:`.MapperProperty.info` attribute of this object. + + .. versionadded:: 0.8 + :param extension: an :class:`.AttributeExtension` instance, or list of extensions, which will be prepended to the list of diff --git a/lib/sqlalchemy/orm/descriptor_props.py b/lib/sqlalchemy/orm/descriptor_props.py index f4c2e1a906..fdbe44c6c1 100644 --- a/lib/sqlalchemy/orm/descriptor_props.py +++ b/lib/sqlalchemy/orm/descriptor_props.py @@ -92,6 +92,9 @@ class CompositeProperty(DescriptorProperty): self.group = kwargs.get('group', None) self.comparator_factory = kwargs.pop('comparator_factory', self.__class__.Comparator) + if 'info' in kwargs: + self.info = kwargs.pop('info') + util.set_creation_order(self) self._create_descriptor() diff --git a/lib/sqlalchemy/orm/interfaces.py b/lib/sqlalchemy/orm/interfaces.py index f41c5894e5..12c38b595b 100644 --- a/lib/sqlalchemy/orm/interfaces.py +++ b/lib/sqlalchemy/orm/interfaces.py @@ -126,6 +126,22 @@ class MapperProperty(_InspectionAttr): def instrument_class(self, mapper): # pragma: no-coverage raise NotImplementedError() + @util.memoized_property + def info(self): + """Info dictionary associated with the object, allowing user-defined + data to be associated with this :class:`.MapperProperty`. + + The dictionary is generated when first accessed. Alternatively, + it can be specified as a constructor argument to the + :func:`.column_property`, :func:`.relationship`, or :func:`.composite` + functions. + + .. versionadded:: 0.8 Added support for .info to all + :class:`.MapperProperty` subclasses. + + """ + return {} + _configure_started = False _configure_finished = False diff --git a/lib/sqlalchemy/orm/properties.py b/lib/sqlalchemy/orm/properties.py index f52e914f73..ad48234c26 100644 --- a/lib/sqlalchemy/orm/properties.py +++ b/lib/sqlalchemy/orm/properties.py @@ -63,6 +63,9 @@ class ColumnProperty(StrategizedProperty): :param extension: + :param info: Optional data dictionary which will be populated into the + :attr:`.info` attribute of this object. + """ self._orig_columns = [expression._labeled(c) for c in columns] self.columns = [expression._labeled(_orm_full_deannotate(c)) @@ -77,6 +80,9 @@ class ColumnProperty(StrategizedProperty): self.active_history = kwargs.pop('active_history', False) self.expire_on_flush = kwargs.pop('expire_on_flush', True) + if 'info' in kwargs: + self.info = kwargs.pop('info') + if 'doc' in kwargs: self.doc = kwargs.pop('doc') else: @@ -243,7 +249,8 @@ class RelationshipProperty(StrategizedProperty): cascade_backrefs=True, load_on_pending=False, strategy_class=None, _local_remote_pairs=None, - query_class=None): + query_class=None, + info=None): self.uselist = uselist self.argument = argument @@ -275,6 +282,9 @@ class RelationshipProperty(StrategizedProperty): self.comparator = self.comparator_factory(self, None) util.set_creation_order(self) + if info is not None: + self.info = info + if strategy_class: self.strategy_class = strategy_class elif self.lazy == 'dynamic': diff --git a/lib/sqlalchemy/pool.py b/lib/sqlalchemy/pool.py index 0b3d7d0ebe..caef60c2ff 100644 --- a/lib/sqlalchemy/pool.py +++ b/lib/sqlalchemy/pool.py @@ -278,13 +278,16 @@ class _ConnectionRecord(object): def __init__(self, pool): self.__pool = pool self.connection = self.__connect() - self.info = {} pool.dispatch.first_connect.\ for_modify(pool.dispatch).\ exec_once(self.connection, self) pool.dispatch.connect(self.connection, self) + @util.memoized_property + def info(self): + return {} + def close(self): if self.connection is not None: self.__pool.logger.debug("Closing connection %r", self.connection) @@ -387,10 +390,6 @@ class _ConnectionFairy(object): """Proxies a DB-API connection and provides return-on-dereference support.""" - __slots__ = '_pool', '__counter', 'connection', \ - '_connection_record', '__weakref__', \ - '_detached_info', '_echo' - def __init__(self, pool): self._pool = pool self.__counter = 0 @@ -400,7 +399,8 @@ class _ConnectionFairy(object): conn = self.connection = self._connection_record.get_connection() rec.fairy = weakref.ref( self, - lambda ref:_finalize_fairy and _finalize_fairy(conn, rec, pool, ref, _echo) + lambda ref: _finalize_fairy and \ + _finalize_fairy(conn, rec, pool, ref, _echo) ) _refs.add(rec) except: @@ -420,20 +420,21 @@ class _ConnectionFairy(object): def is_valid(self): return self.connection is not None - @property + @util.memoized_property def info(self): - """An info collection unique to this DB-API connection.""" + """Info dictionary associated with the underlying DBAPI connection + referred to by this :class:`.ConnectionFairy`, allowing user-defined + data to be associated with the connection. + + The data here will follow along with the DBAPI connection including + after it is returned to the connection pool and used again + in subsequent instances of :class:`.ConnectionFairy`. + """ try: return self._connection_record.info except AttributeError: - if self.connection is None: - raise exc.InvalidRequestError("This connection is closed") - try: - return self._detached_info - except AttributeError: - self._detached_info = value = {} - return value + raise exc.InvalidRequestError("This connection is closed") def invalidate(self, e=None): """Mark this connection as invalidated. @@ -500,8 +501,7 @@ class _ConnectionFairy(object): self._connection_record.fairy = None self._connection_record.connection = None self._pool._do_return_conn(self._connection_record) - self._detached_info = \ - self._connection_record.info.copy() + self.info = self.info.copy() self._connection_record = None def close(self): diff --git a/lib/sqlalchemy/schema.py b/lib/sqlalchemy/schema.py index 4ce27582bc..8b46dc2507 100644 --- a/lib/sqlalchemy/schema.py +++ b/lib/sqlalchemy/schema.py @@ -71,6 +71,14 @@ class SchemaItem(events.SchemaEventTarget, visitors.Visitable): @util.memoized_property def info(self): + """Info dictionary associated with the object, allowing user-defined + data to be associated with this :class:`.SchemaItem`. + + The dictionary is automatically generated when first accessed. + It can also be specified in the constructor of some objects, + such as :class:`.Table` and :class:`.Column`. + + """ return {} def _get_table_key(name, schema): @@ -204,8 +212,8 @@ class Table(SchemaItem, expression.TableClause): ``Table`` object. Defaults to ``None`` which indicates all columns should be reflected. - :param info: A dictionary which defaults to ``{}``. A space to store - application specific data. This must be a dictionary. + :param info: Optional data dictionary which will be populated into the + :attr:`.SchemaItem.info` attribute of this object. :param keep_existing: When ``True``, indicates that if this Table is already present in the given :class:`.MetaData`, ignore @@ -408,11 +416,6 @@ class Table(SchemaItem, expression.TableClause): self, include_columns, exclude_columns ) - @util.memoized_property - def info(self): - """Dictionary provided for storage of additional information.""" - return {} - @property def _sorted_constraints(self): """Return the set of constraints as a list, sorted by creation order.""" @@ -797,8 +800,8 @@ class Column(SchemaItem, expression.ColumnClause): contain multiple columns, use the :class:`.Index` construct instead. - :param info: A dictionary which defaults to ``{}``. A space to store - application specific data. This must be a dictionary. + :param info: Optional data dictionary which will be populated into the + :attr:`.SchemaItem.info` attribute of this object. :param nullable: If set to the default of ``True``, indicates the column will be rendered as allowing NULL, else it's rendered as @@ -971,11 +974,6 @@ class Column(SchemaItem, expression.ColumnClause): else: return self.description - @util.memoized_property - def info(self): - """Dictionary provided for storage of additional information.""" - return {} - def references(self, column): """Return True if this Column references the given column via foreign key.""" diff --git a/test/engine/test_pool.py b/test/engine/test_pool.py index d8013b8902..e5e3b42cea 100644 --- a/test/engine/test_pool.py +++ b/test/engine/test_pool.py @@ -182,7 +182,7 @@ class PoolTest(PoolTestBase): lazy_gc() self.assert_(p.checkedout() == 0) - def test_properties(self): + def test_info(self): p = self._queuepool_fixture(pool_size=1, max_overflow=0) c = p.connect() diff --git a/test/orm/test_mapper.py b/test/orm/test_mapper.py index ab66ad65ff..88237a9510 100644 --- a/test/orm/test_mapper.py +++ b/test/orm/test_mapper.py @@ -387,6 +387,27 @@ class MapperTest(_fixtures.FixtureTest, AssertsCompiledSQL): }) assert m.get_property('addresses') + def test_info(self): + users = self.tables.users + Address = self.classes.Address + class MyComposite(object): + pass + for constructor, args in [ + (column_property, (users.c.name,)), + (relationship, (Address,)), + (composite, (MyComposite, 'id', 'name')) + ]: + obj = constructor(*args, info={"x": "y"}) + eq_(obj.info, {"x": "y"}) + obj.info["q"] = "p" + eq_(obj.info, {"x": "y", "q": "p"}) + + obj = constructor(*args) + eq_(obj.info, {}) + obj.info["q"] = "p" + eq_(obj.info, {"q": "p"}) + + def test_add_property(self): users, addresses, Address = (self.tables.users, self.tables.addresses,