]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
- tests for hybrid
authorMike Bayer <mike_mp@zzzcomputing.com>
Tue, 18 Jan 2011 01:05:09 +0000 (20:05 -0500)
committerMike Bayer <mike_mp@zzzcomputing.com>
Tue, 18 Jan 2011 01:05:09 +0000 (20:05 -0500)
- documentation for hybrid
- rewrite descriptor, synonym, comparable_property documentation

doc/build/orm/extensions/hybrid.rst
doc/build/orm/mapper_config.rst
lib/sqlalchemy/ext/declarative.py
lib/sqlalchemy/ext/hybrid.py
lib/sqlalchemy/orm/__init__.py
lib/sqlalchemy/orm/attributes.py
lib/sqlalchemy/orm/mapper.py
test/ext/test_hybrid.py

index 251c872ddfcbf73ae98fe05634e4e05de392f87c..7c6b5f7ebf6427a3966315d01e2254362693bf8a 100644 (file)
@@ -8,7 +8,9 @@ Hybrid Attributes
 API Reference
 -------------
 
-.. autoclass:: method
-.. autoclass:: property_
+.. autoclass:: hybrid_method
+    :members:
+.. autoclass:: hybrid_property
+    :members:
 .. autoclass:: Comparator
-
+    :show-inheritance:
index 75ce82c25ac1a621594fa919f73cb7c1ebf5b25e..e8eab259bf26c650208fe496990e08b5eda87182 100644 (file)
@@ -382,6 +382,8 @@ function. The standard SQLAlchemy technique for descriptors is to create a
 plain descriptor, and to have it read/write from a mapped attribute with a
 different name. Below we illustrate this using Python 2.6-style properties::
 
+    from sqlalchemy.orm import mapper
+
     class EmailAddress(object):
 
         @property
@@ -401,33 +403,92 @@ The approach above will work, but there's more we can add. While our
 descriptor and into the ``_email`` mapped attribute, the class level
 ``EmailAddress.email`` attribute does not have the usual expression semantics
 usable with :class:`.Query`. To provide these, we instead use the
-:func:`.synonym` function as follows::
+:mod:`~sqlalchemy.ext.hybrid` extension as follows::
 
-    mapper(EmailAddress, addresses_table, properties={
-        'email': synonym('_email', map_column=True)
-    })
+    from sqlalchemy.ext.hybrid import hybrid_property
+
+    class EmailAddress(object):
+
+        @hybrid_property
+        def email(self):
+            return self._email
+
+        @email.setter
+        def email(self, email):
+            self._email = email
+
+The ``email`` attribute now provides a SQL expression when used at the class level:
+
+.. sourcecode:: python+sql
 
-The ``email`` attribute is now usable in the same way as any
-other mapped attribute, including filter expressions,
-get/set operations, etc.::
+    from sqlalchemy.orm import Session
+    session = Session()
+
+    {sql}address = session.query(EmailAddress).filter(EmailAddress.email == 'address@example.com').one()
+    SELECT addresses.email AS addresses_email, addresses.id AS addresses_id 
+    FROM addresses 
+    WHERE addresses.email = ?
+    ('address@example.com',)
+    {stop}
+
+    address.email = 'otheraddress@example.com'
+    {sql}session.commit()
+    UPDATE addresses SET email=? WHERE addresses.id = ?
+    ('otheraddress@example.com', 1)
+    COMMIT
+    {stop}
+
+The :class:`~.hybrid_property` also allows us to change the behavior of the attribute, including 
+defining separate behaviors when the attribute is accessed at the instance level versus at 
+the class/expression level, using the :meth:`.hybrid_property.expression` modifier.  Such
+as, if we wanted to add a host name automatically, we might define two sets of string manipulation
+logic::
+
+    class EmailAddress(object):
+        @hybrid_property
+        def email(self):
+            """Return the value of _email up until the last twelve 
+            characters."""
+
+            return self._email[:-12]
+
+        @email.setter
+        def email(self, email):
+            """Set the value of _email, tacking on the twelve character 
+            value @example.com."""
+
+            self._email = email + "@example.com"
+
+        @email.expression
+        def email(cls):
+            """Produce a SQL expression that represents the value 
+            of the _email column, minus the last twelve characters."""
+
+            return func.substr(cls._email, 0, func.length(cls._email) - 12)
+
+Above, accessing the ``email`` property of an instance of ``EmailAddress`` will return the value of 
+the ``_email`` attribute, removing
+or adding the hostname ``@example.com`` from the value.   When we query against the ``email`` attribute,
+a SQL function is rendered which produces the same effect:
+
+.. sourcecode:: python+sql
 
