From: Mike Bayer Date: Mon, 18 Apr 2016 20:18:31 +0000 (-0400) Subject: Propagate hybrid properties / info X-Git-Tag: rel_1_1_0b1~67 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=6f6e2c48ba0be827ee434891f54eb2173edf9bfc;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git Propagate hybrid properties / info 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 --- diff --git a/doc/build/changelog/migration_11.rst b/doc/build/changelog/migration_11.rst index be1609130a..fffdc4a9d4 100644 --- a/doc/build/changelog/migration_11.rst +++ b/doc/build/changelog/migration_11.rst @@ -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: diff --git a/lib/sqlalchemy/ext/hybrid.py b/lib/sqlalchemy/ext/hybrid.py index 18516eae3e..2f473fd314 100644 --- a/lib/sqlalchemy/ext/hybrid.py +++ b/lib/sqlalchemy/ext/hybrid.py @@ -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) diff --git a/test/ext/test_hybrid.py b/test/ext/test_hybrid.py index dac65dbab8..c67b66051a 100644 --- a/test/ext/test_hybrid.py +++ b/test/ext/test_hybrid.py @@ -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'