summaryrefslogtreecommitdiff
path: root/lib/sqlalchemy
diff options
context:
space:
mode:
authorDiana Clarke <diana.joan.clarke@gmail.com>2017-03-16 16:05:18 -0400
committerMike Bayer <mike_mp@zzzcomputing.com>2017-03-21 15:42:42 -0400
commitcaeb274e287f514a50524fc9fe4aeedcb3740147 (patch)
tree0668af2a6bbe4dce44ee4ea1c611a5f6ed856b31 /lib/sqlalchemy
parentf881dae8179b94f72ab0dc85d8f62be8c9ce2fe0 (diff)
downloadsqlalchemy-caeb274e287f514a50524fc9fe4aeedcb3740147.tar.gz
Allow reuse of hybrid_property across subclasses
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 matches the behavior of ``@property`` in standard Python. Co-authored-by: Mike Bayer <mike_mp@zzzcomputing.com> Fixes: #3911 Fixes: #3912 Change-Id: Iff033d8ccaae20ded9289cbfa789c376759381f5
Diffstat (limited to 'lib/sqlalchemy')
-rw-r--r--lib/sqlalchemy/ext/hybrid.py213
1 files changed, 192 insertions, 21 deletions
diff --git a/lib/sqlalchemy/ext/hybrid.py b/lib/sqlalchemy/ext/hybrid.py
index 509dd560a..17049d995 100644
--- a/lib/sqlalchemy/ext/hybrid.py
+++ b/lib/sqlalchemy/ext/hybrid.py
@@ -372,6 +372,67 @@ lowercasing can be applied to all comparison operations (i.e. ``eq``,
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
--------------------
@@ -714,7 +775,9 @@ class hybrid_property(interfaces.InspectionAttrInfo):
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::
@@ -734,12 +797,14 @@ class hybrid_property(interfaces.InspectionAttrInfo):
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)
@@ -753,29 +818,96 @@ class hybrid_property(interfaces.InspectionAttrInfo):
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
@@ -784,17 +916,56 @@ class hybrid_property(interfaces.InspectionAttrInfo):
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):