Skip to content

Commit

Permalink
- column_property(), composite_property(), and relation() now
Browse files Browse the repository at this point in the history
accept a single or list of AttributeExtensions using the
"extension" keyword argument.
- Added a Validator AttributeExtension, as well as a
@validates decorator which is used in a similar fashion
as @ReConstructor, and marks a method as validating
one or more mapped attributes.
- removed validate_attributes example, the new methodology replaces it
  • Loading branch information
zzzeek committed Sep 2, 2008
1 parent 3e25e6e commit 3829b89
Show file tree
Hide file tree
Showing 11 changed files with 195 additions and 140 deletions.
14 changes: 10 additions & 4 deletions CHANGES
Original file line number Diff line number Diff line change
Expand Up @@ -33,16 +33,22 @@ CHANGES
clause will appear in the WHERE clause of the query as well
since this discrimination has multiple trigger points.

- AttributeListener has been refined such that the event
- AttributeExtension has been refined such that the event
is fired before the mutation actually occurs. Addtionally,
the append() and set() methods must now return the given value,
which is used as the value to be used in the mutation operation.
This allows creation of validating AttributeListeners which
raise before the action actually occurs, and which can change
the given value into something else before its used.
A new example "validate_attributes.py" shows one such recipe
for doing this. AttributeListener helper functions are
also on the way.

- column_property(), composite_property(), and relation() now
accept a single or list of AttributeExtensions using the
"extension" keyword argument.

- Added a Validator AttributeExtension, as well as a
@validates decorator which is used in a similar fashion
as @reconstructor, and marks a method as validating
one or more mapped attributes.

- class.someprop.in_() raises NotImplementedError pending the
implementation of "in_" for relation [ticket:1140]
Expand Down
42 changes: 39 additions & 3 deletions doc/build/content/mappers.txt
Original file line number Diff line number Diff line change
Expand Up @@ -139,18 +139,54 @@ Correlated subqueries may be used as well:
)
})

#### Overriding Attribute Behavior with Synonyms {@name=overriding}
#### Changing Attribute Behavior {@name=attributes}

A common request is the ability to create custom class properties that override the behavior of setting/getting an attribute. As of 0.4.2, the `synonym()` construct provides an easy way to do this in conjunction with a normal Python `property` constructs. Below, we re-map the `email` column of our mapped table to a custom attribute setter/getter, mapping the actual column to the property named `_email`:
##### Simple Validators {@name=validators}

A quick way to add a "validation" routine to an attribute is to use the `@validates` decorator. This is a shortcut for using the [docstrings_sqlalchemy.orm_Validator](rel:docstrings_sqlalchemy.orm_Validator) attribute extension with individual column or relation based attributes. An attribute validator can raise an exception, halting the process of mutating the attribute's value, or can change the given value into something different. Validators, like all attribute extensions, are only called by normal userland code; they are not issued when the ORM is populating the object.

{python}
class MyAddress(object):
addresses_table = Table('addresses', metadata,
Column('id', Integer, primary_key=True),
Column('email', String)
)

class EmailAddress(object):
@validates('email')
def validate_email(self, key, address):
assert '@' in address
return address

mapper(EmailAddress, addresses_table)

Validators also receive collection events, when items are added to a collection:

{python}
class User(object):
@validates('addresses')
def validate_address(self, key, address):
assert '@' in address.email
return address

##### Using Descriptors {@name=overriding}

A more comprehensive way to produce modified behavior for an attribute is to use descriptors. These are commonly used in Python using the `property()` 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. To have the descriptor named the same as a column, map the column under a different name, i.e.:

{python}
class EmailAddress(object):
def _set_email(self, email):
self._email = email
def _get_email(self):
return self._email
email = property(_get_email, _set_email)

mapper(MyAddress, addresses_table, properties={
'_email': addresses_table.c.email
})

However, the approach above is not complete. While our `EmailAddress` object will shuttle the value through the `email` descriptor and into the `_email` mapped attribute, the class level `EmailAddress.email` attribute does not have the usual expression semantics usable with `Query`. To provide these, we instead use the `synonym()` function as follows:

{python}
mapper(MyAddress, addresses_table, properties={
'email': synonym('_email', map_column=True)
})
Expand Down
3 changes: 2 additions & 1 deletion examples/custom_attributes/listen_for_events.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""
Illustrates how to use AttributeExtension to listen for change events.
Illustrates how to use AttributeExtension to listen for change events
across the board.
"""

Expand Down
117 changes: 0 additions & 117 deletions examples/custom_attributes/validate_attributes.py

This file was deleted.

29 changes: 28 additions & 1 deletion lib/sqlalchemy/orm/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
)
from sqlalchemy.orm.util import (
AliasedClass as aliased,
Validator,
join,
object_mapper,
outerjoin,
Expand All @@ -44,7 +45,7 @@
SynonymProperty,
)
from sqlalchemy.orm import mapper as mapperlib
from sqlalchemy.orm.mapper import reconstructor
from sqlalchemy.orm.mapper import reconstructor, validates
from sqlalchemy.orm import strategies
from sqlalchemy.orm.query import AliasOption, Query
from sqlalchemy.sql import util as sql_util
Expand All @@ -59,6 +60,7 @@
'EXT_STOP',
'InstrumentationManager',
'MapperExtension',
'Validator',
'PropComparator',
'Query',
'aliased',
Expand Down Expand Up @@ -91,6 +93,7 @@
'synonym',
'undefer',
'undefer_group',
'validates'
)


Expand Down Expand Up @@ -206,6 +209,14 @@ class that will handle this relationship in the other direction,
a class or function that returns a new list-holding object. will be
used in place of a plain list for storing elements.
extension
an [sqlalchemy.orm.interfaces#AttributeExtension] instance,
or list of extensions, which will be prepended to the list of
attribute listeners for the resulting descriptor placed on the class.
These listeners will receive append and set events before the
operation proceeds, and may be used to halt (via exception throw)
or change the value used in the operation.
foreign_keys
a list of columns which are to be used as "foreign key" columns.
this parameter should be used in conjunction with explicit
Expand Down Expand Up @@ -396,6 +407,14 @@ def column_property(*args, **kwargs):
attribute is first accessed on an instance. See also
[sqlalchemy.orm#deferred()].
extension
an [sqlalchemy.orm.interfaces#AttributeExtension] instance,
or list of extensions, which will be prepended to the list of
attribute listeners for the resulting descriptor placed on the class.
These listeners will receive append and set events before the
operation proceeds, and may be used to halt (via exception throw)
or change the value used in the operation.
"""

