API Reference
-------------
-.. autoclass:: method
-.. autoclass:: property_
+.. autoclass:: hybrid_method
+ :members:
+.. autoclass:: hybrid_property
+ :members:
.. autoclass:: Comparator
-
+ :show-inheritance:
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
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
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
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
# 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):
@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
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
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):
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
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):
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,
"""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.
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
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
-"""
-
-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"
+ )