[Sqlalchemy-commits] sqlalchemy: - tests for hybrid
Brought to you by:
zzzeek
From: <co...@sq...> - 2011-01-18 01:05:21
|
details: http://hg.sqlalchemy.org/sqlalchemy/sqlalchemy/rev/1fefc67bb211 changeset: 7275:1fefc67bb211 user: zzzeek date: Mon Jan 17 20:05:09 2011 -0500 description: - tests for hybrid - documentation for hybrid - rewrite descriptor, synonym, comparable_property documentation diffstat: doc/build/orm/extensions/hybrid.rst | 8 +- doc/build/orm/mapper_config.rst | 114 ++++++++++-- lib/sqlalchemy/ext/declarative.py | 61 ------ lib/sqlalchemy/ext/hybrid.py | 321 ++++++++++++++++++++++++++++++++++- lib/sqlalchemy/orm/__init__.py | 94 +++++---- lib/sqlalchemy/orm/attributes.py | 7 + lib/sqlalchemy/orm/mapper.py | 9 +- test/ext/test_hybrid.py | 286 +++++++++++++++++++++++-------- 8 files changed, 680 insertions(+), 220 deletions(-) diffs (truncated from 1120 to 300 lines): diff -r 9ac2e813da54 -r 1fefc67bb211 doc/build/orm/extensions/hybrid.rst --- a/doc/build/orm/extensions/hybrid.rst Sun Jan 16 20:24:05 2011 -0500 +++ b/doc/build/orm/extensions/hybrid.rst Mon Jan 17 20:05:09 2011 -0500 @@ -8,7 +8,9 @@ API Reference ------------- -.. autoclass:: method -.. autoclass:: property_ +.. autoclass:: hybrid_method + :members: +.. autoclass:: hybrid_property + :members: .. autoclass:: Comparator - + :show-inheritance: diff -r 9ac2e813da54 -r 1fefc67bb211 doc/build/orm/mapper_config.rst --- a/doc/build/orm/mapper_config.rst Sun Jan 16 20:24:05 2011 -0500 +++ b/doc/build/orm/mapper_config.rst Mon Jan 17 20:05:09 2011 -0500 @@ -382,6 +382,8 @@ 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 @@ 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 -The ``email`` attribute is now usable in the same way as any -other mapped attribute, including filter expressions, -get/set operations, etc.:: + class EmailAddress(object): - address = session.query(EmailAddress).filter(EmailAddress.email == 'some address').one() + @hybrid_property + def email(self): + return self._email - address.email = 'some other address' - session.flush() + @email.setter + def email(self, email): + self._email = email - q = session.query(EmailAddress).filter_by(email='some other address') +The ``email`` attribute now provides a SQL expression when used at the class level: -If the mapped class does not provide a property, the :func:`.synonym` construct will create a default getter/setter object automatically. +.. sourcecode:: python+sql -To use synonyms with :mod:`~sqlalchemy.ext.declarative`, see the section -:ref:`declarative_synonyms`. + from sqlalchemy.orm import Session + session = Session() -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. + {sql}address = session.query(EmailAddress).filter(EmailAddress.email == 'ad...@ex...').one() + SELECT addresses.email AS addresses_email, addresses.id AS addresses_id + FROM addresses + WHERE addresses.email = ? + ('ad...@ex...',) + {stop} + + address.email = 'oth...@ex...' + {sql}session.commit() + UPDATE addresses SET email=? WHERE addresses.id = ? + ('oth...@ex...', 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 + + {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} + + + +Read more about Hybrids at :ref:`hybrids_toplevel`. + +Synonyms +~~~~~~~~ + +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 @@ 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 -r 9ac2e813da54 -r 1fefc67bb211 lib/sqlalchemy/ext/declarative.py --- a/lib/sqlalchemy/ext/declarative.py Sun Jan 16 20:24:05 2011 -0500 +++ b/lib/sqlalchemy/ext/declarative.py Mon Jan 17 20:05:09 2011 -0500 @@ -191,67 +191,6 @@ 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 -r 9ac2e813da54 -r 1fefc67bb211 lib/sqlalchemy/ext/hybrid.py --- a/lib/sqlalchemy/ext/hybrid.py Sun Jan 16 20:24:05 2011 -0500 +++ b/lib/sqlalchemy/ext/hybrid.py Mon Jan 17 20:05:09 2011 -0500 @@ -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 @@ @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 |