summaryrefslogtreecommitdiff
path: root/lib/sqlalchemy/ext/hybrid.py
diff options
context:
space:
mode:
Diffstat (limited to 'lib/sqlalchemy/ext/hybrid.py')
-rw-r--r--lib/sqlalchemy/ext/hybrid.py218
1 files changed, 125 insertions, 93 deletions
diff --git a/lib/sqlalchemy/ext/hybrid.py b/lib/sqlalchemy/ext/hybrid.py
index 9fb0ee763..32ad6b8f7 100644
--- a/lib/sqlalchemy/ext/hybrid.py
+++ b/lib/sqlalchemy/ext/hybrid.py
@@ -9,10 +9,10 @@
"hybrid" means the attribute has distinct behaviors defined at the
class level and at the instance level.
-The :mod:`~sqlalchemy.ext.hybrid` extension provides a special form of method
-decorator, is around 50 lines of code and has almost no dependencies on the rest
-of SQLAlchemy. It can, in theory, work with any descriptor-based expression
-system.
+The :mod:`~sqlalchemy.ext.hybrid` extension provides a special form of
+method decorator, is around 50 lines of code and has almost no
+dependencies on the rest of SQLAlchemy. It can, in theory, work with
+any descriptor-based expression system.
Consider a mapping ``Interval``, representing integer ``start`` and ``end``
values. We can define higher level functions on mapped classes that produce
@@ -51,9 +51,10 @@ as the class itself::
def intersects(self, other):
return self.contains(other.start) | self.contains(other.end)
-Above, the ``length`` property returns the difference between the ``end`` and
-``start`` attributes. With an instance of ``Interval``, this subtraction occurs
-in Python, using normal Python descriptor mechanics::
+Above, the ``length`` property returns the difference between the
+``end`` and ``start`` attributes. With an instance of ``Interval``,
+this subtraction occurs in Python, using normal Python descriptor
+mechanics::
>>> i1 = Interval(5, 10)
>>> i1.length
@@ -82,11 +83,12 @@ locate attributes, so can also be used with hybrid attributes::
FROM interval
WHERE interval."end" - interval.start = :param_1
-The ``Interval`` class example also illustrates two methods, ``contains()`` and ``intersects()``,
-decorated with :class:`.hybrid_method`.
-This decorator applies the same idea to methods that :class:`.hybrid_property` applies
-to attributes. The methods return boolean values, and take advantage
-of the Python ``|`` and ``&`` bitwise operators to produce equivalent instance-level and
+The ``Interval`` class example also illustrates two methods,
+``contains()`` and ``intersects()``, decorated with
+:class:`.hybrid_method`. This decorator applies the same idea to
+methods that :class:`.hybrid_property` applies to attributes. The
+methods return boolean values, and take advantage of the Python ``|``
+and ``&`` bitwise operators to produce equivalent instance-level and
SQL expression-level boolean behavior::
>>> i1.contains(6)
@@ -118,12 +120,15 @@ SQL expression-level boolean behavior::
Defining Expression Behavior Distinct from Attribute Behavior
--------------------------------------------------------------
-Our usage of the ``&`` and ``|`` bitwise operators above was fortunate, considering
-our functions operated on two boolean values to return a new one. In many cases, the construction
-of an in-Python function and a SQLAlchemy SQL expression have enough differences that two
-separate Python expressions should be defined. The :mod:`~sqlalchemy.ext.hybrid` decorators
-define the :meth:`.hybrid_property.expression` modifier for this purpose. As an example we'll
-define the radius of the interval, which requires the usage of the absolute value function::
+Our usage of the ``&`` and ``|`` bitwise operators above was
+fortunate, considering our functions operated on two boolean values to
+return a new one. In many cases, the construction of an in-Python
+function and a SQLAlchemy SQL expression have enough differences that
+two separate Python expressions should be defined. The
+:mod:`~sqlalchemy.ext.hybrid` decorators define the
+:meth:`.hybrid_property.expression` modifier for this purpose. As an
+example we'll define the radius of the interval, which requires the
+usage of the absolute value function::
from sqlalchemy import func
@@ -138,8 +143,9 @@ define the radius of the interval, which requires the usage of the absolute valu
def radius(cls):
return func.abs(cls.length) / 2
-Above the Python function ``abs()`` is used for instance-level operations, the SQL function
-``ABS()`` is used via the :attr:`.func` object for class-level expressions::
+Above the Python function ``abs()`` is used for instance-level
+operations, the SQL function ``ABS()`` is used via the :attr:`.func`
+object for class-level expressions::
>>> i1.radius
2
@@ -153,8 +159,8 @@ Above the Python function ``abs()`` is used for instance-level operations, the S
Defining Setters
----------------
-Hybrid properties can also define setter methods. If we wanted ``length`` above, when
-set, to modify the endpoint value::
+Hybrid properties can also define setter methods. If we wanted
+``length`` above, when set, to modify the endpoint value::
class Interval(object):
# ...
@@ -179,9 +185,10 @@ The ``length(self, value)`` method is now called upon set::
Working with Relationships
--------------------------
-There's no essential difference when creating hybrids that work with related objects as
-opposed to column-based data. The need for distinct expressions tends to be greater.
-Consider the following declarative mapping which relates a ``User`` to a ``SavingsAccount``::
+There's no essential difference when creating hybrids that work with
+related objects as opposed to column-based data. The need for distinct
+expressions tends to be greater. Consider the following declarative
+mapping which relates a ``User`` to a ``SavingsAccount``::
from sqlalchemy import Column, Integer, ForeignKey, Numeric, String
from sqlalchemy.orm import relationship
@@ -222,27 +229,34 @@ Consider the following declarative mapping which relates a ``User`` to a ``Savin
def balance(cls):
return SavingsAccount.balance
-The above hybrid property ``balance`` works with the first ``SavingsAccount`` entry in the list of
-accounts for this user. The in-Python getter/setter methods can treat ``accounts`` as a Python
+The above hybrid property ``balance`` works with the first
+``SavingsAccount`` entry in the list of accounts for this user. The
+in-Python getter/setter methods can treat ``accounts`` as a Python
list available on ``self``.
-However, at the expression level, we can't travel along relationships to column attributes
-directly since SQLAlchemy is explicit about joins. So here, it's expected that the ``User`` class will be
-used in an appropriate context such that an appropriate join to ``SavingsAccount`` will be present::
+However, at the expression level, we can't travel along relationships
+to column attributes directly since SQLAlchemy is explicit about
+joins. So here, it's expected that the ``User`` class will be used
+in an appropriate context such that an appropriate join to
+``SavingsAccount`` will be present::
- >>> print Session().query(User, User.balance).join(User.accounts).filter(User.balance > 5000)
- SELECT "user".id AS user_id, "user".name AS user_name, account.balance AS account_balance
+ >>> print Session().query(User, User.balance).\
+ ... join(User.accounts).filter(User.balance > 5000)
+ SELECT "user".id AS user_id, "user".name AS user_name,
+ account.balance AS account_balance
FROM "user" JOIN account ON "user".id = account.user_id
WHERE account.balance > :balance_1
-Note however, that while the instance level accessors need to worry about whether ``self.accounts``
-is even present, this issue expresses itself differently at the SQL expression level, where we basically
+Note however, that while the instance level accessors need to worry
+about whether ``self.accounts`` is even present, this issue expresses
+itself differently at the SQL expression level, where we basically
would use an outer join::
>>> from sqlalchemy import or_
>>> print (Session().query(User, User.balance).outerjoin(User.accounts).
... filter(or_(User.balance < 5000, User.balance == None)))
- SELECT "user".id AS user_id, "user".name AS user_name, account.balance AS account_balance
+ SELECT "user".id AS user_id, "user".name AS user_name,
+ account.balance AS account_balance
FROM "user" LEFT OUTER JOIN account ON "user".id = account.user_id
WHERE account.balance < :balance_1 OR account.balance IS NULL
@@ -251,10 +265,11 @@ would use an outer join::
Building Custom Comparators
---------------------------
-The hybrid property also includes a helper that allows construction of custom comparators.
-A comparator object allows one to customize the behavior of each SQLAlchemy expression
-operator individually. They are useful when creating custom types that have
-some highly idiosyncratic behavior on the SQL side.
+The hybrid property also includes a helper that allows construction of
+custom comparators. A comparator object allows one to customize the
+behavior of each SQLAlchemy expression operator individually. They
+are useful when creating custom types that have some highly
+idiosyncratic behavior on the SQL side.
The example class below allows case-insensitive comparisons on the attribute
named ``word_insensitive``::
@@ -291,9 +306,10 @@ SQL function to both sides::
FROM searchword
WHERE lower(searchword.word) = lower(:lower_1)
-The ``CaseInsensitiveComparator`` above implements part of the :class:`.ColumnOperators`
-interface. A "coercion" operation like lowercasing can be applied to all comparison operations
-(i.e. ``eq``, ``lt``, ``gt``, etc.) using :meth:`.Operators.operate`::
+The ``CaseInsensitiveComparator`` above implements part of the
+:class:`.ColumnOperators` interface. A "coercion" operation like
+lowercasing can be applied to all comparison operations (i.e. ``eq``,
+``lt``, ``gt``, etc.) using :meth:`.Operators.operate`::
class CaseInsensitiveComparator(Comparator):
def operate(self, op, other):
@@ -302,17 +318,20 @@ interface. A "coercion" operation like lowercasing can be applied to all compa
Hybrid Value Objects
--------------------
-Note in our previous example, if we were to compare the ``word_insensitive`` attribute of
-a ``SearchWord`` instance to a plain Python string, the plain Python string would not
-be coerced to lower case - the ``CaseInsensitiveComparator`` we built, being returned
-by ``@word_insensitive.comparator``, only applies to the SQL side.
-
-A more comprehensive form of the custom comparator is to construct a *Hybrid Value Object*.
-This technique applies the target value or expression to a value object which is then
-returned by the accessor in all cases. The value object allows control
-of all operations upon the value as well as how compared values are treated, both
-on the SQL expression side as well as the Python value side. Replacing the
-previous ``CaseInsensitiveComparator`` class with a new ``CaseInsensitiveWord`` class::
+Note in our previous example, if we were to compare the
+``word_insensitive`` attribute of a ``SearchWord`` instance to a plain
+Python string, the plain Python string would not be coerced to lower
+case - the ``CaseInsensitiveComparator`` we built, being returned by
+``@word_insensitive.comparator``, only applies to the SQL side.
+
+A more comprehensive form of the custom comparator is to construct a
+*Hybrid Value Object*. This technique applies the target value or
+expression to a value object which is then returned by the accessor in
+all cases. The value object allows control of all operations upon
+the value as well as how compared values are treated, both on the SQL
+expression side as well as the Python value side. Replacing the
+previous ``CaseInsensitiveComparator`` class with a new
+``CaseInsensitiveWord`` class::
class CaseInsensitiveWord(Comparator):
"Hybrid value representing a lower case representation of a word."
@@ -339,12 +358,13 @@ previous ``CaseInsensitiveComparator`` class with a new ``CaseInsensitiveWord``
key = 'word'
"Label to apply to Query tuple results"
-Above, the ``CaseInsensitiveWord`` object represents ``self.word``, which may be a SQL function,
-or may be a Python native. By overriding ``operate()`` and ``__clause_element__()``
-to work in terms of ``self.word``, all comparison operations will work against the
+Above, the ``CaseInsensitiveWord`` object represents ``self.word``,
+which may be a SQL function, or may be a Python native. By
+overriding ``operate()`` and ``__clause_element__()`` to work in terms
+of ``self.word``, all comparison operations will work against the
"converted" form of ``word``, whether it be SQL side or Python side.
-Our ``SearchWord`` class can now deliver the ``CaseInsensitiveWord`` object unconditionally
-from a single hybrid call::
+Our ``SearchWord`` class can now deliver the ``CaseInsensitiveWord``
+object unconditionally from a single hybrid call::
class SearchWord(Base):
__tablename__ = 'searchword'
@@ -355,9 +375,10 @@ from a single hybrid call::
def word_insensitive(self):
return CaseInsensitiveWord(self.word)
-The ``word_insensitive`` attribute now has case-insensitive comparison behavior
-universally, including SQL expression vs. Python expression (note the Python value is
-converted to lower case on the Python side here)::
+The ``word_insensitive`` attribute now has case-insensitive comparison
+behavior universally, including SQL expression vs. Python expression
+(note the Python value is converted to lower case on the Python side
+here)::
>>> print Session().query(SearchWord).filter_by(word_insensitive="Trucks")
SELECT searchword.id AS searchword_id, searchword.word AS searchword_word
@@ -374,7 +395,8 @@ SQL expression versus SQL expression::
... filter(
... sw1.word_insensitive > sw2.word_insensitive
... )
- SELECT lower(searchword_1.word) AS lower_1, lower(searchword_2.word) AS lower_2
+ SELECT lower(searchword_1.word) AS lower_1,
+ lower(searchword_2.word) AS lower_2
FROM searchword AS searchword_1, searchword AS searchword_2
WHERE lower(searchword_1.word) > lower(searchword_2.word)
@@ -388,8 +410,9 @@ Python only expression::
>>> print ws1.word_insensitive
someword
-The Hybrid Value pattern is very useful for any kind of value that may have multiple representations,
-such as timestamps, time deltas, units of measurement, currencies and encrypted passwords.
+The Hybrid Value pattern is very useful for any kind of value that may
+have multiple representations, such as timestamps, time deltas, units
+of measurement, currencies and encrypted passwords.
See Also:
@@ -402,16 +425,17 @@ See Also:
Building Transformers
----------------------
-A *transformer* is an object which can receive a :class:`.Query` object and return a
-new one. The :class:`.Query` object includes a method :meth:`.with_transformation`
-that simply returns a new :class:`.Query` transformed by the given function.
+A *transformer* is an object which can receive a :class:`.Query`
+object and return a new one. The :class:`.Query` object includes a
+method :meth:`.with_transformation` that returns a new :class:`.Query`
+transformed by the given function.
We can combine this with the :class:`.Comparator` class to produce one type
of recipe which can both set up the FROM clause of a query as well as assign
filtering criterion.
-Consider a mapped class ``Node``, which assembles using adjacency list into a hierarchical
-tree pattern::
+Consider a mapped class ``Node``, which assembles using adjacency list
+into a hierarchical tree pattern::
from sqlalchemy import Column, Integer, ForeignKey
from sqlalchemy.orm import relationship
@@ -424,8 +448,9 @@ tree pattern::
parent_id = Column(Integer, ForeignKey('node.id'))
parent = relationship("Node", remote_side=id)
-Suppose we wanted to add an accessor ``grandparent``. This would return the ``parent`` of
-``Node.parent``. When we have an instance of ``Node``, this is simple::
+Suppose we wanted to add an accessor ``grandparent``. This would
+return the ``parent`` of ``Node.parent``. When we have an instance of
+``Node``, this is simple::
from sqlalchemy.ext.hybrid import hybrid_property
@@ -436,11 +461,13 @@ Suppose we wanted to add an accessor ``grandparent``. This would return the ``p
def grandparent(self):
return self.parent.parent
-For the expression, things are not so clear. We'd need to construct a :class:`.Query` where we
-:meth:`~.Query.join` twice along ``Node.parent`` to get to the ``grandparent``. We can instead
-return a transforming callable that we'll combine with the :class:`.Comparator` class
-to receive any :class:`.Query` object, and return a new one that's joined to the ``Node.parent``
-attribute and filtered based on the given criterion::
+For the expression, things are not so clear. We'd need to construct
+a :class:`.Query` where we :meth:`~.Query.join` twice along
+``Node.parent`` to get to the ``grandparent``. We can instead return
+a transforming callable that we'll combine with the
+:class:`.Comparator` class to receive any :class:`.Query` object, and
+return a new one that's joined to the ``Node.parent`` attribute and
+filtered based on the given criterion::
from sqlalchemy.ext.hybrid import Comparator
@@ -469,15 +496,17 @@ attribute and filtered based on the given criterion::
def grandparent(cls):
return GrandparentTransformer(cls)
-The ``GrandparentTransformer`` overrides the core :meth:`.Operators.operate` method
-at the base of the :class:`.Comparator` hierarchy to return a query-transforming
-callable, which then runs the given comparison operation in a particular context.
-Such as, in the example above, the ``operate`` method is called, given the
-:attr:`.Operators.eq` callable as well as the right side of the comparison
-``Node(id=5)``. A function ``transform`` is then returned which will transform
-a :class:`.Query` first to join to ``Node.parent``, then to compare ``parent_alias``
-using :attr:`.Operators.eq` against the left and right sides, passing into
-:class:`.Query.filter`:
+The ``GrandparentTransformer`` overrides the core
+:meth:`.Operators.operate` method at the base of the
+:class:`.Comparator` hierarchy to return a query-transforming
+callable, which then runs the given comparison operation in a
+particular context. Such as, in the example above, the ``operate``
+method is called, given the :attr:`.Operators.eq` callable as well as
+the right side of the comparison ``Node(id=5)``. A function
+``transform`` is then returned which will transform a :class:`.Query`
+first to join to ``Node.parent``, then to compare ``parent_alias``
+using :attr:`.Operators.eq` against the left and right sides, passing
+into :class:`.Query.filter`:
.. sourcecode:: pycon+sql
@@ -549,7 +578,6 @@ class hybrid_method(object):
"""
-
def __init__(self, func, expr=None):
"""Create a new :class:`.hybrid_method`.
@@ -577,7 +605,8 @@ class hybrid_method(object):
return self.func.__get__(instance, owner)
def expression(self, expr):
- """Provide a modifying decorator that defines a SQL-expression producing method."""
+ """Provide a modifying decorator that defines a
+ SQL-expression producing method."""
self.expr = expr
return self
@@ -634,19 +663,22 @@ class hybrid_property(object):
return self
def deleter(self, fdel):
- """Provide a modifying decorator that defines a value-deletion method."""
+ """Provide a modifying decorator that defines a
+ value-deletion method."""
self.fdel = fdel
return self
def expression(self, expr):
- """Provide a modifying decorator that defines a SQL-expression producing method."""
+ """Provide a modifying decorator that defines a SQL-expression
+ producing method."""
self.expr = expr
return self
def comparator(self, comparator):
- """Provide a modifying decorator that defines a custom comparator producing method.
+ """Provide a modifying decorator that defines a custom
+ comparator producing method.
The return value of the decorated method should be an instance of
:class:`~.hybrid.Comparator`.
@@ -655,13 +687,15 @@ class hybrid_property(object):
proxy_attr = attributes.\
create_proxied_attribute(self)
+
def expr(owner):
return proxy_attr(owner, self.__name__, self, comparator(owner))
self.expr = expr
return self
class Comparator(interfaces.PropComparator):
- """A helper class that allows easy construction of custom :class:`~.orm.interfaces.PropComparator`
+ """A helper class that allows easy construction of custom
+ :class:`~.orm.interfaces.PropComparator`
classes for usage with hybrids."""
property = None
@@ -678,5 +712,3 @@ class Comparator(interfaces.PropComparator):
def adapted(self, adapter):
# interesting....
return self
-
-