From: Mike Bayer Date: Tue, 18 Jan 2011 01:05:09 +0000 (-0500) Subject: - tests for hybrid X-Git-Tag: rel_0_7b1~64 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=dc7d611182c9a0c364f334180c6c7a4ca99f79e6;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git - tests for hybrid - documentation for hybrid - rewrite descriptor, synonym, comparable_property documentation --- diff --git a/doc/build/orm/extensions/hybrid.rst b/doc/build/orm/extensions/hybrid.rst index 251c872ddf..7c6b5f7ebf 100644 --- a/doc/build/orm/extensions/hybrid.rst +++ b/doc/build/orm/extensions/hybrid.rst @@ -8,7 +8,9 @@ Hybrid Attributes API Reference ------------- -.. autoclass:: method -.. autoclass:: property_ +.. autoclass:: hybrid_method + :members: +.. autoclass:: hybrid_property + :members: .. autoclass:: Comparator - + :show-inheritance: diff --git a/doc/build/orm/mapper_config.rst b/doc/build/orm/mapper_config.rst index 75ce82c25a..e8eab259bf 100644 --- a/doc/build/orm/mapper_config.rst +++ b/doc/build/orm/mapper_config.rst @@ -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 diff --git a/lib/sqlalchemy/ext/declarative.py b/lib/sqlalchemy/ext/declarative.py index 898a9a7280..133bc84765 100755 --- a/lib/sqlalchemy/ext/declarative.py +++ b/lib/sqlalchemy/ext/declarative.py @@ -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 diff --git a/lib/sqlalchemy/ext/hybrid.py b/lib/sqlalchemy/ext/hybrid.py index 9fb8a27d82..5c747400de 100644 --- a/lib/sqlalchemy/ext/hybrid.py +++ b/lib/sqlalchemy/ext/hybrid.py @@ -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 diff --git a/lib/sqlalchemy/orm/__init__.py b/lib/sqlalchemy/orm/__init__.py index ff8c7155c6..b8b995f812 100644 --- a/lib/sqlalchemy/orm/__init__.py +++ b/lib/sqlalchemy/orm/__init__.py @@ -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. diff --git a/lib/sqlalchemy/orm/attributes.py b/lib/sqlalchemy/orm/attributes.py index eb923bc4ee..ca1137c3f7 100644 --- a/lib/sqlalchemy/orm/attributes.py +++ b/lib/sqlalchemy/orm/attributes.py @@ -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 diff --git a/lib/sqlalchemy/orm/mapper.py b/lib/sqlalchemy/orm/mapper.py index 8fe68fb8c0..399c4436fd 100644 --- a/lib/sqlalchemy/orm/mapper.py +++ b/lib/sqlalchemy/orm/mapper.py @@ -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 diff --git a/test/ext/test_hybrid.py b/test/ext/test_hybrid.py index 46fc01aae7..201d3821f6 100644 --- a/test/ext/test_hybrid.py +++ b/test/ext/test_hybrid.py @@ -1,102 +1,240 @@ -""" - -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" + )