-    address = session.query(EmailAddress).filter(EmailAddress.email == 'some address').one()
+    {sql}address = session.query(EmailAddress).filter(EmailAddress.email == 'address').one()
+    SELECT addresses.email AS addresses_email, addresses.id AS addresses_id 
+    FROM addresses 
+    WHERE substr(addresses.email, ?, length(addresses.email) - ?) = ?
+    (0, 12, 'address')
+    {stop}
 
-    address.email = 'some other address'
-    session.flush()
 
-    q = session.query(EmailAddress).filter_by(email='some other address')
 
-If the mapped class does not provide a property, the :func:`.synonym` construct will create a default getter/setter object automatically.
+Read more about Hybrids at :ref:`hybrids_toplevel`.
 
-To use synonyms with :mod:`~sqlalchemy.ext.declarative`, see the section 
-:ref:`declarative_synonyms`.
+Synonyms
+~~~~~~~~
 
-Note that the "synonym" feature is eventually to be replaced by the superior
-"hybrid attributes" approach, slated to become a built in feature of SQLAlchemy
-in a future release.  "hybrid" attributes are simply Python properties that evaulate
-at both the class level and at the instance level.  For an example of their usage,
-see the :mod:`derived_attributes` example.
+Synonyms are a mapper-level construct that applies expression behavior to a descriptor
+based attribute.  The functionality of synonym is superceded as of 0.7 by hybrid attributes.
 
 .. autofunction:: synonym
 
@@ -438,11 +499,16 @@ Custom Comparators
 
 The expressions returned by comparison operations, such as
 ``User.name=='ed'``, can be customized, by implementing an object that
-explicitly defines each comparison method needed. This is a relatively rare
-use case. For most needs, the approach in :ref:`mapper_sql_expressions` will
-often suffice, or alternatively a scheme like that of the 
-:mod:`.derived_attributes` example.  Those approaches should be tried first
-before resorting to custom comparison objects.
+explicitly defines each comparison method needed. 
+
+This is a relatively rare use case which generally applies only to 
+highly customized types.  Usually, custom SQL behaviors can be 
+associated with a mapped class by composing together the classes'
+existing mapped attributes with other expression components, 
+using either mapped SQL expressions as those described in
+:ref:`mapper_sql_expressions`, or so-called "hybrid" attributes
+as described at :ref:`hybrids_toplevel`.  Those approaches should be 
+considered first before resorting to custom comparison objects.
 
 Each of :func:`.column_property`, :func:`~.composite`, :func:`.relationship`,
 and :func:`.comparable_property` accept an argument called
index 898a9a7280e48dbc62163cd2175e53b1f42bbe69..133bc8476504edd471217930affa5d43064e77fc 100755 (executable)
@@ -191,67 +191,6 @@ a class, unless the :class:`.relationship` is declared with ``viewonly=True``.
 Otherwise, the unit-of-work system may attempt duplicate INSERT and
 DELETE statements against the underlying table.
 
-.. _declarative_synonyms:
-
-Defining Synonyms
-=================
-
-Synonyms are introduced in :ref:`synonyms`. To define a getter/setter
-which proxies to an underlying attribute, use
-:func:`~.synonym` with the ``descriptor`` argument.  Here we present
-using Python 2.6 style properties::
-
-    class MyClass(Base):
-        __tablename__ = 'sometable'
-
-        id = Column(Integer, primary_key=True)
-
-        _attr = Column('attr', String)
-
-        @property
-        def attr(self):
-            return self._attr
-
-        @attr.setter
-        def attr(self, attr):
-            self._attr = attr
-
-        attr = synonym('_attr', descriptor=attr)
-
-The above synonym is then usable as an instance attribute as well as a
-class-level expression construct::
-
-    x = MyClass()
-    x.attr = "some value"
-    session.query(MyClass).filter(MyClass.attr == 'some other value').all()
-
-For simple getters, the :func:`synonym_for` decorator can be used in
-conjunction with ``@property``::
-
-    class MyClass(Base):
-        __tablename__ = 'sometable'
-
-        id = Column(Integer, primary_key=True)
-        _attr = Column('attr', String)
-
-        @synonym_for('_attr')
-        @property
-        def attr(self):
-            return self._attr
-
-Similarly, :func:`comparable_using` is a front end for the
-:func:`~.comparable_property` ORM function::
-
-    class MyClass(Base):
-        __tablename__ = 'sometable'
-
-        name = Column('name', String)
-
-        @comparable_using(MyUpperCaseComparator)
-        @property
-        def uc_name(self):
-            return self.name.upper()
-
 .. _declarative_sql_expressions:
 
 Defining SQL Expressions