return ColumnProperty(*args, **kwargs)
Expand Down Expand Up @@ -461,6 +480,14 @@ def __eq__(self, other):
An optional instance of [sqlalchemy.orm#PropComparator] which provides
SQL expression generation functions for this composite type.
extension
an [sqlalchemy.orm.interfaces#AttributeExtension] instance,
or list of extensions, which will be prepended to the list of
attribute listeners for the resulting descriptor placed on the class.
These listeners will receive append and set events before the
operation proceeds, and may be used to halt (via exception throw)
or change the value used in the operation.
"""
return CompositeProperty(class_, *cols, **kwargs)

Expand Down
30 changes: 24 additions & 6 deletions lib/sqlalchemy/orm/mapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ def __init__(self,
self.column_prefix = column_prefix
self.polymorphic_on = polymorphic_on
self._dependency_processors = []
self._validators = {}
self._clause_adapter = None
self._requires_row_aliasing = False
self.__inherits_equated_pairs = None
Expand Down Expand Up @@ -868,11 +869,13 @@ def __compile_class(self):
event_registry.add_listener('on_init', _event_on_init)
event_registry.add_listener('on_init_failure', _event_on_init_failure)
for key, method in util.iterate_attributes(self.class_):
if (isinstance(method, types.FunctionType) and
hasattr(method, '__sa_reconstructor__')):
event_registry.add_listener('on_load', method)
break

if isinstance(method, types.FunctionType):
if hasattr(method, '__sa_reconstructor__'):
event_registry.add_listener('on_load', method)
elif hasattr(method, '__sa_validators__'):
for name in method.__sa_validators__:
self._validators[name] = method

if 'reconstruct_instance' in self.extension.methods:
def reconstruct(instance):
self.extension.reconstruct_instance(self, instance)
Expand Down Expand Up @@ -1652,7 +1655,22 @@ def reconstructor(fn):
fn.__sa_reconstructor__ = True
return fn


def validates(*names):
"""Decorate a method as a 'validator' for one or more named properties.
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.
"""
def wrap(fn):
fn.__sa_validators__ = names
return fn
return wrap

def _event_on_init(state, instance, args, kwargs):
"""Trigger mapper compilation and run init_instance hooks."""

Expand Down
11 changes: 9 additions & 2 deletions lib/sqlalchemy/orm/properties.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ def __init__(self, *columns, **kwargs):
self.group = kwargs.pop('group', None)
self.deferred = kwargs.pop('deferred', False)
self.comparator_factory = kwargs.pop('comparator_factory', ColumnProperty.ColumnComparator)
self.extension = kwargs.pop('extension', None)
util.set_creation_order(self)
if self.deferred:
self.strategy_class = strategies.DeferredColumnLoader
Expand Down Expand Up @@ -100,7 +101,7 @@ def __str__(self):

class CompositeProperty(ColumnProperty):
"""subclasses ColumnProperty to provide composite type support."""

def __init__(self, class_, *columns, **kwargs):
super(CompositeProperty, self).__init__(*columns, **kwargs)
self._col_position_map = dict((c, i) for i, c in enumerate(columns))
Expand Down Expand Up @@ -161,6 +162,9 @@ def __str__(self):
return str(self.parent.class_.__name__) + "." + self.key

class SynonymProperty(MapperProperty):

extension = None

def __init__(self, name, map_column=None, descriptor=None, comparator_factory=None):
self.name = name
self.map_column = map_column
Expand Down Expand Up @@ -210,6 +214,8 @@ def merge(self, session, source, dest, dont_load, _recursive):
class ComparableProperty(MapperProperty):
"""Instruments a Python property for use in query expressions."""

extension = None

def __init__(self, comparator_factory, descriptor=None):
self.descriptor = descriptor
self.comparator_factory = comparator_factory
Expand Down Expand Up @@ -244,7 +250,7 @@ def __init__(self, argument,
backref=None,
_is_backref=False,
post_update=False,
cascade=False,
cascade=False, extension=None,
viewonly=False, lazy=True,
collection_class=None, passive_deletes=False,
passive_updates=True, remote_side=None,
Expand All @@ -269,6 +275,7 @@ def __init__(self, argument,
self.comparator = PropertyLoader.Comparator(self, None)
self.join_depth = join_depth
self.local_remote_pairs = _local_remote_pairs
self.extension = extension
self.__join_cache = {}
self.comparator_factory = PropertyLoader.Comparator
util.set_creation_order(self)
Expand Down
Loading

0 comments on commit 3829b89

Please sign in to comment.