diff options
Diffstat (limited to 'lib/sqlalchemy/ext/hybrid.py')
-rw-r--r-- | lib/sqlalchemy/ext/hybrid.py | 218 |
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 - - |