[Sqlalchemy-commits] sqlalchemy: - crudely, this replaces CompositeProperty's base to be
Brought to you by:
zzzeek
From: <co...@sq...> - 2010-12-20 21:54:00
|
details: http://hg.sqlalchemy.org/sqlalchemy/sqlalchemy/rev/98b8c844446e changeset: 7179:98b8c844446e user: zzzeek date: Mon Dec 20 13:47:48 2010 -0500 description: - crudely, this replaces CompositeProperty's base to be DescriptorProperty. We have to lose mutability (yikes composites were using mutable too !). Also the getter is not particularly efficient since it recreates the composite every time, probably want to stick it in __dict__. also rewrite the unit tests Subject: sqlalchemy: - migrate composites to its own test suite, break up tests into individual feature tests details: http://hg.sqlalchemy.org/sqlalchemy/sqlalchemy/rev/e3ec59015770 changeset: 7180:e3ec59015770 user: zzzeek date: Mon Dec 20 15:18:58 2010 -0500 description: - migrate composites to its own test suite, break up tests into individual feature tests Subject: sqlalchemy: - move the "descriptor" properties into a separate module. details: http://hg.sqlalchemy.org/sqlalchemy/sqlalchemy/rev/1a1c54e6f84b changeset: 7181:1a1c54e6f84b user: zzzeek date: Mon Dec 20 16:04:05 2010 -0500 description: - move the "descriptor" properties into a separate module. Subject: sqlalchemy: - clean it up a bit details: http://hg.sqlalchemy.org/sqlalchemy/sqlalchemy/rev/b3ebecbdb8d6 changeset: 7182:b3ebecbdb8d6 user: zzzeek date: Mon Dec 20 16:21:31 2010 -0500 description: - clean it up a bit - don't need __set_composite_values__() - break the bad news about mutations Subject: sqlalchemy: - with composites gone all the get_col_value crap is gone too details: http://hg.sqlalchemy.org/sqlalchemy/sqlalchemy/rev/8c8489a784cf changeset: 7183:8c8489a784cf user: zzzeek date: Mon Dec 20 16:41:26 2010 -0500 description: - with composites gone all the get_col_value crap is gone too diffstat: doc/build/orm/extensions/hybrid.rst | 2 + doc/build/orm/mapper_config.rst | 18 +- lib/sqlalchemy/orm/descriptor_props.py | 312 ++++++++++++++++++++++++++++++ lib/sqlalchemy/orm/interfaces.py | 15 - lib/sqlalchemy/orm/mapper.py | 48 +--- lib/sqlalchemy/orm/properties.py | 255 +------------------------ lib/sqlalchemy/orm/strategies.py | 53 ----- lib/sqlalchemy/orm/sync.py | 11 +- test/orm/test_composites.py | 337 +++++++++++++++++++++++++++++++++ test/orm/test_mapper.py | 300 +----------------------------- 10 files changed, 688 insertions(+), 663 deletions(-) diffs (truncated from 1544 to 300 lines): diff -r 0e489ba1b1be -r 8c8489a784cf doc/build/orm/extensions/hybrid.rst --- a/doc/build/orm/extensions/hybrid.rst Mon Dec 20 10:51:49 2010 -0500 +++ b/doc/build/orm/extensions/hybrid.rst Mon Dec 20 16:41:26 2010 -0500 @@ -1,3 +1,5 @@ +.. _hybrids_toplevel: + Hybrid Attributes ================= diff -r 0e489ba1b1be -r 8c8489a784cf doc/build/orm/mapper_config.rst --- a/doc/build/orm/mapper_config.rst Mon Dec 20 10:51:49 2010 -0500 +++ b/doc/build/orm/mapper_config.rst Mon Dec 20 16:41:26 2010 -0500 @@ -488,8 +488,15 @@ Composite Column Types ----------------------- -Sets of columns can be associated with a single user-defined datatype. The ORM provides a single attribute which represents the group of columns -using the class you provide. +Sets of columns can be associated with a single user-defined datatype. The ORM +provides a single attribute which represents the group of columns using the +class you provide. + +.. note:: + As of SQLAlchemy 0.7, composites are implemented as a simple wrapper using + the :ref:`hybrids_toplevel` feature. Note that composites no longer + "conceal" the underlying colunm based attributes, or support in-place + mutation. A simple example represents pairs of columns as a "Point" object. Starting with a table that represents two points as x1/y1 and x2/y2:: @@ -513,9 +520,6 @@ self.y = y def __composite_values__(self): return self.x, self.y - def __set_composite_values__(self, x, y): - self.x = x - self.y = y def __eq__(self, other): return other is not None and \ other.x == self.x and \ @@ -530,10 +534,6 @@ column-based attributes. It also should supply adequate ``__eq__()`` and ``__ne__()`` methods which test the equality of two instances. -The ``__set_composite_values__()`` method is optional. If it's not -provided, the names of the mapped columns are taken as the names of -attributes on the object, and ``setattr()`` is used to set data. - The :func:`.composite` function is then used in the mapping:: from sqlalchemy.orm import composite diff -r 0e489ba1b1be -r 8c8489a784cf lib/sqlalchemy/orm/descriptor_props.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/lib/sqlalchemy/orm/descriptor_props.py Mon Dec 20 16:41:26 2010 -0500 @@ -0,0 +1,312 @@ +# descriptor_props.py +# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010 Michael Bayer +# mi...@zz... +# +# This module is part of SQLAlchemy and is released under +# the MIT License: http://www.opensource.org/licenses/mit-license.php + +"""Descriptor proprerties are more "auxilliary" properties +that exist as configurational elements, but don't participate +as actively in the load/persist ORM loop. They all +build on the "hybrid" extension to produce class descriptors. + +""" + +from sqlalchemy.orm.interfaces import \ + MapperProperty, PropComparator, StrategizedProperty +from sqlalchemy.orm import attributes +from sqlalchemy import util, sql, exc as sa_exc +from sqlalchemy.sql import expression +properties = util.importlater('sqlalchemy.orm', 'properties') + +class DescriptorProperty(MapperProperty): + """:class:`MapperProperty` which proxies access to a + user-defined descriptor.""" + + def instrument_class(self, mapper): + from sqlalchemy.ext import hybrid + + prop = self + + # hackety hack hack + class _ProxyImpl(object): + accepts_scalar_loader = False + expire_missing = True + + def __init__(self, key): + self.key = key + + if hasattr(prop, 'get_history'): + def get_history(self, state, dict_, **kw): + return prop.get_history(state, dict_, **kw) + + 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) + fget.__doc__ = self.doc + + descriptor = hybrid.property_( + fget=fget, + fset=fset, + fdel=fdel, + ) + elif isinstance(self.descriptor, property): + descriptor = hybrid.property_( + fget=self.descriptor.fget, + fset=self.descriptor.fset, + fdel=self.descriptor.fdel, + ) + else: + descriptor = hybrid.property_( + fget=self.descriptor.__get__, + fset=self.descriptor.__set__, + fdel=self.descriptor.__delete__, + ) + + proxy_attr = attributes.\ + create_proxied_attribute(self.descriptor or descriptor)\ + ( + self.key, + self.descriptor or descriptor, + lambda: self._comparator_factory(mapper) + ) + def get_comparator(owner): + return util.update_wrapper(proxy_attr, descriptor) + descriptor.expr = get_comparator + descriptor.impl = _ProxyImpl(self.key) + mapper.class_manager.instrument_attribute(self.key, descriptor) + + def setup(self, context, entity, path, adapter, **kwargs): + pass + + def create_row_processor(self, selectcontext, path, mapper, row, adapter): + return None, None, None + + def merge(self, session, source_state, source_dict, + dest_state, dest_dict, load, _recursive): + pass + +class CompositeProperty(DescriptorProperty): + + def __init__(self, class_, *columns, **kwargs): + self.columns = columns + self.composite_class = class_ + self.active_history = kwargs.get('active_history', False) + self.deferred = kwargs.get('deferred', False) + self.group = kwargs.get('group', None) + + def fget(instance): + # this could be optimized to store the value in __dict__, + # but more complexity and tests would be needed to pick + # up on changes to the mapped columns made independently + # of those on the composite. + return self.composite_class( + *[getattr(instance, key) for key in self._attribute_keys] + ) + + def fset(instance, value): + if value is None: + fdel(instance) + else: + for key, value in zip(self._attribute_keys, value.__composite_values__()): + setattr(instance, key, value) + + def fdel(instance): + for key in self._attribute_keys: + setattr(instance, key, None) + self.descriptor = property(fget, fset, fdel) + + @util.memoized_property + def _attribute_keys(self): + return [ + self.parent._columntoproperty[col].key + for col in self.columns + ] + + def get_history(self, state, dict_, **kw): + """Provided for userland code that uses attributes.get_history().""" + + added = [] + deleted = [] + + has_history = False + for col in self.columns: + key = self.parent._columntoproperty[col].key + hist = state.manager[key].impl.get_history(state, dict_) + if hist.has_changes(): + has_history = True + + added.extend(hist.non_deleted()) + 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 do_init(self): + for col in self.columns: + prop = self.parent._columntoproperty[col] + prop.active_history = self.active_history + if self.deferred: + prop.deferred = self.deferred + prop.strategy_class = strategies.DeferredColumnLoader + prop.group = self.group + # strategies ... + + def _comparator_factory(self, mapper): + return CompositeProperty.Comparator(self) + + class Comparator(PropComparator): + def __init__(self, prop, adapter=None): + self.prop = 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.columns]) + else: + return expression.ClauseList(*self.prop.columns) + + __hash__ = None + + def __eq__(self, other): + if other is None: + values = [None] * len(self.prop.columns) + else: + values = other.__composite_values__() + return sql.and_( + *[a==b for a, b in zip(self.prop.columns, 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): + """A 'do nothing' :class:`MapperProperty` that disables + an attribute on a concrete subclass that is only present + on the inherited mapper, not the concrete classes' mapper. + + Cases where this occurs include: + + * When the superclass mapper is mapped against a + "polymorphic union", which includes all attributes from + all subclasses. + * When a relationship() is configured on an inherited mapper, + but not on the subclass mapper. Concrete mappers require + that relationship() is configured explicitly on each + subclass. + + """ + + 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 " + "property explicitly to %s." % + (self.parent, self.key, self.parent)) + + class NoninheritedConcreteProp(object): + def __set__(s, obj, value): + warn() + def __delete__(s, obj): + warn() |