]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
Extended the :doc:`/core/inspection` system so that all Python descriptors
authorMike Bayer <mike_mp@zzzcomputing.com>
Sun, 30 Dec 2012 00:31:28 +0000 (19:31 -0500)
committerMike Bayer <mike_mp@zzzcomputing.com>
Sun, 30 Dec 2012 00:31:28 +0000 (19:31 -0500)
associated with the ORM or its extensions can be retrieved.
This fulfills the common request of being able to inspect
all :class:`.QueryableAttribute` descriptors in addition to
extension types such as :class:`.hybrid_property` and
:class:`.AssociationProxy`.  See :attr:`.Mapper.all_orm_descriptors`.

12 files changed:
doc/build/changelog/changelog_08.rst
doc/build/orm/extensions/associationproxy.rst
doc/build/orm/extensions/hybrid.rst
doc/build/orm/internals.rst
doc/build/orm/tutorial.rst
lib/sqlalchemy/ext/associationproxy.py
lib/sqlalchemy/ext/hybrid.py
lib/sqlalchemy/orm/attributes.py
lib/sqlalchemy/orm/instrumentation.py
lib/sqlalchemy/orm/interfaces.py
lib/sqlalchemy/orm/mapper.py
test/orm/test_inspect.py

index b350162a414ef6ce9cbe70a272a397176b1416ea..6c584e4b4f2b0e4989e7feb597debed283079c40 100644 (file)
@@ -4,8 +4,17 @@
 ==============
 
 .. changelog::
-    :version: 0.8.0b2
-    :released: December 14, 2012
+    :version: 0.8.0
+
+    .. change::
+        :tags: orm, feature
+
+      Extended the :doc:`/core/inspection` system so that all Python descriptors
+      associated with the ORM or its extensions can be retrieved.
+      This fulfills the common request of being able to inspect
+      all :class:`.QueryableAttribute` descriptors in addition to
+      extension types such as :class:`.hybrid_property` and
+      :class:`.AssociationProxy`.  See :attr:`.Mapper.all_orm_descriptors`.
 
     .. change::
         :tags: mysql, feature
       history events are more accurate in scenarios where multiple add/remove
       of the same object occurs.
 
+.. changelog::
+    :version: 0.8.0b2
+    :released: December 14, 2012
+
     .. change::
         :tags: sqlite, bug
         :tickets: 2568
index 03eafda8a64d569eaa3e97c6d6308270aac93393..90bb29ebf8760148a1df8182373ccd7ca2f8bc29 100644 (file)
@@ -7,11 +7,11 @@ Association Proxy
 
 ``associationproxy`` is used to create a read/write view of a
 target attribute across a relationship.  It essentially conceals
-the usage of a "middle" attribute between two endpoints, and 
+the usage of a "middle" attribute between two endpoints, and
 can be used to cherry-pick fields from a collection of
 related objects or to reduce the verbosity of using the association
 object pattern.   Applied creatively, the association proxy allows
-the construction of sophisticated collections and dictionary 
+the construction of sophisticated collections and dictionary
 views of virtually any geometry, persisted to the database using
 standard, transparently configured relational patterns.
 
@@ -97,10 +97,10 @@ for us transparently::
 
 The :class:`.AssociationProxy` object produced by the :func:`.association_proxy` function
 is an instance of a `Python descriptor <http://docs.python.org/howto/descriptor.html>`_.
-It is always declared with the user-defined class being mapped, regardless of 
+It is always declared with the user-defined class being mapped, regardless of
 whether Declarative or classical mappings via the :func:`.mapper` function are used.
 
-The proxy functions by operating upon the underlying mapped attribute 
+The proxy functions by operating upon the underlying mapped attribute
 or collection in response to operations, and changes made via the proxy are immediately
 apparent in the mapped attribute, as well as vice versa.   The underlying
 attribute remains fully accessible.
