:ticket:`3896`
+.. _change_3911_3912:
+
+Hybrid attributes support reuse among subclasses, redefinition of @getter
+-------------------------------------------------------------------------
+
+The :class:`sqlalchemy.ext.hybrid.hybrid_property` class now supports
+calling mutators like ``@setter``, ``@expression`` etc. multiple times
+across subclasses, and now provides a ``@getter`` mutator, so that
+a particular hybrid can be repurposed across subclasses or other
+classes. This now is similar to the behavior of ``@property`` in standard
+Python::
+
+ class FirstNameOnly(Base):
+ # ...
+
+ first_name = Column(String)
+
+ @hybrid_property
+ def name(self):
+ return self.first_name
+
+ @name.setter
+ def name(self, value):
+ self.first_name = value
+
+ class FirstNameLastName(FirstNameOnly):
+ # ...
+
+ last_name = Column(String)
+
+ @FirstNameOnly.name.getter
+ def name(self):
+ return self.first_name + ' ' + self.last_name
+
+ @name.setter
+ def name(self, value):
+ self.first_name, self.last_name = value.split(' ', maxsplit=1)
+
+ @name.expression
+ def name(cls):
+ return func.concat(cls.first_name, ' ', cls.last_name)
+
+Above, the ``FirstNameOnly.name`` hybrid is referenced by the
+``FirstNameLastName`` subclass in order to repurpose it specifically to the
+new subclass. This is achieved by copying the hybrid object to a new one
+within each call to ``@getter``, ``@setter``, as well as in all other
+mutator methods like ``@expression``, leaving the previous hybrid's definition
+intact. Previously, methods like ``@setter`` would modify the existing
+hybrid in-place, interfering with the definition on the superclass.
+
+.. note:: Be sure to read the documentation at :ref:`hybrid_reuse_subclass`
+ for important notes regarding how to override
+ :meth:`.hybrid_property.expression`
+ and :meth:`.hybrid_property.comparator`, as a special qualifier
+ :attr:`.hybrid_property.overrides` may be necessary to avoid name
+ conflicts with :class:`.QueryableAttribute` in some cases.
+
+:ticket:`3911`
+
+:ticket:`3912`
+
New Features and Improvements - Core
====================================
def operate(self, op, other):
return op(func.lower(self.__clause_element__()), func.lower(other))
+.. _hybrid_reuse_subclass:
+
+Reusing Hybrid Properties across Subclasses
+-------------------------------------------
+
+A hybrid can be referred to from a superclass, to allow modifying
+methods like :meth:`.hybrid_property.getter`, :meth:`.hybrid_property.setter`
+to be used to redefine those methods on a subclass. This is similar to
+how the standard Python ``@property`` object works::
+
+ class FirstNameOnly(Base):
+ # ...
+
+ first_name = Column(String)
+
+ @hybrid_property
+ def name(self):
+ return self.first_name
+
+ @name.setter
+ def name(self, value):
+ self.first_name = value
+
+ class FirstNameLastName(FirstNameOnly):
+ # ...
+
+ last_name = Column(String)
+
+ @FirstNameOnly.name.getter
+ def name(self):
+ return self.first_name + ' ' + self.last_name
+
+ @name.setter
+ def name(self, value):
+ self.first_name, self.last_name = value.split(' ', 1)
+
+Above, the ``FirstNameLastName`` class refers to the hybrid from
+``FirstNameOnly.name`` to repurpose its getter and setter for the subclass.
+
+When overriding :meth:`.hybrid_property.expression` and
+:meth:`.hybrid_property.comparator` alone as the first reference
+to the superclass, these names conflict
+with the same-named accessors on the class-level :class:`.QueryableAttribute`
+object returned at the class level. To override these methods when
+referring directly to the parent class descriptor, add
+the special qualifier :attr:`.hybrid_property.overrides`, which will
+de-reference the instrumented attribute back to the hybrid object::
+
+ class FirstNameLastName(FirstNameOnly):
+ # ...
+
+ last_name = Column(String)
+
+ @FirstNameOnly.overrides.expression
+ def name(cls):
+ return func.concat(cls.first_name, ' ', cls.last_name)
+
+.. versionadded:: 1.2 Added :meth:`.hybrid_property.getter` as well as the
+ ability to redefine accessors per-subclass.
+
+
Hybrid Value Objects
--------------------
is_attribute = True
extension_type = HYBRID_PROPERTY
- def __init__(self, fget, fset=None, fdel=None, expr=None):
+ def __init__(
+ self, fget, fset=None, fdel=None,
+ expr=None, custom_comparator=None):
"""Create a new :class:`.hybrid_property`.
Usage is typically via decorator::
self.fget = fget
self.fset = fset
self.fdel = fdel
- self.expression(expr or fget)
+ self.expr = expr
+ self.custom_comparator = custom_comparator
+
util.update_wrapper(self, fget)
def __get__(self, instance, owner):
if instance is None:
- return self.expr(owner)
+ return self._expr_comparator(owner)
else:
return self.fget(instance)
raise AttributeError("can't delete attribute")
self.fdel(instance)
- def setter(self, fset):
- """Provide a modifying decorator that defines a value-setter method."""
+ def _copy(self, **kw):
+ defaults = {
+ key: value
+ for key, value in self.__dict__.items()
+ if not key.startswith("_")}
+ defaults.update(**kw)
+ return type(self)(**defaults)
- self.fset = fset
+ @property
+ def overrides(self):
+ """Prefix for a method that is overriding an existing attribute.
+
+ The :attr:`.hybrid_property.overrides` accessor just returns
+ this hybrid object, which when called at the class level from
+ a parent class, will de-reference the "instrumented attribute"
+ normally returned at this level, and allow modifying decorators
+ like :meth:`.hybrid_property.expression` and
+ :meth:`.hybrid_property.comparator`
+ to be used without conflicting with the same-named attributes
+ normally present on the :class:`.QueryableAttribute`::
+
+ class SuperClass(object):
+ # ...
+
+ @hybrid_property
+ def foobar(self):
+ return self._foobar
+
+ class SubClass(SuperClass):
+ # ...
+
+ @SuperClass.foobar.overrides.expression
+ def foobar(cls):
+ return func.subfoobar(self._foobar)
+
+ .. versionadded:: 1.2
+
+ .. seealso::
+
+ :ref:`hybrid_reuse_subclass`
+
+ """
return self
+ def getter(self, fget):
+ """Provide a modifying decorator that defines a getter method.
+
+ .. versionadded:: 1.2
+
+ """
+
+ return self._copy(fget=fget)
+
+ def setter(self, fset):
+ """Provide a modifying decorator that defines a setter method."""
+
+ return self._copy(fset=fset)
+
def deleter(self, fdel):
- """Provide a modifying decorator that defines a
- value-deletion method."""
+ """Provide a modifying decorator that defines a deletion method."""
- self.fdel = fdel
- return self
+ return self._copy(fdel=fdel)
def expression(self, expr):
"""Provide a modifying decorator that defines a SQL-expression
- producing method."""
+ producing method.
+
+ When a hybrid is invoked at the class level, the SQL expression given
+ here is wrapped inside of a specialized :class:`.QueryableAttribute`,
+ which is the same kind of object used by the ORM to represent other
+ mapped attributes. The reason for this is so that other class-level
+ attributes such as docstrings and a reference to the hybrid itself may
+ be maintained within the structure that's returned, without any
+ modifications to the original SQL expression passed in.
+
+ .. note::
+
+ when referring to a hybrid property from an owning class (e.g.
+ ``SomeClass.some_hybrid``), an instance of
+ :class:`.QueryableAttribute` is returned, representing the
+ expression or comparator object as well as this hybrid object.
+ However, that object itself has accessors called ``expression`` and
+ ``comparator``; so when attempting to override these decorators on a
+ subclass, it may be necessary to qualify it using the
+ :attr:`.hybrid_property.overrides` modifier first. See that
+ modifier for details.
- def _expr(cls):
- return ExprComparator(expr(cls), self)
- util.update_wrapper(_expr, expr)
+ """
- self.expr = _expr
- return self.comparator(_expr)
+ return self._copy(expr=expr)
def comparator(self, comparator):
"""Provide a modifying decorator that defines a custom
The return value of the decorated method should be an instance of
:class:`~.hybrid.Comparator`.
+ When a hybrid is invoked at the class level, the
+ :class:`~.hybrid.Comparator` object given here is wrapped inside of a
+ specialized :class:`.QueryableAttribute`, which is the same kind of
+ object used by the ORM to represent other mapped attributes. The
+ reason for this is so that other class-level attributes such as
+ docstrings and a reference to the hybrid itself may be maintained
+ within the structure that's returned, without any modifications to the
+ original comparator object passed in.
+
+ .. note::
+
+ when referring to a hybrid property from an owning class (e.g.
+ ``SomeClass.some_hybrid``), an instance of
+ :class:`.QueryableAttribute` is returned, representing the
+ expression or comparator object as this hybrid object. However,
+ that object itself has accessors called ``expression`` and
+ ``comparator``; so when attempting to override these decorators on a
+ subclass, it may be necessary to qualify it using the
+ :attr:`.hybrid_property.overrides` modifier first. See that
+ modifier for details.
+
"""
+ return self._copy(custom_comparator=comparator)
+
+ @util.memoized_property
+ def _expr_comparator(self):
+ if self.custom_comparator is not None:
+ return self._get_comparator(self.custom_comparator)
+ elif self.expr is not None:
+ return self._get_expr(self.expr)
+ else:
+ return self._get_expr(self.fget)
- proxy_attr = attributes.\
- create_proxied_attribute(self)
+ def _get_expr(self, expr):
- def expr(owner):
+ def _expr(cls):
+ return ExprComparator(expr(cls), self)
+ util.update_wrapper(_expr, expr)
+
+ return self._get_comparator(_expr)
+
+ def _get_comparator(self, comparator):
+
+ proxy_attr = attributes.create_proxied_attribute(self)
+
+ def expr_comparator(owner):
return proxy_attr(
owner, self.__name__, self, comparator(owner),
doc=comparator.__doc__ or self.__doc__)
- self.expr = expr
- return self
+ return expr_comparator
class Comparator(interfaces.PropComparator):
return A
+
def _relationship_fixture(self):
Base = declarative_base()
eq_(a1._value, 10)
+class PropertyOverrideTest(fixtures.TestBase, AssertsCompiledSQL):
+ __dialect__ = 'default'
+
+ def _fixture(self):
+ Base = declarative_base()
+
+ class Person(Base):
+ __tablename__ = 'person'
+ id = Column(Integer, primary_key=True)
+ _name = Column(String)
+
+ @hybrid.hybrid_property
+ def name(self):
+ return self._name
+
+ @name.setter
+ def name(self, value):
+ self._name = value.title()
+
+ class OverrideSetter(Person):
+ __tablename__ = 'override_setter'
+ id = Column(Integer, ForeignKey('person.id'), primary_key=True)
+ other = Column(String)
+
+ @Person.name.setter
+ def name(self, value):
+ self._name = value.upper()
+
+ class OverrideGetter(Person):
+ __tablename__ = 'override_getter'
+ id = Column(Integer, ForeignKey('person.id'), primary_key=True)
+ other = Column(String)
+
+ @Person.name.getter
+ def name(self):
+ return "Hello " + self._name
+
+ class OverrideExpr(Person):
+ __tablename__ = 'override_expr'
+ id = Column(Integer, ForeignKey('person.id'), primary_key=True)
+ other = Column(String)
+
+ @Person.name.overrides.expression
+ def name(self):
+ return func.concat("Hello", self._name)
+
+ class FooComparator(hybrid.Comparator):
+ def __clause_element__(self):
+ return func.concat("Hello", self.expression._name)
+
+ class OverrideComparator(Person):
+ __tablename__ = 'override_comp'
+ id = Column(Integer, ForeignKey('person.id'), primary_key=True)
+ other = Column(String)
+
+ @Person.name.overrides.comparator
+ def name(self):
+ return FooComparator(self)
+
+ return (
+ Person, OverrideSetter, OverrideGetter,
+ OverrideExpr, OverrideComparator
+ )
+
+ def test_property(self):
+ Person, _, _, _, _ = self._fixture()
+ p1 = Person()
+ p1.name = 'mike'
+ eq_(p1._name, 'Mike')
+ eq_(p1.name, 'Mike')
+
+ def test_override_setter(self):
+ _, OverrideSetter, _, _, _ = self._fixture()
+ p1 = OverrideSetter()
+ p1.name = 'mike'
+ eq_(p1._name, 'MIKE')
+ eq_(p1.name, 'MIKE')
+
+ def test_override_getter(self):
+ _, _, OverrideGetter, _, _ = self._fixture()
+ p1 = OverrideGetter()
+ p1.name = 'mike'
+ eq_(p1._name, 'Mike')
+ eq_(p1.name, 'Hello Mike')
+
+ def test_override_expr(self):
+ Person, _, _, OverrideExpr, _ = self._fixture()
+
+ self.assert_compile(
+ Person.name.__clause_element__(),
+ "person._name"
+ )
+
+ self.assert_compile(
+ OverrideExpr.name.__clause_element__(),
+ "concat(:concat_1, person._name)"
+ )
+
+ def test_override_comparator(self):
+ Person, _, _, _, OverrideComparator = self._fixture()
+
+ self.assert_compile(
+ Person.name.__clause_element__(),
+ "person._name"
+ )
+
+ self.assert_compile(
+ OverrideComparator.name.__clause_element__(),
+ "concat(:concat_1, person._name)"
+ )
+
+
class PropertyMirrorTest(fixtures.TestBase, AssertsCompiledSQL):
__dialect__ = 'default'