--- /dev/null
+.. change::
+ :tags: bug, regression, orm
+ :tickets: 6215
+
+ Fixed regression where the ORM compilation scheme would assume the function
+ name of a hybrid property would be the same as the attribute name in such a
+ way that an ``AttributeError`` would be raised, when it would attempt to
+ determine the correct name for each element in a result tuple. A similar
+ issue exists in 1.3 but only impacts the names of tuple rows. The fix here
+ adds a check that the hybrid's function name is actually present in the
+ ``__dict__`` of the class or its superclasses before assigning this name;
+ otherwise, the hybrid is considered to be "unnamed" and ORM result tuples
+ will use the naming scheme of the underlying expression.
to use either the "fetch" or False synchronization strategy as illustrated
above.
+.. note:: For ORM bulk updates to work with hybrids, the function name
+ of the hybrid must match that of how it is accessed. Something
+ like this wouldn't work::
+
+ class Interval(object):
+ # ...
+
+ def _get(self):
+ return self.end - self.start
+
+ def _set(self, value):
+ self.end = self.start + value
+
+ def _update_expr(cls, value):
+ return [
+ (cls.end, cls.start + value)
+ ]
+
+ length = hybrid_property(
+ fget=_get, fset=_set, update_expr=_update_expr
+ )
+
+ The Python descriptor protocol does not provide any reliable way for
+ a descriptor to know what attribute name it was accessed as, and
+ the UPDATE scheme currently relies upon being able to access the
+ attribute from an instance by name in order to perform the instance
+ synchronization step.
+
.. versionadded:: 1.2 added support for bulk updates to hybrid properties.
Working with Relationships
proxy_attr = attributes.create_proxied_attribute(self)
def expr_comparator(owner):
+ # because this is the descriptor protocol, we don't really know
+ # what our attribute name is. so search for it through the
+ # MRO.
+ for lookup in owner.__mro__:
+ if self.__name__ in lookup.__dict__:
+ if lookup.__dict__[self.__name__] is self:
+ name = self.__name__
+ break
+ else:
+ name = attributes.NO_KEY
+
return proxy_attr(
owner,
- self.__name__,
+ name,
self,
comparator(owner),
doc=comparator.__doc__ or self.__doc__,
from ..sql import visitors
+class NoKey(str):
+ pass
+
+
+NO_KEY = NoKey("no name")
+
+
@inspection._self_inspects
class QueryableAttribute(
interfaces._MappedAttribute,
subclass representing a column expression.
"""
+ if self.key is NO_KEY:
+ annotations = {"entity_namespace": self._entity_namespace}
+ else:
+ annotations = {
+ "proxy_key": self.key,
+ "entity_namespace": self._entity_namespace,
+ }
- return self.comparator.__clause_element__()._annotate(
- {"proxy_key": self.key, "entity_namespace": self._entity_namespace}
- )
+ return self.comparator.__clause_element__()._annotate(annotations)
@property
def _entity_namespace(self):
from sqlalchemy import inspect
from sqlalchemy import Integer
from sqlalchemy import Numeric
+from sqlalchemy import select
from sqlalchemy import String
from sqlalchemy.ext import hybrid
from sqlalchemy.ext.declarative import declarative_base
A = self._fixture()
eq_(A.value.__doc__, "This is a docstring")
+ def test_no_name_one(self):
+ """test :ticket:`6215`"""
+
+ Base = declarative_base()
+
+ class A(Base):
+ __tablename__ = "a"
+ id = Column(Integer, primary_key=True)
+ name = Column(String(50))
+
+ @hybrid.hybrid_property
+ def same_name(self):
+ return self.id
+
+ def name1(self):
+ return self.id
+
+ different_name = hybrid.hybrid_property(name1)
+
+ no_name = hybrid.hybrid_property(lambda self: self.name)
+
+ stmt = select(A.same_name, A.different_name, A.no_name)
+ compiled = stmt.compile()
+
+ eq_(
+ [ent._label_name for ent in compiled.compile_state._entities],
+ ["same_name", "id", "name"],
+ )
+
+ def test_no_name_two(self):
+ """test :ticket:`6215`"""
+ Base = declarative_base()
+
+ class SomeMixin(object):
+ @hybrid.hybrid_property
+ def same_name(self):
+ return self.id
+
+ def name1(self):
+ return self.id
+
+ different_name = hybrid.hybrid_property(name1)
+
+ no_name = hybrid.hybrid_property(lambda self: self.name)
+
+ class A(SomeMixin, Base):
+ __tablename__ = "a"
+ id = Column(Integer, primary_key=True)
+ name = Column(String(50))
+
+ stmt = select(A.same_name, A.different_name, A.no_name)
+ compiled = stmt.compile()
+
+ eq_(
+ [ent._label_name for ent in compiled.compile_state._entities],
+ ["same_name", "id", "name"],
+ )
+
class PropertyExpressionTest(fixtures.TestBase, AssertsCompiledSQL):
__dialect__ = "default"