]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
Propagate hybrid properties / info
authorMike Bayer <mike_mp@zzzcomputing.com>
Mon, 18 Apr 2016 20:18:31 +0000 (16:18 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Mon, 18 Apr 2016 20:20:20 +0000 (16:20 -0400)
Keystone and others depend on the .property attribute being
"mirrored" when a @hybrid_property is linked directly to a
mapped attribute.  Restore this linkage and also create a defined
behavior for the .info dictionary; it is that of the hybrid itself.
Add this behavioral change to the migration notes.

Change-Id: I8ac34ef52039387230c648866c5ca15d381f7fee
References: #3653

doc/build/changelog/migration_11.rst
lib/sqlalchemy/ext/hybrid.py
test/ext/test_hybrid.py

index be1609130a684250e1c22e0a07313657f96ac12d..fffdc4a9d46ac64c7079add7b7dea09adf4a0be3 100644 (file)
@@ -542,8 +542,8 @@ for an attribute being replaced.
 
 .. _change_3653:
 
-Hybrid properties and methods now propagate the docstring
----------------------------------------------------------
+Hybrid properties and methods now propagate the docstring as well as .info
+--------------------------------------------------------------------------
 
 A hybrid method or property will now reflect the ``__doc__`` value
 present in the original docstring::
@@ -582,6 +582,22 @@ for elaborate schemes like that of the
 recipe, however we'll be looking to see that no other regressions occur for
 users.
 
+As part of this change, the :attr:`.hybrid_property.info` collection is now
+also propagated from the hybrid descriptor itself, rather than from the underlying
+expression.  That is, accessing ``A.some_name.info`` now returns the same
+dictionary that you'd get from ``inspect(A).all_orm_descriptors['some_name'].info``::
+
+    >>> A.some_name.info['foo'] = 'bar'
+    >>> from sqlalchemy import inspect
+    >>> inspect(A).all_orm_descriptors['some_name'].info
+    {'foo': 'bar'}
+
+Note that this ``.info`` dictionary is **separate** from that of a mapped attribute
+which the hybrid descriptor may be proxying directly; this is a behavioral
+change from 1.0.   The wrapper will still proxy other useful attributes
+of a mirrored attribute such as :attr:`.QueryableAttribute.property` and
+:attr:`.QueryableAttribute.class_`.
+
 :ticket:`3653`
 
 .. _change_3601:
index 18516eae3e1653e2dd612ddadeb38f9d70e5b14a..2f473fd314068367be02779e395b902f2b0d1827 100644 (file)
@@ -771,7 +771,7 @@ class hybrid_property(interfaces.InspectionAttrInfo):
         producing method."""
 
         def _expr(cls):
-            return ExprComparator(expr(cls))
+            return ExprComparator(expr(cls), self)
         util.update_wrapper(_expr, expr)
 
         self.expr = _expr
@@ -819,10 +819,21 @@ class Comparator(interfaces.PropComparator):
 
 
 class ExprComparator(Comparator):
+    def __init__(self, expression, hybrid):
+        self.expression = expression
+        self.hybrid = hybrid
 
     def __getattr__(self, key):
         return getattr(self.expression, key)
 
+    @property
+    def info(self):
+        return self.hybrid.info
+
+    @property
+    def property(self):
+        return self.expression.property
+
     def operate(self, op, *other, **kwargs):
         return op(self.expression, *other, **kwargs)
 
index dac65dbab8dba9fd21ea6ac82f118746ddfb4be5..c67b66051a5f4d68496715a65bc0b93aff7743a5 100644 (file)
@@ -266,6 +266,59 @@ class PropertyValueTest(fixtures.TestBase, AssertsCompiledSQL):
         eq_(a1._value, 10)
 
 
+class PropertyMirrorTest(fixtures.TestBase, AssertsCompiledSQL):
+    __dialect__ = 'default'
+
+    def _fixture(self):
+        Base = declarative_base()
+
+        class A(Base):
+            __tablename__ = 'a'
+            id = Column(Integer, primary_key=True)
+            _value = Column("value", String)
+
+            @hybrid.hybrid_property
+            def value(self):
+                "This is an instance-level docstring"
+                return self._value
+        return A
+
+    def test_property(self):
+        A = self._fixture()
+
+        is_(A.value.property, A._value.property)
+
+    def test_key(self):
+        A = self._fixture()
+        eq_(A.value.key, "value")
+        eq_(A._value.key, "_value")
+
+    def test_class(self):
+        A = self._fixture()
+        is_(A.value.class_, A._value.class_)
+
+    def test_get_history(self):
+        A = self._fixture()
+        inst = A(_value=5)
+        eq_(A.value.get_history(inst), A._value.get_history(inst))
+
+    def test_info_not_mirrored(self):
+        A = self._fixture()
+        A._value.info['foo'] = 'bar'
+        A.value.info['bar'] = 'hoho'
+
+        eq_(A._value.info, {'foo': 'bar'})
+        eq_(A.value.info, {'bar': 'hoho'})
+
+    def test_info_from_hybrid(self):
+        A = self._fixture()
+        A._value.info['foo'] = 'bar'
+        A.value.info['bar'] = 'hoho'
+
+        insp = inspect(A)
+        is_(insp.all_orm_descriptors['value'].info, A.value.info)
+
+
 class MethodExpressionTest(fixtures.TestBase, AssertsCompiledSQL):
     __dialect__ = 'default'