]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
Unwrap Proxy objects when scanning declared_attr
authorMike Bayer <mike_mp@zzzcomputing.com>
Thu, 23 Aug 2018 16:40:26 +0000 (12:40 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Thu, 23 Aug 2018 16:47:08 +0000 (12:47 -0400)
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`.

Fixes: #4326
Change-Id: I582d03f05c3768b3344f93e3791240e9e69b9d1e

doc/build/changelog/unreleased_12/4326.rst [new file with mode: 0644]
lib/sqlalchemy/ext/declarative/base.py
lib/sqlalchemy/orm/attributes.py
lib/sqlalchemy/orm/base.py
lib/sqlalchemy/orm/descriptor_props.py
test/ext/declarative/test_mixin.py

diff --git a/doc/build/changelog/unreleased_12/4326.rst b/doc/build/changelog/unreleased_12/4326.rst
new file mode 100644 (file)
index 0000000..12fef5b
--- /dev/null
@@ -0,0 +1,11 @@
+.. 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`.
+
index 818b92c98a763750b3e72a280b61db119d64df60..9e15582d6f7c5a4935427b8ef52e39fd392a5328 100644 (file)
@@ -11,7 +11,7 @@ from ...orm import mapper, class_mapper, synonym
 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
@@ -287,8 +287,18 @@ class _MapperConfig(object):
                                 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__
index 0bbe70655ef1faa5dc2bdfe3ceaf54257746fbe0..745032e447542c8237eeda65a266ab0eb587a245 100644 (file)
@@ -271,6 +271,8 @@ def create_proxied_attribute(descriptor):
             self._adapt_to_entity = adapt_to_entity
             self.__doc__ = doc
 
+        _is_internal_proxy = True
+
         @property
         def property(self):
             return self.comparator.property
index 8c9d562695114ec73cfb16b6268bf525601aa0c2..e06e1fc78a61c6514d2eef35cac639fa98a76130 100644 (file)
@@ -475,6 +475,13 @@ class InspectionAttr(object):
 
     """
 
+    _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`."""
 
index a8db3fe1c0ed41d7b63f138262445eadcb3930eb..aaf53b69812bd4f01c82d735d840abe78f61ab24 100644 (file)
@@ -500,6 +500,7 @@ class ConcreteInheritedProperty(DescriptorProperty):
                                  (self.parent, self.key, self.parent))
 
         class NoninheritedConcreteProp(object):
+
             def __set__(s, obj, value):
                 warn()
 
index 07c790dc426320a7e08287ab43193cba18fbd24c..6f95bed60f4aed97f658e5f6fe01da3ad4d8e629 100644 (file)
@@ -1325,6 +1325,52 @@ class DeclarativeMixinPropertyTest(DeclarativeTestBase):
         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):