Commits

Mike Bayer committed 58aed2c Merge

merge default

  • Participants
  • Parent commits 77e2264, c886a39
  • Branches rel_0_8

Comments (0)

Files changed (15)

doc/build/changelog/changelog_08.rst

 ==============
 
 .. 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
+        :pullreq: 33
+
+      GAE dialect now accepts username/password arguments in the URL,
+      courtesy Owen Nelson.
+
+    .. change::
+        :tags: mysql, bug
+        :pullreq: 33
+
+      GAE dialect won't fail on None match if the error code can't be extracted
+      from the exception throw; courtesy Owen Nelson.
 
     .. change::
         :tags: orm, bug
       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

doc/build/core/types.rst

 database's dialect module. See the :ref:`sqlalchemy.dialects_toplevel`
 reference for the database you're interested in.
 
-For example, MySQL has a ``BIGINTEGER`` type and PostgreSQL has an
+For example, MySQL has a ``BIGINT`` type and PostgreSQL has an
 ``INET`` type.  To use these, import them from the module explicitly::
 
     from sqlalchemy.dialects import mysql
 
-    table = Table('foo', meta,
-        Column('id', mysql.BIGINTEGER),
+    table = Table('foo', metadata,
+        Column('id', mysql.BIGINT),
         Column('enumerates', mysql.ENUM('a', 'b', 'c'))
     )
 
 
     from sqlalchemy.dialects import postgresql
 
-    table = Table('foo', meta,
+    table = Table('foo', metadata,
         Column('ipaddress', postgresql.INET),
-        Column('elements', postgresql.ARRAY(str))
-        )
+        Column('elements', postgresql.ARRAY(String))
+    )
 
 Each dialect provides the full set of typenames supported by
 that backend within its `__all__` collection, so that a simple

doc/build/orm/extensions/associationproxy.rst

 
 ``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.
 
 
 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.
 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::
 
         # ...
 
         # 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-
 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
         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")
                 )
 
         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')]
 
 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::
 
 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
 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
         # 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)
                     )
         # 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"
                         )
 
 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
         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)
                     )
     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"
                 )
         # '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')
 
 
 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
 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
 .. autoclass:: AssociationProxy
    :members:
    :undoc-members:
+
+.. autodata:: ASSOCIATION_PROXY

doc/build/orm/extensions/hybrid.rst

 
 .. autoclass:: hybrid_method
     :members:
+
 .. autoclass:: hybrid_property
     :members:
+
 .. autoclass:: Comparator
     :show-inheritance:
+
+.. autodata:: HYBRID_METHOD
+
+.. autodata:: HYBRID_PROPERTY

doc/build/orm/inheritance.rst

             'polymorphic_identity':'engineer',
         }
 
-    class Manager(Person):
+    class Manager(Employee):
         __tablename__ = 'manager'
         id = Column(Integer, ForeignKey('employee.id'), primary_key=True)
         manager_name = Column(String(30))

doc/build/orm/internals.rst

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

doc/build/orm/tutorial.rst

 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

lib/sqlalchemy/dialects/mysql/gaerdbms.py

         return NullPool
 
     def create_connect_args(self, url):
-        return [[], {'database': url.database}]
+        return [[], {'database': url.database,
+                     'user': url.username,
+                     'password': url.password}]
 
     def _extract_error_code(self, exception):
         match = re.compile(r"^(\d+):").match(str(exception))
-        code = match.group(1)
+        # The rdbms api will wrap then re-raise some types of errors
+        # making this regex return no matches.
+        code = match.group(1) if match else None
         if code:
             return int(code)
 

lib/sqlalchemy/ext/associationproxy.py

 import operator
 import weakref
 from .. import exc, orm, util
-from ..orm import collections
+from ..orm import collections, interfaces
 from ..sql import not_
 
 
     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):

lib/sqlalchemy/ext/hybrid.py

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

lib/sqlalchemy/orm/attributes.py

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

lib/sqlalchemy/orm/instrumentation.py

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

lib/sqlalchemy/orm/interfaces.py

     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

lib/sqlalchemy/orm/mapper.py

         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`

test/orm/test_inspect.py

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