index 9fb8a27d82506f6bc8cb556150afc7be6917b3c0..5c747400de8b1c6118c148681c1615673795f0ce 100644 (file)
@@ -4,34 +4,35 @@
 # This module is part of SQLAlchemy and is released under
 # the MIT License: http://www.opensource.org/licenses/mit-license.php
 
-"""Define attributes on ORM-mapped classes that have 'hybrid' behavior.
+"""Define attributes on ORM-mapped classes that have "hybrid" behavior.
 
-'hybrid' means the attribute has distinct behaviors defined at the
+"hybrid" means the attribute has distinct behaviors defined at the
 class level and at the instance level.
 
-Consider a table `interval` as below::
+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 class-level expression generator.
+
+Consider a table ``interval`` as below::
 
     from sqlalchemy import MetaData, Table, Column, Integer
-    from sqlalchemy.orm import mapper, create_session
 
-    engine = create_engine('sqlite://')
     metadata = MetaData()
 
     interval_table = Table('interval', metadata,
         Column('id', Integer, primary_key=True),
         Column('start', Integer, nullable=False),
-        Column('end', Integer, nullable=False))
-    metadata.create_all(engine)
+        Column('end', Integer, nullable=False)
+    )
 
 We can define higher level functions on mapped classes that produce SQL
 expressions at the class level, and Python expression evaluation at the
-instance level.  Below, each function decorated with :func:`hybrid.method`
-or :func:`hybrid.property` may receive ``self`` as an instance of the class,
+instance level.  Below, each function decorated with :func:`.hybrid_method`
+or :func:`.hybrid_property` may receive ``self`` as an instance of the class,
 or as the class itself::
 
-    # A base class for intervals
-
-    from sqlalchemy.orm.hybrid import hybrid_property, hybrid_method
+    from sqlalchemy.ext.hybrid import hybrid_property, hybrid_method
+    from sqlalchemy.orm import mapper, Session, aliased
 
     class Interval(object):
         def __init__(self, start, end):
@@ -49,15 +50,271 @@ or as the class itself::
         @hybrid_method
         def intersects(self, other):
             return self.contains(other.start) | self.contains(other.end)
