Commits

Mike Bayer  committed 4576c7a

- [feature] Added support for .info dictionary argument to
column_property(), relationship(), composite().
All MapperProperty classes have an auto-creating .info
dict available overall.

  • Participants
  • Parent commits 5a6d3fd

Comments (0)

Files changed (11)

     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

File doc/build/core/schema.rst

 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:
 * `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:
 
 .. autoclass:: SchemaItem
     :show-inheritance:
+    :members:
 
 .. autoclass:: Table
     :members:
 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
     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()
 
     event.listen(
         users,
-        "after_create", 
+        "after_create",
         AddConstraint(constraint)
     )
     event.listen(
     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.
 

File lib/sqlalchemy/engine/base.py

 
     @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
 

File lib/sqlalchemy/orm/__init__.py

           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
 
         .. 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`
       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

File lib/sqlalchemy/orm/descriptor_props.py

         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()
 

File lib/sqlalchemy/orm/interfaces.py

     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
 

File lib/sqlalchemy/orm/properties.py

 
         :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))
         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:
         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
         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':

File lib/sqlalchemy/pool.py

     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)
     """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
             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:
     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.
             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):

File lib/sqlalchemy/schema.py

 
     @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):
         ``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
                     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."""
             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
         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."""

File test/engine/test_pool.py

                     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()

File test/orm/test_mapper.py

                                         })
         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,