diff options
| author | Diana Clarke <diana.joan.clarke@gmail.com> | 2017-03-16 16:05:18 -0400 |
|---|---|---|
| committer | Mike Bayer <mike_mp@zzzcomputing.com> | 2017-03-21 15:42:42 -0400 |
| commit | caeb274e287f514a50524fc9fe4aeedcb3740147 (patch) | |
| tree | 0668af2a6bbe4dce44ee4ea1c611a5f6ed856b31 /lib/sqlalchemy | |
| parent | f881dae8179b94f72ab0dc85d8f62be8c9ce2fe0 (diff) | |
| download | sqlalchemy-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.py | 213 |
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): |