+    
+    mapper(Interval, interval_table)
+
+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
+    5
+    
+At the class level, the usual descriptor behavior of returning the descriptor
+itself is modified by :class:`.hybrid_property`, to instead evaluate the function 
+body given the ``Interval`` class as the argument::
+    
+    >>> print Interval.length
+    interval."end" - interval.start
+    
+    >>> print Session().query(Interval).filter(Interval.length > 10)
+    SELECT interval.id AS interval_id, interval.start AS interval_start, 
+    interval."end" AS interval_end 
+    FROM interval 
+    WHERE interval."end" - interval.start > :param_1
+    
+ORM methods such as :meth:`~.Query.filter_by` generally use ``getattr()`` to 
+locate attributes, so can also be used with hybrid attributes::
+
+    >>> print Session().query(Interval).filter_by(length=5)
+    SELECT interval.id AS interval_id, interval.start AS interval_start, 
+    interval."end" AS interval_end 
+    FROM interval 
+    WHERE interval."end" - interval.start = :param_1
+
+The ``contains()`` and ``intersects()`` methods are decorated with :class:`.hybrid_method`.
+This decorator applies the same idea to methods which accept
+zero or more arguments.   The above 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)
+    True
+    >>> i1.contains(15)
+    False
+    >>> i1.intersects(Interval(7, 18))
+    True
+    >>> i1.intersects(Interval(25, 29))
+    False
+    
+    >>> print Session().query(Interval).filter(Interval.contains(15))
+    SELECT interval.id AS interval_id, interval.start AS interval_start, 
+    interval."end" AS interval_end 
+    FROM interval 
+    WHERE interval.start <= :start_1 AND interval."end" > :end_1
+
+    >>>  ia = aliased(Interval)
+    >>> print Session().query(Interval, ia).filter(Interval.intersects(ia))
+    SELECT interval.id AS interval_id, interval.start AS interval_start, 
+    interval."end" AS interval_end, interval_1.id AS interval_1_id, 
+    interval_1.start AS interval_1_start, interval_1."end" AS interval_1_end 
+    FROM interval, interval AS interval_1 
+    WHERE interval.start <= interval_1.start 
+        AND interval."end" > interval_1.start 
+        OR interval.start <= interval_1."end" 
+        AND interval."end" > interval_1."end"
+    
+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::
+
+    from sqlalchemy import func
+    
+    class Interval(object):
+        # ...
+        
+        @hybrid_property
+        def radius(self):
+            return abs(self.length) / 2
+            
+        @radius.expression
+        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::
+
+    >>> i1.radius
+    2
+    
+    >>> print Session().query(Interval).filter(Interval.radius > 5)
+    SELECT interval.id AS interval_id, interval.start AS interval_start, 
+        interval."end" AS interval_end 
+    FROM interval 
+    WHERE abs(interval."end" - interval.start) / :abs_1 > :param_1
+
+Defining Setters
+----------------
+
+Hybrid properties can also define setter methods.  If we wanted ``length`` above, when 
+set, to modify the endpoint value::
+
+    class Interval(object):
+        # ...
+        
+        @hybrid_property
+        def length(self):
+            return self.end - self.start
+
+        @length.setter
+        def length(self, value):
+            self.end = self.start + value
+
+The ``length(self, value)`` method is now called upon set::
+
+    >>> i1 = Interval(5, 10)
+    >>> i1.length
+    5
+    >>> i1.length = 12
+    >>> i1.end
+    17
+
+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``::
+
+    from sqlalchemy import Column, Integer, ForeignKey, Numeric, String
+    from sqlalchemy.orm import relationship
+    from sqlalchemy.ext.declarative import declarative_base
+    from sqlalchemy.ext.hybrid import hybrid_property
+    
+    Base = declarative_base()
+    
+    class SavingsAccount(Base):
+        __tablename__ = 'account'
+        id = Column(Integer, primary_key=True)
+        user_id = Column(Integer, ForeignKey('user.id'), nullable=False)
+        balance = Column(Numeric(15, 5))
+
+    class User(Base):
+        __tablename__ = 'user'
+        id = Column(Integer, primary_key=True)
+        name = Column(String(100), nullable=False)
+        
+        accounts = relationship("SavingsAccount", backref="owner")
+        
+        @hybrid_property
+        def balance(self):
+            if self.accounts:
+                return self.accounts[0].balance
+            else:
+                return None
+
+        @balance.setter
+        def balance(self, value):
+            if not self.accounts:
+                account = Account(owner=self)
+            else:
+                account = self.accounts[0]
+            account.balance = balance
+
+        @balance.expression
+        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
+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::
+
+    >>> 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
+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 
+    FROM "user" LEFT OUTER JOIN account ON "user".id = account.user_id 
+    WHERE account.balance <  :balance_1 OR account.balance IS NULL
+
+.. _hybrid_custom_comparators:
+
+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 example class below allows case-insensitive comparisons on the attribute
+named ``word_insensitive``::
+
+    from sqlalchemy.ext.hybrid import Comparator
+    
+    class CaseInsensitiveComparator(Comparator):
+        def __eq__(self, other):
+            return func.lower(self.__clause_element__()) == func.lower(other)
+
+    class SearchWord(Base):
+        __tablename__ = 'searchword'
+        id = Column(Integer, primary_key=True)
+        word = Column(String(255), nullable=False)
+        
+        @hybrid_property
+        def word_insensitive(self):
+            return self.word.lower()
+        
+        @word_insensitive.comparator
+        def word_insensitive(cls):
+            return CaseInsensitiveComparator(cls.word)
 
+Above, SQL expressions against ``word_insensitive`` will apply the ``LOWER()`` 
+SQL function to both sides::
 
