--- /dev/null
+.. change::
+ :tags: bug, orm, declarative
+ :tickets: 4326
+
+ Fixed bug where the declarative scan for attributes would receive the
+ expression proxy delivered by a hybrid attribute at the class level, and
+ not the hybrid attribute itself, when receiving the descriptor via the
+ ``@declared_attr`` callable on a subclass of an already-mapped class. This
+ would lead to an attribute that did not report itself as a hybrid when
+ viewed within :attr:`.Mapper.all_orm_descriptors`.
+
from ...orm.interfaces import MapperProperty
from ...orm.properties import ColumnProperty, CompositeProperty
from ...orm.attributes import QueryableAttribute
-from ...orm.base import _is_mapped_class
+from ...orm.base import _is_mapped_class, InspectionAttr
from ... import util, exc
from ...util import topological
from ...sql import expression
util.warn_deprecated(
"Use of sqlalchemy.util.classproperty on "
"declarative classes is deprecated.")
- dict_[name] = column_copies[obj] = \
- ret = getattr(cls, name)
+ # access attribute using normal class access
+ ret = getattr(cls, name)
+
+ # correct for proxies created from hybrid_property
+ # or similar. note there is no known case that
+ # produces nested proxies, so we are only
+ # looking one level deep right now.
+ if isinstance(ret, InspectionAttr) and \
+ ret._is_internal_proxy:
+ ret = ret.descriptor
+
+ dict_[name] = column_copies[obj] = ret
if isinstance(ret, (Column, MapperProperty)) and \
ret.doc is None:
ret.doc = obj.__doc__
self._adapt_to_entity = adapt_to_entity
self.__doc__ = doc
+ _is_internal_proxy = True
+
@property
def property(self):
return self.comparator.property
"""
+ _is_internal_proxy = False
+ """True if this object is an internal proxy object.
+
+ .. versionadded:: 1.2.12
+
+ """
+
is_clause_element = False
"""True if this object is an instance of :class:`.ClauseElement`."""
(self.parent, self.key, self.parent))
class NoninheritedConcreteProp(object):
+
def __set__(s, obj, value):
warn()
eq_(MyModel.type_.__doc__, """this is a document.""")
eq_(MyModel.t2.__doc__, """this is another document.""")
+ def test_correct_for_proxies(self):
+ from sqlalchemy.ext.hybrid import hybrid_property
+ from sqlalchemy.ext import hybrid
+ from sqlalchemy import inspect
+
+ class Mixin(object):
+ @hybrid_property
+ def hp1(cls):
+ return 42
+
+ @declared_attr
+ def hp2(cls):
+ @hybrid_property
+ def hp2(self):
+ return 42
+
+ return hp2
+
+ class Base(declarative_base(), Mixin):
+ __tablename__ = 'test'
+ id = Column(String, primary_key=True)
+
+ class Derived(Base):
+ pass
+
+ # in all cases we get a proxy when we use class-bound access
+ # for the hybrid
+ assert Base.hp1._is_internal_proxy
+ assert Base.hp2._is_internal_proxy
+ assert Derived.hp1._is_internal_proxy
+ assert Derived.hp2._is_internal_proxy
+
+ # however when declarative sets it up, it checks for this proxy
+ # and adjusts
+ b1 = inspect(Base)
+ d1 = inspect(Derived)
+ is_(
+ b1.all_orm_descriptors['hp1'],
+ d1.all_orm_descriptors['hp1'],
+ )
+
+ is_(
+ b1.all_orm_descriptors['hp2'],
+ d1.all_orm_descriptors['hp2'],
+ )
+
def test_column_in_mapper_args(self):
class MyMixin(object):