]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
- [feature] Added support for .info dictionary argument to
authorMike Bayer <mike_mp@zzzcomputing.com>
Fri, 24 Aug 2012 22:48:42 +0000 (18:48 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Fri, 24 Aug 2012 22:48:42 +0000 (18:48 -0400)
column_property(), relationship(), composite().
All MapperProperty classes have an auto-creating .info
dict available overall.

CHANGES
doc/build/core/schema.rst
lib/sqlalchemy/engine/base.py
lib/sqlalchemy/orm/__init__.py
lib/sqlalchemy/orm/descriptor_props.py
lib/sqlalchemy/orm/interfaces.py
lib/sqlalchemy/orm/properties.py
lib/sqlalchemy/pool.py
lib/sqlalchemy/schema.py
test/engine/test_pool.py
test/orm/test_mapper.py

diff --git a/CHANGES b/CHANGES
index 77d29ab2b2dd9d9ffbccbd5c3ca3dd07c361e0de..39102110c75b744bba2903ff3f56964b67f7c147 100644 (file)
--- 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
index 709366f2b0b62d035a403e243b9c582f46d25736..8e3c77eede9739e50b3fd8487748703cab2ad69c 100644 (file)
@@ -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 <http://alembic.readthedocs.org>`_ - 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 <http://code.google.com/p/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 <declarative_toplevel>`
-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.
 
index bf313199467c38e34af070722b9353b260967089..2894f2c219a573f8ce3d98b70acd2ef83b6ecc14 100644 (file)
@@ -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
 
index 2078b2396125ca9ee2d7426258473465d743f658..f31e8b0237cdf5ff60c9bed1b2e06750bec8566d 100644 (file)
@@ -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
index f4c2e1a9063ac1ac9fb4a3d0e12379ed675ca07e..fdbe44c6c14762cc3119f2318b6d4f4f4b793a26 100644 (file)
@@ -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()
 
index f41c5894e5d4cf16c4a577d7f1b670372e793f09..12c38b595be134b422bfdbe7685a98504cd0f076 100644 (file)
@@ -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
 
index f52e914f73e0cd1e34cbca0083f63409f4110dff..ad48234c260bdd20d38df257071bd33661935aff 100644 (file)
@@ -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':
index 0b3d7d0ebe2ee15841cc8becdc4659049e36797a..caef60c2ff991219646e163b1be73016fe3790f8 100644 (file)
@@ -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):
index 4ce27582bceea1dc3744a55ac174af255e2cc861..8b46dc2507fc81b93fd8ccfd71b097c477e3e2e5 100644 (file)
@@ -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."""
index d8013b89027e9f1485ddd29777724f465c12b506..e5e3b42cea27bdaa74efa0a9bf3236b8659a7144 100644 (file)
@@ -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()
index ab66ad65ff1d75d63837269d02e66c4c45192fc8..88237a951030f086d9c14629f33bbf4409f0ada0 100644 (file)
@@ -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,