@@ -129,7 +129,7 @@ Is translated by the association proxy into the operation::
 The example works here because we have designed the constructor for ``Keyword``
 to accept a single positional argument, ``keyword``.   For those cases where a
 single-argument constructor isn't feasible, the association proxy's creational
-behavior can be customized using the ``creator`` argument, which references a 
+behavior can be customized using the ``creator`` argument, which references a
 callable (i.e. Python function) that will produce a new object instance given the
 singular argument.  Below we illustrate this using a lambda as is typical::
 
@@ -137,7 +137,7 @@ singular argument.  Below we illustrate this using a lambda as is typical::
         # ...
 
         # use Keyword(keyword=kw) on append() events
-        keywords = association_proxy('kw', 'keyword', 
+        keywords = association_proxy('kw', 'keyword',
                         creator=lambda kw: Keyword(keyword=kw))
 
 The ``creator`` function accepts a single argument in the case of a list-
@@ -154,15 +154,15 @@ proxies are useful for keeping "association objects" out the way during
 regular use.
 
 Suppose our ``userkeywords`` table above had additional columns
-which we'd like to map explicitly, but in most cases we don't 
+which we'd like to map explicitly, but in most cases we don't
 require direct access to these attributes.  Below, we illustrate
-a new mapping which introduces the ``UserKeyword`` class, which 
+a new mapping which introduces the ``UserKeyword`` class, which
 is mapped to the ``userkeywords`` table illustrated earlier.
 This class adds an additional column ``special_key``, a value which
 we occasionally want to access, but not in the usual case.   We
 create an association proxy on the ``User`` class called
 ``keywords``, which will bridge the gap from the ``user_keywords``
-collection of ``User`` to the ``.keyword`` attribute present on each 
+collection of ``User`` to the ``.keyword`` attribute present on each
 ``UserKeyword``::
 
     from sqlalchemy import Column, Integer, String, ForeignKey
@@ -192,8 +192,8 @@ collection of ``User`` to the ``.keyword`` attribute present on each
         special_key = Column(String(50))
 
         # bidirectional attribute/collection of "user"/"user_keywords"
-        user = relationship(User, 
-                    backref=backref("user_keywords", 
+        user = relationship(User,
+                    backref=backref("user_keywords",
                                     cascade="all, delete-orphan")
                 )
 
@@ -216,14 +216,14 @@ collection of ``User`` to the ``.keyword`` attribute present on each
         def __repr__(self):
             return 'Keyword(%s)' % repr(self.keyword)
 
-With the above configuration, we can operate upon the ``.keywords`` 
+With the above configuration, we can operate upon the ``.keywords``
 collection of each ``User`` object, and the usage of ``UserKeyword``
 is concealed::
 
     >>> user = User('log')
     >>> for kw in (Keyword('new_from_blammo'), Keyword('its_big')):
     ...     user.keywords.append(kw)
-    ... 
+    ...
     >>> print(user.keywords)
     [Keyword('new_from_blammo'), Keyword('its_big')]
 
@@ -234,12 +234,12 @@ Where above, each ``.keywords.append()`` operation is equivalent to::
 The ``UserKeyword`` association object has two attributes here which are populated;
 the ``.keyword`` attribute is populated directly as a result of passing
 the ``Keyword`` object as the first argument.   The ``.user`` argument is then
-assigned as the ``UserKeyword`` object is appended to the ``User.user_keywords`` 
+assigned as the ``UserKeyword`` object is appended to the ``User.user_keywords``
 collection, where the bidirectional relationship configured between ``User.user_keywords``
 and ``UserKeyword.user`` results in a population of the ``UserKeyword.user`` attribute.
 The ``special_key`` argument above is left at its default value of ``None``.
 
-For those cases where we do want ``special_key`` to have a value, we 
+For those cases where we do want ``special_key`` to have a value, we
 create the ``UserKeyword`` object explicitly.  Below we assign all three
 attributes, where the assignment of ``.user`` has the effect of the ``UserKeyword``
 being appended to the ``User.user_keywords`` collection::
@@ -259,7 +259,7 @@ Proxying to Dictionary Based Collections
 
 The association proxy can proxy to dictionary based collections as well.   SQLAlchemy
 mappings usually use the :func:`.attribute_mapped_collection` collection type to
-create dictionary collections, as well as the extended techniques described in 
+create dictionary collections, as well as the extended techniques described in
 :ref:`dictionary_collections`.
 
 The association proxy adjusts its behavior when it detects the usage of a
@@ -269,7 +269,7 @@ arguments to the creation function instead of one, the key and the value. As
 always, this creation function defaults to the constructor of the intermediary
 class, and can be customized using the ``creator`` argument.
 
-Below, we modify our ``UserKeyword`` example such that the ``User.user_keywords`` 
+Below, we modify our ``UserKeyword`` example such that the ``User.user_keywords``
 collection will now be mapped using a dictionary, where the ``UserKeyword.special_key``
 argument will be used as the key for the dictionary.   We then apply a ``creator``
 argument to the ``User.keywords`` proxy so that these values are assigned appropriately
@@ -291,7 +291,7 @@ when new elements are added to the dictionary::
         # proxy to 'user_keywords', instantiating UserKeyword
         # assigning the new key to 'special_key', values to
         # 'keyword'.
-        keywords = association_proxy('user_keywords', 'keyword', 
+        keywords = association_proxy('user_keywords', 'keyword',
                         creator=lambda k, v:
                                     UserKeyword(special_key=k, keyword=v)
                     )
@@ -308,7 +308,7 @@ when new elements are added to the dictionary::
         # bidirectional user/user_keywords relationships, mapping
         # user_keywords with a dictionary against "special_key" as key.
         user = relationship(User, backref=backref(
-                        "user_keywords", 
+                        "user_keywords",
                         collection_class=attribute_mapped_collection("special_key"),
                         cascade="all, delete-orphan"
                         )
@@ -344,8 +344,8 @@ Composite Association Proxies
 
 Given our previous examples of proxying from relationship to scalar
 attribute, proxying across an association object, and proxying dictionaries,
-we can combine all three techniques together to give ``User`` 
-a ``keywords`` dictionary that deals strictly with the string value 
+we can combine all three techniques together to give ``User``
+a ``keywords`` dictionary that deals strictly with the string value
 of ``special_key`` mapped to the string ``keyword``.  Both the ``UserKeyword``
 and ``Keyword`` classes are entirely concealed.  This is achieved by building
 an association proxy on ``User`` that refers to an association proxy
@@ -365,11 +365,11 @@ present on ``UserKeyword``::
         id = Column(Integer, primary_key=True)
         name = Column(String(64))
 
-        # the same 'user_keywords'->'keyword' proxy as in 
+        # the same 'user_keywords'->'keyword' proxy as in
         # the basic dictionary example
         keywords = association_proxy(
-                    'user_keywords', 
-                    'keyword', 
+                    'user_keywords',
+                    'keyword',
                     creator=lambda k, v:
                                 UserKeyword(special_key=k, keyword=v)
                     )
@@ -380,11 +380,11 @@ present on ``UserKeyword``::
     class UserKeyword(Base):
         __tablename__ = 'user_keyword'
         user_id = Column(Integer, ForeignKey('user.id'), primary_key=True)
-        keyword_id = Column(Integer, ForeignKey('keyword.id'), 
+        keyword_id = Column(Integer, ForeignKey('keyword.id'),
                                                         primary_key=True)
         special_key = Column(String)
         user = relationship(User, backref=backref(
-                "user_keywords", 
+                "user_keywords",
                 collection_class=attribute_mapped_collection("special_key"),
                 cascade="all, delete-orphan"
                 )
@@ -394,7 +394,7 @@ present on ``UserKeyword``::
         # 'kw'
         kw = relationship("Keyword")
 
-        # 'keyword' is changed to be a proxy to the 
+        # 'keyword' is changed to be a proxy to the
         # 'keyword' attribute of 'Keyword'
         keyword = association_proxy('kw', 'keyword')
 
@@ -432,8 +432,8 @@ association proxy, to apply a dictionary value to the collection at once::
 
 One caveat with our example above is that because ``Keyword`` objects are created
 for each dictionary set operation, the example fails to maintain uniqueness for
-the ``Keyword`` objects on their string name, which is a typical requirement for 
-a tagging scenario such as this one.  For this use case the recipe 
+the ``Keyword`` objects on their string name, which is a typical requirement for
+a tagging scenario such as this one.  For this use case the recipe
 `UniqueObject <http://www.sqlalchemy.org/trac/wiki/UsageRecipes/UniqueObject>`_, or
 a comparable creational strategy, is
 recommended, which will apply a "lookup first, then create" strategy to the constructor
@@ -450,32 +450,32 @@ and :meth:`.RelationshipProperty.Comparator.has` operations are available, and w
 a "nested" EXISTS clause, such as in our basic association object example::
 
     >>> print(session.query(User).filter(User.keywords.any(keyword='jek')))
-    SELECT user.id AS user_id, user.name AS user_name 
-    FROM user 
-    WHERE EXISTS (SELECT 1 
-    FROM user_keyword 
-    WHERE user.id = user_keyword.user_id AND (EXISTS (SELECT 1 
-    FROM keyword 
+    SELECT user.id AS user_id, user.name AS user_name
+    FROM user
+    WHERE EXISTS (SELECT 1
+    FROM user_keyword
+    WHERE user.id = user_keyword.user_id AND (EXISTS (SELECT 1
+    FROM keyword
     WHERE keyword.id = user_keyword.keyword_id AND keyword.keyword = :keyword_1)))
 
 For a proxy to a scalar attribute, ``__eq__()`` is supported::
 
     >>> print(session.query(UserKeyword).filter(UserKeyword.keyword == 'jek'))
     SELECT user_keyword.*
-    FROM user_keyword 
-    WHERE EXISTS (SELECT 1 
-        FROM keyword 
+    FROM user_keyword
+    WHERE EXISTS (SELECT 1
+        FROM keyword
         WHERE keyword.id = user_keyword.keyword_id AND keyword.keyword = :keyword_1)
 
 and ``.contains()`` is available for a proxy to a scalar collection::
 
     >>> print(session.query(User).filter(User.keywords.contains('jek')))
     SELECT user.*
-    FROM user 
-    WHERE EXISTS (SELECT 1 
-    FROM userkeywords, keyword 
-    WHERE user.id = userkeywords.user_id 
-        AND keyword.id = userkeywords.keyword_id 
+    FROM user
+    WHERE EXISTS (SELECT 1
+    FROM userkeywords, keyword
+    WHERE user.id = userkeywords.user_id
+        AND keyword.id = userkeywords.keyword_id
         AND keyword.keyword = :keyword_1)
 
 :class:`.AssociationProxy` can be used with :meth:`.Query.join` somewhat manually
@@ -508,3 +508,5 @@ API Documentation
 .. autoclass:: AssociationProxy
    :members:
    :undoc-members:
+
+.. autodata:: ASSOCIATION_PROXY
\ No newline at end of file
index 7c6b5f7ebf6427a3966315d01e2254362693bf8a..3ee76fd9bd819cb6f265e36315b2eb5422adf901 100644 (file)
@@ -10,7 +10,13 @@ API Reference
 
 .. autoclass:: hybrid_method
     :members:
+
 .. autoclass:: hybrid_property
     :members:
+
 .. autoclass:: Comparator
     :show-inheritance:
+
+.. autodata:: HYBRID_METHOD
+
+.. autodata:: HYBRID_PROPERTY
index d699f251c9019eb0ae61b7449a495b00d7293ade..38efdb08ad72111e47d1df5c80e5b460f86d4e32 100644 (file)
@@ -27,19 +27,25 @@ sections, are listed here.
     :members:
     :show-inheritance:
 
+.. autoclass:: sqlalchemy.orm.interfaces._InspectionAttr
+    :members:
+    :show-inheritance:
+
 .. autoclass:: sqlalchemy.orm.state.InstanceState
     :members:
     :show-inheritance:
 
 .. autoclass:: sqlalchemy.orm.attributes.InstrumentedAttribute
-    :members:
+    :members: __get__, __set__, __delete__
     :show-inheritance:
-    :inherited-members:
+    :undoc-members:
 
 .. autoclass:: sqlalchemy.orm.interfaces.MapperProperty
     :members:
     :show-inheritance:
 
+.. autodata:: sqlalchemy.orm.interfaces.NOT_EXTENSION
+
 .. autoclass:: sqlalchemy.orm.interfaces.PropComparator
     :members:
     :show-inheritance:
index fadad8551bbfa8604fcedfecc84db6912b419e06..927914ae7a77b8c3039550d150d7693c2bf43bc1 100644 (file)
@@ -274,7 +274,7 @@ exists with a value of ``None`` on our ``User`` instance due to the ``id``
 column we declared in our mapping.  By
 default, the ORM creates class attributes for all columns present
 in the table being mapped.   These class attributes exist as
-`Python descriptors <http://docs.python.org/howto/descriptor.html>`_, and
+:term:`descriptors`, and
 define **instrumentation** for the mapped class. The
 functionality of this instrumentation includes the ability to fire on change
 events, track modifications, and to automatically load new data from the database when
index f6c0764e4b9d8ef42a9a194b86c20732bcb9a383..793a5fde9d242ec17a8a8f88eade8a159cea7a09 100644 (file)
@@ -16,7 +16,7 @@ import itertools
 import operator
 import weakref
 from .. import exc, orm, util
-from ..orm import collections
+from ..orm import collections, interfaces
 from ..sql import not_
 
 
@@ -75,9 +75,22 @@ def association_proxy(target_collection, attr, **kw):
     return AssociationProxy(target_collection, attr, **kw)
 
 
-class AssociationProxy(object):
+ASSOCIATION_PROXY = util.symbol('ASSOCIATION_PROXY')
+"""Symbol indicating an :class:`_InspectionAttr` that's
+    of type :class:`.AssociationProxy`.
+
+   Is assigned to the :attr:`._InspectionAttr.extension_type`
+   attibute.
+
+"""
+
+class AssociationProxy(interfaces._InspectionAttr):
     """A descriptor that presents a read/write view of an object attribute."""
 
+    is_attribute = False
+    extension_type = ASSOCIATION_PROXY
+
+
     def __init__(self, target_collection, attr, creator=None,
                  getset_factory=None, proxy_factory=None,
                  proxy_bulk_set=None):
index 047b2ff95cc7af0a77bd6ea2334cd91d04384892..b274aa766e8d04416b5fa80e438f9c3674884a30 100644 (file)
@@ -628,13 +628,41 @@ there's probably a whole lot of amazing things it can be used for.
 from .. import util
 from ..orm import attributes, interfaces
 
+HYBRID_METHOD = util.symbol('HYBRID_METHOD')
+"""Symbol indicating an :class:`_InspectionAttr` that's
+   of type :class:`.hybrid_method`.
 
-class hybrid_method(object):
+   Is assigned to the :attr:`._InspectionAttr.extension_type`
+   attibute.
+
+   .. seealso::
+
+    :attr:`.Mapper.all_orm_attributes`
+
+"""
+
+HYBRID_PROPERTY = util.symbol('HYBRID_PROPERTY')
+"""Symbol indicating an :class:`_InspectionAttr` that's
+    of type :class:`.hybrid_method`.
+
+   Is assigned to the :attr:`._InspectionAttr.extension_type`
+   attibute.
+
+   .. seealso::
+
+    :attr:`.Mapper.all_orm_attributes`
+
+"""
+
+class hybrid_method(interfaces._InspectionAttr):
     """A decorator which allows definition of a Python object method with both
     instance-level and class-level behavior.
 
     """
 
+    is_attribute = True
+    extension_type = HYBRID_METHOD
+
     def __init__(self, func, expr=None):
         """Create a new :class:`.hybrid_method`.
 
@@ -669,12 +697,15 @@ class hybrid_method(object):
         return self
 
 
-class hybrid_property(object):
+class hybrid_property(interfaces._InspectionAttr):
     """A decorator which allows definition of a Python descriptor with both
     instance-level and class-level behavior.
 
     """
 
+    is_attribute = True
+    extension_type = HYBRID_PROPERTY
+
     def __init__(self, fget, fset=None, fdel=None, expr=None):
         """Create a new :class:`.hybrid_property`.
 
index d2f20e94d6515f55d34c70bf2e6f7c2e1e8e482f..c3a297119ab0b16c2765372ed24086427800e411 100644 (file)
@@ -120,7 +120,23 @@ PASSIVE_ONLY_PERSISTENT = util.symbol("PASSIVE_ONLY_PERSISTENT",
 class QueryableAttribute(interfaces._MappedAttribute,
                             interfaces._InspectionAttr,
                             interfaces.PropComparator):
-    """Base class for class-bound attributes. """
+    """Base class for :term:`descriptor` objects that intercept
+    attribute events on behalf of a :class:`.MapperProperty`
+    object.  The actual :class:`.MapperProperty` is accessible
+    via the :attr:`.QueryableAttribute.property`
+    attribute.
+
+
+    .. seealso::
+
+        :class:`.InstrumentedAttribute`
+
+        :class:`.MapperProperty`
+
+        :attr:`.Mapper.all_orm_descriptors`
+
+        :attr:`.Mapper.attrs`
+    """
 
     is_attribute = True
 
@@ -231,7 +247,13 @@ inspection._self_inspects(QueryableAttribute)
 
 
 class InstrumentedAttribute(QueryableAttribute):
-    """Class bound instrumented attribute which adds descriptor methods."""
+    """Class bound instrumented attribute which adds basic
+    :term:`descriptor` methods.
+
+    See :class:`.QueryableAttribute` for a description of most features.
+
+
+    """
 
     def __set__(self, instance, value):
         self.impl.set(instance_state(instance),
index 5a4fc20934e71f774d60e8fb63b341610c2ac0a4..cfd5b600c85cf4fd5b4ed738e82c9495f7a7f548 100644 (file)
@@ -29,7 +29,7 @@ alternate instrumentation forms.
 """
 
 
-from . import exc, collections, events
+from . import exc, collections, events, interfaces
 from operator import attrgetter
 from .. import event, util
 state = util.importlater("sqlalchemy.orm", "state")
@@ -83,6 +83,24 @@ class ClassManager(dict):
         # raises unless self.mapper has been assigned
         raise exc.UnmappedClassError(self.class_)
 
+    def _all_sqla_attributes(self, exclude=None):
+        """return an iterator of all classbound attributes that are
+        implement :class:`._InspectionAttr`.
+
+        This includes :class:`.QueryableAttribute` as well as extension
+        types such as :class:`.hybrid_property` and :class:`.AssociationProxy`.
+
+        """
+        if exclude is None:
+            exclude = set()
+        for supercls in self.class_.__mro__:
+            for key in set(supercls.__dict__).difference(exclude):
+                exclude.add(key)
+                val = supercls.__dict__[key]
+                if isinstance(val, interfaces._InspectionAttr):
+                    yield key, val
+
+
     def _attr_has_impl(self, key):
         """Return True if the given attribute is fully initialized.
 
index 55a980b2e744bb0975c31619fe4084b72733ac5c..654bc40cf2dd0dca19a906e3ee4f55d6afb9120f 100644 (file)
@@ -53,18 +53,79 @@ from .deprecated_interfaces import AttributeExtension, \
     MapperExtension
 
 
+NOT_EXTENSION = util.symbol('NOT_EXTENSION')
+"""Symbol indicating an :class:`_InspectionAttr` that's
+   not part of sqlalchemy.ext.
+
+   Is assigned to the :attr:`._InspectionAttr.extension_type`
+   attibute.
+
+"""
+
 class _InspectionAttr(object):
-    """Define a series of attributes that all ORM inspection
-    targets need to have."""
+    """A base class applied to all ORM objects that can be returned
+    by the :func:`.inspect` function.
+
+    The attributes defined here allow the usage of simple boolean
+    checks to test basic facts about the object returned.
+
+    While the boolean checks here are basically the same as using
+    the Python isinstance() function, the flags here can be used without
+    the need to import all of these classes, and also such that
+    the SQLAlchemy class system can change while leaving the flags
+    here intact for forwards-compatibility.
+
+    """
 
     is_selectable = False
+    """Return True if this object is an instance of :class:`.Selectable`."""
+
     is_aliased_class = False
+    """True if this object is an instance of :class:`.AliasedClass`."""
+
     is_instance = False
+    """True if this object is an instance of :class:`.InstanceState`."""
+
     is_mapper = False
+    """True if this object is an instance of :class:`.Mapper`."""
+
     is_property = False
+    """True if this object is an instance of :class:`.MapperProperty`."""
+
     is_attribute = False
+    """True if this object is a Python :term:`descriptor`.
+
+    This can refer to one of many types.   Usually a
+    :class:`.QueryableAttribute` which handles attributes events on behalf
+    of a :class:`.MapperProperty`.   But can also be an extension type
+    such as :class:`.AssociationProxy` or :class:`.hybrid_property`.
+    The :attr:`._InspectionAttr.extension_type` will refer to a constant
+    identifying the specific subtype.
+
+    .. seealso::
+
+        :attr:`.Mapper.all_orm_descriptors`
+
+    """
+
     is_clause_element = False
+    """True if this object is an instance of :class:`.ClauseElement`."""
+
+    extension_type = NOT_EXTENSION
+    """The extension type, if any.
+    Defaults to :data:`.interfaces.NOT_EXTENSION`
 
+    .. versionadded:: 0.8.0
+
+    .. seealso::
+
+        :data:`.HYBRID_METHOD`
+
+        :data:`.HYBRID_PROPERTY`
+
+        :data:`.ASSOCIATION_PROXY`
+
+    """
 
 class _MappedAttribute(object):
     """Mixin for attributes which should be replaced by mapper-assigned
index 626105b5e14c8fb018aef0696668b82ec552e433..6d8fa4dbbfc91c2e44ebd50946102a130c40442d 100644 (file)
@@ -1502,12 +1502,49 @@ class Mapper(_InspectionAttr):
         returned, inclding :attr:`.synonyms`, :attr:`.column_attrs`,
         :attr:`.relationships`, and :attr:`.composites`.
 
+        .. seealso::
+
+            :attr:`.Mapper.all_orm_descriptors`
 
         """
         if _new_mappers:
             configure_mappers()
         return util.ImmutableProperties(self._props)
 
+    @util.memoized_property
+    def all_orm_descriptors(self):
+        """A namespace of all :class:`._InspectionAttr` attributes associated
+        with the mapped class.
+
+        These attributes are in all cases Python :term:`descriptors` associated
+        with the mapped class or its superclasses.
+
+        This namespace includes attributes that are mapped to the class
+        as well as attributes declared by extension modules.
+        It includes any Python descriptor type that inherits from
+        :class:`._InspectionAttr`.  This includes :class:`.QueryableAttribute`,
+        as well as extension types such as :class:`.hybrid_property`,
+        :class:`.hybrid_method` and :class:`.AssociationProxy`.
+
+        To distinguish between mapped attributes and extension attributes,
+        the attribute :attr:`._InspectionAttr.extension_type` will refer
+        to a constant that distinguishes between different extension types.
+
+        When dealing with a :class:`.QueryableAttribute`, the
+        :attr:`.QueryableAttribute.property` attribute refers to the
+        :class:`.MapperProperty` property, which is what you get when referring
+        to the collection of mapped properties via :attr:`.Mapper.attrs`.
+
+        .. versionadded:: 0.8.0
+
+        .. seealso::
+
+            :attr:`.Mapper.attrs`
+
+        """
+        return util.ImmutableProperties(
+                            dict(self.class_manager._all_sqla_attributes()))
+
     @_memoized_configured_property
     def synonyms(self):
         """Return a namespace of all :class:`.SynonymProperty`
index fdf675183a6d8920d7cdf846a1f2c6398a790b7d..2a401f91d7f49cc45eb38e90e2ceea35e120e631 100644 (file)
@@ -159,7 +159,7 @@ class TestORMInspection(_fixtures.FixtureTest):
         )
         is_(syn.name_syn, User.name_syn.original_property)
         eq_(dict(syn), {
-            "name_syn":User.name_syn.original_property
+            "name_syn": User.name_syn.original_property
         })
 
     def test_relationship_filter(self):
@@ -237,6 +237,70 @@ class TestORMInspection(_fixtures.FixtureTest):
         assert hasattr(prop, 'expression')
 
 
+    def test_extension_types(self):
+        from sqlalchemy.ext.associationproxy import \
+                                        association_proxy, ASSOCIATION_PROXY
+        from sqlalchemy.ext.hybrid import hybrid_property, hybrid_method, \
+                                        HYBRID_PROPERTY, HYBRID_METHOD
+        from sqlalchemy import Table, MetaData, Integer, Column
+        from sqlalchemy.orm import mapper
+        from sqlalchemy.orm.interfaces import NOT_EXTENSION
+
+        class SomeClass(self.classes.User):
+            some_assoc = association_proxy('addresses', 'email_address')
+
+            @hybrid_property
+            def upper_name(self):
+                raise NotImplementedError()
+
+            @hybrid_method
+            def conv(self, fn):
+                raise NotImplementedError()
+
+        class SomeSubClass(SomeClass):
+            @hybrid_property
+            def upper_name(self):
+                raise NotImplementedError()
+
+            @hybrid_property
+            def foo(self):
+                raise NotImplementedError()
+
+        t = Table('sometable', MetaData(),
+                        Column('id', Integer, primary_key=True))
+        mapper(SomeClass, t)
+        mapper(SomeSubClass, inherits=SomeClass)
+
+        insp = inspect(SomeSubClass)
+        eq_(
+            dict((k, v.extension_type)
+                for k, v in insp.all_orm_descriptors.items()
+            ),
+            {
+                'id': NOT_EXTENSION,
+                'name': NOT_EXTENSION,
+                'name_syn': NOT_EXTENSION,
+                'addresses': NOT_EXTENSION,
+                'orders': NOT_EXTENSION,
+                'upper_name': HYBRID_PROPERTY,
+                'foo': HYBRID_PROPERTY,
+                'conv': HYBRID_METHOD,
+                'some_assoc': ASSOCIATION_PROXY
+            }
+        )
+        is_(
+            insp.all_orm_descriptors.upper_name,
+            SomeSubClass.__dict__['upper_name']
+        )
+        is_(
+            insp.all_orm_descriptors.some_assoc,
+            SomeClass.some_assoc
+        )
+        is_(
+            inspect(SomeClass).all_orm_descriptors.upper_name,
+            SomeClass.__dict__['upper_name']
+        )
+
     def test_instance_state(self):
         User = self.classes.User
         u1 = User()