+    >>> print Session().query(SearchWord).filter_by(word_insensitive="Trucks")
+    SELECT searchword.id AS searchword_id, searchword.word AS searchword_word 
+    FROM searchword 
+    WHERE lower(searchword.word) = lower(:lower_1)
 
 """
 from sqlalchemy import util
 from sqlalchemy.orm import attributes, interfaces
+import new
 
 class hybrid_method(object):
+    """A decorator which allows definition of a Python object method with both
+    instance-level and class-level behavior.
+    
+    """
+
+
     def __init__(self, func, expr=None):
+        """Create a new :class:`.hybrid_method`.
+        
+        Usage is typically via decorator::
+        
+            from sqlalchemy.ext.hybrid import hybrid_method
+        
+            class SomeClass(object):
+                @hybrid_method
+                def value(self, x, y):
+                    return self._value + x + y
+            
+                @hybrid_property.expression
+                def value(self, x, y):
+                    return func.some_function(self._value, x, y)
+            
+        """
         self.func = func
         self.expr = expr or func
 
@@ -68,11 +325,34 @@ class hybrid_method(object):
             return new.instancemethod(self.func, instance, owner)
 
     def expression(self, expr):
+        """Provide a modifying decorator that defines a SQL-expression producing method."""
+
         self.expr = expr
         return self
 
 class hybrid_property(object):
+    """A decorator which allows definition of a Python descriptor with both
+    instance-level and class-level behavior.
+    
+    """
+
     def __init__(self, fget, fset=None, fdel=None, expr=None):
+        """Create a new :class:`.hybrid_property`.
+        
+        Usage is typically via decorator::
+        
+            from sqlalchemy.ext.hybrid import hybrid_property
+        
+            class SomeClass(object):
+                @hybrid_property
+                def value(self):
+                    return self._value
+            
+                @hybrid_property.setter
+                def value(self, value):
+                    self._value = value
+            
+        """
         self.fget = fget
         self.fset = fset
         self.fdel = fdel
@@ -92,18 +372,31 @@ class hybrid_property(object):
         self.fdel(instance)
 
     def setter(self, fset):
+        """Provide a modifying decorator that defines a value-setter method."""
+
         self.fset = fset
         return self
 
     def deleter(self, fdel):
+        """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."""
+
         self.expr = expr
         return self
 
     def comparator(self, comparator):
+        """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`.
+        
+        """
+
         proxy_attr = attributes.\
                         create_proxied_attribute(self)
         def expr(owner):
@@ -113,6 +406,10 @@ class hybrid_property(object):
 
 
 class Comparator(interfaces.PropComparator):
+    """A helper class that allows easy construction of custom :class:`~.orm.interfaces.PropComparator`
+    classes for usage with hybrids."""
+
+
     def __init__(self, expression):
         self.expression = expression
 
index ff8c7155c6c8a11f798801eaa9f07e64f2675667..b8b995f8121e4a0d4beae41d8720263707017cbb 100644 (file)
@@ -884,27 +884,14 @@ def mapper(class_, local_table=None, *args, **params):
 
 def synonym(name, map_column=False, descriptor=None, 
                         comparator_factory=None, doc=None):
-    """Set up `name` as a synonym to another mapped property.
+    """Denote an attribute name as a synonym to a mapped property.
 
-    Used with the ``properties`` dictionary sent to
-    :func:`~sqlalchemy.orm.mapper`.
-
-    Any existing attributes on the class which map the key name sent
-    to the ``properties`` dictionary will be used by the synonym to provide
-    instance-attribute behavior (that is, any Python property object, provided
-    by the ``property`` builtin or providing a ``__get__()``, ``__set__()``
-    and ``__del__()`` method).  If no name exists for the key, the
-    ``synonym()`` creates a default getter/setter object automatically and
-    applies it to the class.
-
-    `name` refers to the name of the existing mapped property, which can be
-    any other ``MapperProperty`` including column-based properties and
-    relationships.
+    .. note:: :func:`.synonym` is superceded as of 0.7 by 
+       the :mod:`~sqlalchemy.ext.hybrid` extension.  See 
+       the documentation for hybrids at :ref:`hybrids_toplevel`.
 
-    If `map_column` is ``True``, an additional ``ColumnProperty`` is created
-    on the mapper automatically, using the synonym's name as the keyname of
-    the property, and the keyname of this ``synonym()`` as the name of the
-    column to map.  For example, if a table has a column named ``status``::
+    Used with the ``properties`` dictionary sent to
+    :func:`~sqlalchemy.orm.mapper`::
 
         class MyClass(object):
             def _get_status(self):
@@ -916,10 +903,20 @@ def synonym(name, map_column=False, descriptor=None,
         mapper(MyClass, sometable, properties={
             "status":synonym("_status", map_column=True)
         })
+    
+    Above, the ``status`` attribute of MyClass will produce
+    expression behavior against the table column named ``status``,
+    using the Python attribute ``_status`` on the mapped class
+    to represent the underlying value.
+
+    :param name: the name of the existing mapped property, which can be
+      any other ``MapperProperty`` including column-based properties and
+      relationships.
 
-    The column named ``status`` will be mapped to the attribute named
-    ``_status``, and the ``status`` attribute on ``MyClass`` will be used to
-    proxy access to the column-based attribute.
+    :param map_column: if ``True``, an additional ``ColumnProperty`` is created
+      on the mapper automatically, using the synonym's name as the keyname of
+      the property, and the keyname of this ``synonym()`` as the name of the
+      column to map.
 
     """
     return SynonymProperty(name, map_column=map_column, 
@@ -931,36 +928,49 @@ def comparable_property(comparator_factory, descriptor=None):
     """Provides a method of applying a :class:`.PropComparator` 
     to any Python descriptor attribute.
 
-    Allows a regular Python @property (descriptor) to be used in Queries and
+    .. note:: :func:`.comparable_property` is superceded as of 0.7 by 
+       the :mod:`~sqlalchemy.ext.hybrid` extension.  See the example 
+       at :ref:`hybrid_custom_comparators`.
+       
+    Allows a regular Python @property (descriptor) to be used in queries and
     SQL constructs like a managed attribute.  comparable_property wraps a
     descriptor with a proxy that directs operator overrides such as ==
     (__eq__) to the supplied comparator but proxies everything else through to
-    the original descriptor::
+    the original descriptor.  Used with the ``properties`` dictionary sent to
+    :func:`~sqlalchemy.orm.mapper`::
 
       from sqlalchemy.orm import mapper, comparable_property
       from sqlalchemy.orm.interfaces import PropComparator
       from sqlalchemy.sql import func
-
-      class MyClass(object):
-          @property
-          def myprop(self):
-              return 'foo'
-
-      class MyComparator(PropComparator):
+      from sqlalchemy import Table, MetaData, Integer, String, Column
+      
+      metadata = MetaData()
+
+      word_table = Table('word', metadata,
+        Column('id', Integer, primary_key=True),
+        Column('word', String(200), nullable=False)
+      )
+      
+      class CaseInsensitiveComparator(PropComparator):
+          def __clause_element__(self):
+              return self.prop
+              
           def __eq__(self, other):
-              return func.lower(other) == foo
-
-      mapper(MyClass, mytable, properties={
-               'myprop': comparable_property(MyComparator)})
-
-    Used with the ``properties`` dictionary sent to
-    :func:`~sqlalchemy.orm.mapper`.
-
-    Note that :func:`comparable_property` is usually not needed for basic
-    needs. The recipe at :mod:`.derived_attributes` offers a simpler
-    pure-Python method of achieving a similar result using class-bound
-    attributes with SQLAlchemy expression constructs.
-
+              return func.lower(self.__clause_element__()) == func.lower(other)
+
+      class SearchWord(object):
+          pass
+
+      mapper(SearchWord, word_table, properties={
+               'word_insensitive': comparable_property(CaseInsensitiveComparator)
+              })
+    
+    A mapping like the above allows the ``word_insensitive`` attribute 
+    to render an expression like::
+    
+        >>> print SearchWord.word_insensitive == "Trucks"
+        lower(:lower_1) = lower(:lower_2)
+        
     :param comparator_factory:
       A PropComparator subclass or factory that defines operator behavior
       for this property.
index eb923bc4ee10e04ead1562c2a762688ad20fc43e..ca1137c3f75459a28e8073f9bfb3bcc540d8acc9 100644 (file)
@@ -174,6 +174,13 @@ def create_proxied_attribute(descriptor):
                 self._comparator = self._comparator.adapted(self.adapter)
             return self._comparator
 
+        def adapted(self, adapter):
+            """Proxy adapted() for the use case of AliasedClass calling adapted."""
+
+            return self.__class__(self.class_, self.key, self.descriptor,
+                                       self._comparator,
+                                       adapter)
+
         def __get__(self, instance, owner):
             if instance is None:
                 return self
index 8fe68fb8c0fd75e18f3ed398749e6ab8f0e13894..399c4436fd6a11a6e61749b0d635825daae6dc96 100644 (file)
@@ -2449,10 +2449,11 @@ def validates(*names):
 
     Designates a method as a validator, a method which receives the
     name of the attribute as well as a value to be assigned, or in the
-    case of a collection to be added to the collection.  The function
-    can then raise validation exceptions to halt the process from continuing,
-    or can modify or replace the value before proceeding.   The function
-    should otherwise return the given value.
+    case of a collection, the value to be added to the collection.  The function
+    can then raise validation exceptions to halt the process from continuing
+    (where Python's built-in ``ValueError`` and ``AssertionError`` exceptions are
+    reasonable choices), or can modify or replace the value before proceeding.   
+    The function should otherwise return the given value.
 
     Note that a validator for a collection **cannot** issue a load of that
     collection within the validation routine - this usage raises
index 46fc01aae7de3168b5cb036a6fefcceaa0df4774..201d3821f640aebf6bc96499cd514181e19f2de2 100644 (file)
-"""
-
-tests for sqlalchemy.ext.hybrid TODO
-
-
-"""
-
-
-from sqlalchemy import *
-from sqlalchemy.orm import *
-from sqlalchemy.ext.declarative import declarative_base
-from sqlalchemy.ext import hybrid
-from sqlalchemy.orm.interfaces import PropComparator
-
-
-"""
-from sqlalchemy import *
-from sqlalchemy.orm import *
+from sqlalchemy import func, Integer, String
+from sqlalchemy.orm import relationship, Session, aliased
+from test.lib.schema import Column
 from sqlalchemy.ext.declarative import declarative_base
 from sqlalchemy.ext import hybrid
-
-Base = declarative_base()
-
-
-class UCComparator(hybrid.Comparator):
-
-    def __eq__(self, other):
-        if other is None:
-            return self.expression == None
-        else:
-            return func.upper(self.expression) == func.upper(other)
-
-class A(Base):
-    __tablename__ = 'a'
-    id = Column(Integer, primary_key=True)
-    _value = Column("value", String)
-
-    @hybrid.property_
-    def value(self):
-        return int(self._value)
-
-    @value.comparator
-    def value(cls):
-        return UCComparator(cls._value)
-
-    @value.setter
-    def value(self, v):
-        self.value = v
-print aliased(A).value
-print aliased(A).__tablename__
-
-sess = create_session()
-
-print A.value == "foo"
-print sess.query(A.value)
-print sess.query(aliased(A).value)
-print sess.query(aliased(A)).filter_by(value="foo")
-"""
-
-"""
-from sqlalchemy import *
-from sqlalchemy.orm import *
-from sqlalchemy.ext.declarative import declarative_base
-from sqlalchemy.ext import hybrid
-
-Base = declarative_base()
-
-class A(Base):
-    __tablename__ = 'a'
-    id = Column(Integer, primary_key=True)
-    _value = Column("value", String)
-
-    @hybrid.property
-    def value(self):
-        return int(self._value)
-
-    @value.expression
-    def value(cls):
-        return func.foo(cls._value) + cls.bar_value
-
-    @value.setter
-    def value(self, v):
-        self.value = v
-
-    @hybrid.property
-    def bar_value(cls):
-        return func.bar(cls._value)
-
-#print A.value
-#print A.value.__doc__
-
-print aliased(A).value
-print aliased(A).__tablename__
-
-sess = create_session()
-
-print sess.query(A).filter_by(value="foo")
-
-print sess.query(aliased(A)).filter_by(value="foo")
-
-
-"""
\ No newline at end of file
+from test.lib.testing import TestBase, eq_, AssertsCompiledSQL
+
+class PropertyComparatorTest(TestBase, AssertsCompiledSQL):
+
+    def _fixture(self):
+        Base = declarative_base()
+
+        class UCComparator(hybrid.Comparator):
+
+            def __eq__(self, other):
+                if other is None:
+                    return self.expression == None
+                else:
+                    return func.upper(self.expression) == func.upper(other)
+
+        class A(Base):
+            __tablename__ = 'a'
+            id = Column(Integer, primary_key=True)
+            _value = Column("value", String)
+
+            @hybrid.hybrid_property
+            def value(self):
+                return self._value - 5
+
+            @value.comparator
+            def value(cls):
+                return UCComparator(cls._value)
+
+            @value.setter
+            def value(self, v):
+                self._value = v + 5
+
+        return A
+
+    def test_set_get(self):
+        A = self._fixture()
+        a1 = A(value=5)
+        eq_(a1._value, 10)
+        eq_(a1.value, 5)
+
+    def test_value(self):
+        A = self._fixture()
+        eq_(str(A.value==5), "upper(a.value) = upper(:upper_1)")
+
+    def test_aliased_value(self):
+        A = self._fixture()
+        eq_(str(aliased(A).value==5), "upper(a_1.value) = upper(:upper_1)")
+
+    def test_query(self):
+        A = self._fixture()
+        sess = Session()
+        self.assert_compile(
+            sess.query(A.value),
+            "SELECT a.value AS a_value FROM a"
+        )
+
+    def test_alised_query(self):
+        A = self._fixture()
+        sess = Session()
+        self.assert_compile(
+            sess.query(aliased(A).value),
+            "SELECT a_1.value AS a_1_value FROM a AS a_1"
+        )
+
+    def test_aliased_filter(self):
+        A = self._fixture()
+        sess = Session()
+        self.assert_compile(
+            sess.query(aliased(A)).filter_by(value="foo"),
+            "SELECT a_1.value AS a_1_value, a_1.id AS a_1_id "
+            "FROM a AS a_1 WHERE upper(a_1.value) = upper(:upper_1)"
+        )
+
+class PropertyExpressionTest(TestBase, AssertsCompiledSQL):
+    def _fixture(self):
+        Base = declarative_base()
+
+        class A(Base):
+            __tablename__ = 'a'
+            id = Column(Integer, primary_key=True)
+            _value = Column("value", String)
+
+            @hybrid.hybrid_property
+            def value(self):
+                return int(self._value) - 5
+
+            @value.expression
+            def value(cls):
+                return func.foo(cls._value) + cls.bar_value
+
+            @value.setter
+            def value(self, v):
+                self._value = v + 5
+
+            @hybrid.hybrid_property
+            def bar_value(cls):
+                return func.bar(cls._value)
+
+        return A
+
+    def test_set_get(self):
+        A = self._fixture()
+        a1 = A(value=5)
+        eq_(a1._value, 10)
+        eq_(a1.value, 5)
+
+    def test_expression(self):
+        A = self._fixture()
+        self.assert_compile(
+            A.value,
+            "foo(a.value) + bar(a.value)"
+        )
+
+    def test_aliased_expression(self):
+        A = self._fixture()
+        self.assert_compile(
+            aliased(A).value,
+            "foo(a_1.value) + bar(a_1.value)"
+        )
+
+    def test_query(self):
+        A = self._fixture()
+        sess = Session()
+        self.assert_compile(
+            sess.query(A).filter_by(value="foo"),
+            "SELECT a.value AS a_value, a.id AS a_id "
+            "FROM a WHERE foo(a.value) + bar(a.value) = :param_1"
+        )
+
+    def test_aliased_query(self):
+        A = self._fixture()
+        sess = Session()
+        self.assert_compile(
+            sess.query(aliased(A)).filter_by(value="foo"),
+            "SELECT a_1.value AS a_1_value, a_1.id AS a_1_id "
+            "FROM a AS a_1 WHERE foo(a_1.value) + bar(a_1.value) = :param_1"
+        )
+
+class PropertyValueTest(TestBase, AssertsCompiledSQL):
+    def _fixture(self):
+        Base = declarative_base()
+
+        class A(Base):
+            __tablename__ = 'a'
+            id = Column(Integer, primary_key=True)
+            _value = Column("value", String)
+
+            @hybrid.hybrid_property
+            def value(self):
+                return self._value - 5
+
+            @value.setter
+            def value(self, v):
+                self._value = v + 5
+
+        return A
+
+    def test_set_get(self):
+        A = self._fixture()
+        a1 = A(value=5)
+        eq_(a1.value, 5)
+        eq_(a1._value, 10)
+
+class MethodExpressionTest(TestBase, AssertsCompiledSQL):
+    def _fixture(self):
+        Base = declarative_base()
+
+        class A(Base):
+            __tablename__ = 'a'
+            id = Column(Integer, primary_key=True)
+            _value = Column("value", String)
+
+            @hybrid.hybrid_method
+            def value(self, x):
+                return int(self._value) + x
+
+            @value.expression
+            def value(cls, value):
+                return func.foo(cls._value, value) + value
+
+        return A
+
+    def test_call(self):
+        A = self._fixture()
+        a1 = A(_value=10)
+        eq_(a1.value(7), 17)
+
+    def test_expression(self):
+        A = self._fixture()
+        self.assert_compile(
+            A.value(5),
+            "foo(a.value, :foo_1) + :foo_2"
+        )
+
+    def test_aliased_expression(self):
+        A = self._fixture()
+        self.assert_compile(
+            aliased(A).value(5),
+            "foo(a_1.value, :foo_1) + :foo_2"
+        )
+
+    def test_query(self):
+        A = self._fixture()
+        sess = Session()
+        self.assert_compile(
+            sess.query(A).filter(A.value(5)=="foo"),
+            "SELECT a.value AS a_value, a.id AS a_id "
+            "FROM a WHERE foo(a.value, :foo_1) + :foo_2 = :param_1"
+        )
+
+    def test_aliased_query(self):
+        A = self._fixture()
+        sess = Session()
+        a1 = aliased(A)
+        self.assert_compile(
+            sess.query(a1).filter(a1.value(5)=="foo"),
+            "SELECT a_1.value AS a_1_value, a_1.id AS a_1_id "
+            "FROM a AS a_1 WHERE foo(a_1.value, :foo_1) + :foo_2 = :param_1"
+        )
+
+    def test_query_col(self):
+        A = self._fixture()
+        sess = Session()
+        self.assert_compile(
+            sess.query(A.value(5)),
+            "SELECT foo(a.value, :foo_1) + :foo_2 AS anon_1 FROM a"
+        )
+
+    def test_aliased_query_col(self):
+        A = self._fixture()
+        sess = Session()
+        self.assert_compile(
+            sess.query(aliased(A).value(5)),
+            "SELECT foo(a_1.value, :foo_1) + :foo_2 AS anon_1 FROM a AS a_1"
+        )