You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

423 lines
15 KiB

13 years ago
# orm/descriptor_props.py
12 years ago
# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors <see AUTHORS file>
13 years ago
#
# This module is part of SQLAlchemy and is released under
# the MIT License: http://www.opensource.org/licenses/mit-license.php
"""Descriptor properties are more "auxiliary" properties
that exist as configurational elements, but don't participate
as actively in the load/persist ORM loop.
"""
from sqlalchemy.orm.interfaces import \
MapperProperty, PropComparator, StrategizedProperty
from sqlalchemy.orm.mapper import _none_set
from sqlalchemy.orm import attributes, strategies
from sqlalchemy import util, sql, exc as sa_exc, event, schema
from sqlalchemy.sql import expression
properties = util.importlater('sqlalchemy.orm', 'properties')
class DescriptorProperty(MapperProperty):
12 years ago
""":class:`.MapperProperty` which proxies access to a
13 years ago
user-defined descriptor."""
doc = None
def instrument_class(self, mapper):
prop = self
class _ProxyImpl(object):
accepts_scalar_loader = False
expire_missing = True
def __init__(self, key):
self.key = key
if hasattr(prop, 'get_history'):
12 years ago
def get_history(self, state, dict_,
13 years ago
passive=attributes.PASSIVE_OFF):
return prop.get_history(state, dict_, passive)
if self.descriptor is None:
desc = getattr(mapper.class_, self.key, None)
if mapper._is_userland_descriptor(desc):
self.descriptor = desc
if self.descriptor is None:
def fset(obj, value):
setattr(obj, self.name, value)
def fdel(obj):
delattr(obj, self.name)
def fget(obj):
return getattr(obj, self.name)
self.descriptor = property(
fget=fget,
fset=fset,
fdel=fdel,
)
proxy_attr = attributes.\
create_proxied_attribute(self.descriptor)\
(
self.parent.class_,
12 years ago
self.key,
13 years ago
self.descriptor,
lambda: self._comparator_factory(mapper),
doc=self.doc
)
proxy_attr.impl = _ProxyImpl(self.key)
mapper.class_manager.instrument_attribute(self.key, proxy_attr)
class CompositeProperty(DescriptorProperty):
def __init__(self, class_, *attrs, **kwargs):
self.attrs = attrs
self.composite_class = class_
self.active_history = kwargs.get('active_history', False)
self.deferred = kwargs.get('deferred', False)
self.group = kwargs.get('group', None)
self.comparator_factory = kwargs.pop('comparator_factory',
self.__class__.Comparator)
util.set_creation_order(self)
self._create_descriptor()
def instrument_class(self, mapper):
super(CompositeProperty, self).instrument_class(mapper)
self._setup_event_handlers()
def do_init(self):
12 years ago
"""Initialization which occurs after the :class:`.CompositeProperty`
13 years ago
has been associated with its parent mapper.
"""
self._init_props()
self._setup_arguments_on_columns()
def _create_descriptor(self):
12 years ago
"""Create the Python descriptor that will serve as
13 years ago
the access point on instances of the mapped class.
"""
def fget(instance):
dict_ = attributes.instance_dict(instance)
state = attributes.instance_state(instance)
if self.key not in dict_:
# key not present. Iterate through related
# attributes, retrieve their values. This
# ensures they all load.
values = [getattr(instance, key) for key in self._attribute_keys]
# current expected behavior here is that the composite is
12 years ago
# created on access if the object is persistent or if
# col attributes have non-None. This would be better
13 years ago
# if the composite were created unconditionally,
# but that would be a behavioral change.
if self.key not in dict_ and (
12 years ago
state.key is not None or
13 years ago
not _none_set.issuperset(values)
):
dict_[self.key] = self.composite_class(*values)
state.manager.dispatch.refresh(state, None, [self.key])
return dict_.get(self.key, None)
def fset(instance, value):
dict_ = attributes.instance_dict(instance)
state = attributes.instance_state(instance)
attr = state.manager[self.key]
previous = dict_.get(self.key, attributes.NO_VALUE)
for fn in attr.dispatch.set:
value = fn(state, value, previous, attr.impl)
dict_[self.key] = value
if value is None:
for key in self._attribute_keys:
setattr(instance, key, None)
else:
for key, value in zip(
12 years ago
self._attribute_keys,
13 years ago
value.__composite_values__()):
setattr(instance, key, value)
def fdel(instance):
state = attributes.instance_state(instance)
dict_ = attributes.instance_dict(instance)
previous = dict_.pop(self.key, attributes.NO_VALUE)
attr = state.manager[self.key]
attr.dispatch.remove(state, previous, attr.impl)
for key in self._attribute_keys:
setattr(instance, key, None)
self.descriptor = property(fget, fset, fdel)
@util.memoized_property
def _comparable_elements(self):
return [
getattr(self.parent.class_, prop.key)
for prop in self.props
]
def _init_props(self):
self.props = props = []
for attr in self.attrs:
if isinstance(attr, basestring):
prop = self.parent.get_property(attr)
elif isinstance(attr, schema.Column):
prop = self.parent._columntoproperty[attr]
elif isinstance(attr, attributes.InstrumentedAttribute):
prop = attr.property
props.append(prop)
@property
def columns(self):
return [a for a in self.attrs if isinstance(a, schema.Column)]
def _setup_arguments_on_columns(self):
"""Propagate configuration arguments made on this composite
to the target columns, for those that apply.
"""
for prop in self.props:
prop.active_history = self.active_history
if self.deferred:
prop.deferred = self.deferred
prop.strategy_class = strategies.DeferredColumnLoader
prop.group = self.group
def _setup_event_handlers(self):
"""Establish events that populate/expire the composite attribute."""
def load_handler(state, *args):
dict_ = state.dict
if self.key in dict_:
return
# if column elements aren't loaded, skip.
12 years ago
# __get__() will initiate a load for those
13 years ago
# columns
for k in self._attribute_keys:
if k not in dict_:
return
#assert self.key not in dict_
dict_[self.key] = self.composite_class(
12 years ago
*[state.dict[key] for key in
13 years ago
self._attribute_keys]
)
def expire_handler(state, keys):
if keys is None or set(self._attribute_keys).intersection(keys):
state.dict.pop(self.key, None)
def insert_update_handler(mapper, connection, state):
"""After an insert or update, some columns may be expired due
to server side defaults, or re-populated due to client side
12 years ago
defaults. Pop out the composite value here so that it
13 years ago
recreates.
12 years ago
13 years ago
"""
state.dict.pop(self.key, None)
12 years ago
event.listen(self.parent, 'after_insert',
13 years ago
insert_update_handler, raw=True)
12 years ago
event.listen(self.parent, 'after_update',
13 years ago
insert_update_handler, raw=True)
event.listen(self.parent, 'load', load_handler, raw=True, propagate=True)
event.listen(self.parent, 'refresh', load_handler, raw=True, propagate=True)
event.listen(self.parent, "expire", expire_handler, raw=True, propagate=True)
# TODO: need a deserialize hook here
@util.memoized_property
def _attribute_keys(self):
return [
prop.key for prop in self.props
]
def get_history(self, state, dict_, passive=attributes.PASSIVE_OFF):
"""Provided for userland code that uses attributes.get_history()."""
added = []
deleted = []
has_history = False
for prop in self.props:
key = prop.key
hist = state.manager[key].impl.get_history(state, dict_)
if hist.has_changes():
has_history = True
non_deleted = hist.non_deleted()
if non_deleted:
added.extend(non_deleted)
else:
added.append(None)
if hist.deleted:
deleted.extend(hist.deleted)
else:
deleted.append(None)
if has_history:
return attributes.History(
[self.composite_class(*added)],
(),
[self.composite_class(*deleted)]
)
else:
return attributes.History(
(),[self.composite_class(*added)], ()
)
def _comparator_factory(self, mapper):
return self.comparator_factory(self)
class Comparator(PropComparator):
def __init__(self, prop, adapter=None):
self.prop = self.property = prop
self.adapter = adapter
def __clause_element__(self):
if self.adapter:
# TODO: test coverage for adapted composite comparison
return expression.ClauseList(
*[self.adapter(x) for x in self.prop._comparable_elements])
else:
return expression.ClauseList(*self.prop._comparable_elements)
__hash__ = None
def __eq__(self, other):
if other is None:
values = [None] * len(self.prop._comparable_elements)
else:
values = other.__composite_values__()
return sql.and_(
*[a==b for a, b in zip(self.prop._comparable_elements, values)])
def __ne__(self, other):
return sql.not_(self.__eq__(other))
def __str__(self):
return str(self.parent.class_.__name__) + "." + self.key
class ConcreteInheritedProperty(DescriptorProperty):
12 years ago
"""A 'do nothing' :class:`.MapperProperty` that disables
13 years ago
an attribute on a concrete subclass that is only present
on the inherited mapper, not the concrete classes' mapper.
Cases where this occurs include:
12 years ago
* When the superclass mapper is mapped against a
"polymorphic union", which includes all attributes from
13 years ago
all subclasses.
* When a relationship() is configured on an inherited mapper,
but not on the subclass mapper. Concrete mappers require
12 years ago
that relationship() is configured explicitly on each
subclass.
13 years ago
"""
def _comparator_factory(self, mapper):
comparator_callable = None
for m in self.parent.iterate_to_root():
p = m._props[self.key]
if not isinstance(p, ConcreteInheritedProperty):
comparator_callable = p.comparator_factory
break
return comparator_callable
def __init__(self):
def warn():
raise AttributeError("Concrete %s does not implement "
"attribute %r at the instance level. Add this "
12 years ago
"property explicitly to %s." %
13 years ago
(self.parent, self.key, self.parent))
class NoninheritedConcreteProp(object):
def __set__(s, obj, value):
warn()
def __delete__(s, obj):
warn()
def __get__(s, obj, owner):
if obj is None:
return self.descriptor
warn()
self.descriptor = NoninheritedConcreteProp()
class SynonymProperty(DescriptorProperty):
12 years ago
def __init__(self, name, map_column=None,
13 years ago
descriptor=None, comparator_factory=None,
doc=None):
self.name = name
self.map_column = map_column
self.descriptor = descriptor
self.comparator_factory = comparator_factory
self.doc = doc or (descriptor and descriptor.__doc__) or None
util.set_creation_order(self)
# TODO: when initialized, check _proxied_property,
# emit a warning if its not a column-based property
@util.memoized_property
def _proxied_property(self):
return getattr(self.parent.class_, self.name).property
def _comparator_factory(self, mapper):
prop = self._proxied_property
if self.comparator_factory:
comp = self.comparator_factory(prop, mapper)
else:
comp = prop.comparator_factory(prop, mapper)
return comp
def set_parent(self, parent, init):
if self.map_column:
# implement the 'map_column' option.
if self.key not in parent.mapped_table.c:
raise sa_exc.ArgumentError(
"Can't compile synonym '%s': no column on table "
12 years ago
"'%s' named '%s'"
13 years ago
% (self.name, parent.mapped_table.description, self.key))
elif parent.mapped_table.c[self.key] in \
parent._columntoproperty and \
parent._columntoproperty[
parent.mapped_table.c[self.key]
].key == self.name:
raise sa_exc.ArgumentError(
"Can't call map_column=True for synonym %r=%r, "
"a ColumnProperty already exists keyed to the name "
12 years ago
"%r for column %r" %
13 years ago
(self.key, self.name, self.name, self.key)
)
p = properties.ColumnProperty(parent.mapped_table.c[self.key])
parent._configure_property(
12 years ago
self.name, p,
init=init,
13 years ago
setparent=True)
p._mapped_by_synonym = self.key
self.parent = parent
class ComparableProperty(DescriptorProperty):
"""Instruments a Python property for use in query expressions."""
def __init__(self, comparator_factory, descriptor=None, doc=None):
self.descriptor = descriptor
self.comparator_factory = comparator_factory
self.doc = doc or (descriptor and descriptor.__doc__) or None
util.set_creation_order(self)
def _comparator_factory(self, mapper):
return self.comparator_factory(self, mapper)