diff options
-rw-r--r-- | doc/build/changelog/changelog_08.rst | 17 | ||||
-rw-r--r-- | doc/build/orm/extensions/associationproxy.rst | 88 | ||||
-rw-r--r-- | doc/build/orm/extensions/hybrid.rst | 6 | ||||
-rw-r--r-- | doc/build/orm/internals.rst | 10 | ||||
-rw-r--r-- | doc/build/orm/tutorial.rst | 2 | ||||
-rw-r--r-- | lib/sqlalchemy/ext/associationproxy.py | 17 | ||||
-rw-r--r-- | lib/sqlalchemy/ext/hybrid.py | 35 | ||||
-rw-r--r-- | lib/sqlalchemy/orm/attributes.py | 26 | ||||
-rw-r--r-- | lib/sqlalchemy/orm/instrumentation.py | 20 | ||||
-rw-r--r-- | lib/sqlalchemy/orm/interfaces.py | 65 | ||||
-rw-r--r-- | lib/sqlalchemy/orm/mapper.py | 37 | ||||
-rw-r--r-- | test/orm/test_inspect.py | 66 |
12 files changed, 331 insertions, 58 deletions
diff --git a/doc/build/changelog/changelog_08.rst b/doc/build/changelog/changelog_08.rst index b350162a4..6c584e4b4 100644 --- a/doc/build/changelog/changelog_08.rst +++ b/doc/build/changelog/changelog_08.rst @@ -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 @@ -30,6 +39,10 @@ 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 diff --git a/doc/build/orm/extensions/associationproxy.rst b/doc/build/orm/extensions/associationproxy.rst index 03eafda8a..90bb29ebf 100644 --- a/doc/build/orm/extensions/associationproxy.rst +++ b/doc/build/orm/extensions/associationproxy.rst @@ -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 diff --git a/doc/build/orm/extensions/hybrid.rst b/doc/build/orm/extensions/hybrid.rst index 7c6b5f7eb..3ee76fd9b 100644 --- a/doc/build/orm/extensions/hybrid.rst +++ b/doc/build/orm/extensions/hybrid.rst @@ -10,7 +10,13 @@ API Reference .. autoclass:: hybrid_method :members: + .. autoclass:: hybrid_property :members: + .. autoclass:: Comparator :show-inheritance: + +.. autodata:: HYBRID_METHOD + +.. autodata:: HYBRID_PROPERTY diff --git a/doc/build/orm/internals.rst b/doc/build/orm/internals.rst index d699f251c..38efdb08a 100644 --- a/doc/build/orm/internals.rst +++ b/doc/build/orm/internals.rst @@ -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: diff --git a/doc/build/orm/tutorial.rst b/doc/build/orm/tutorial.rst index fadad8551..927914ae7 100644 --- a/doc/build/orm/tutorial.rst +++ b/doc/build/orm/tutorial.rst @@ -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 diff --git a/lib/sqlalchemy/ext/associationproxy.py b/lib/sqlalchemy/ext/associationproxy.py index f6c0764e4..793a5fde9 100644 --- a/lib/sqlalchemy/ext/associationproxy.py +++ b/lib/sqlalchemy/ext/associationproxy.py @@ -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): diff --git a/lib/sqlalchemy/ext/hybrid.py b/lib/sqlalchemy/ext/hybrid.py index 047b2ff95..b274aa766 100644 --- a/lib/sqlalchemy/ext/hybrid.py +++ b/lib/sqlalchemy/ext/hybrid.py @@ -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`. diff --git a/lib/sqlalchemy/orm/attributes.py b/lib/sqlalchemy/orm/attributes.py index d2f20e94d..c3a297119 100644 --- a/lib/sqlalchemy/orm/attributes.py +++ b/lib/sqlalchemy/orm/attributes.py @@ -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), diff --git a/lib/sqlalchemy/orm/instrumentation.py b/lib/sqlalchemy/orm/instrumentation.py index 5a4fc2093..cfd5b600c 100644 --- a/lib/sqlalchemy/orm/instrumentation.py +++ b/lib/sqlalchemy/orm/instrumentation.py @@ -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. diff --git a/lib/sqlalchemy/orm/interfaces.py b/lib/sqlalchemy/orm/interfaces.py index 55a980b2e..654bc40cf 100644 --- a/lib/sqlalchemy/orm/interfaces.py +++ b/lib/sqlalchemy/orm/interfaces.py @@ -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 diff --git a/lib/sqlalchemy/orm/mapper.py b/lib/sqlalchemy/orm/mapper.py index 626105b5e..6d8fa4dbb 100644 --- a/lib/sqlalchemy/orm/mapper.py +++ b/lib/sqlalchemy/orm/mapper.py @@ -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` diff --git a/test/orm/test_inspect.py b/test/orm/test_inspect.py index fdf675183..2a401f91d 100644 --- a/test/orm/test_inspect.py +++ b/test/orm/test_inspect.py @@ -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() |