synonyms for relationships might just be taken out altogether, since they aren't
documented and are of little use. a plain proxying descriptor, combined with
attribute-based usage with Query (as opposted to naming it by string)
can do the same thing more simply.
- add event support to composites, change the model around so that the composite
is generated at the point of load.
- add a recipe for tracking mutations on composites. will probably make both
of these mutations examples into extensions since they're intricate, should
have a lot of test coverage, and what they need to do is fairly straightforward.
Will use metaclasses so that no extra userland step is needed beyond usage
of the type.
--- /dev/null
+# this example is probably moving to be an extension.
+
+from sqlalchemy import event
+from sqlalchemy.orm import mapper, composite, object_mapper
+
+from sqlalchemy.util import memoized_property
+import weakref
+
+class _CompositeMutationsMixinMeta(type):
+ def __init__(cls, classname, bases, dict_):
+ cls._setup_listeners()
+ return type.__init__(cls, classname, bases, dict_)
+
+class CompositeMutationsMixin(object):
+ """Mixin that defines transparent propagation of change
+ events to a parent object.
+
+ This class might be moved to be a SQLA extension
+ due to its complexity and potential for widespread use.
+
+ """
+ __metaclass__ = _CompositeMutationsMixinMeta
+
+ @memoized_property
+ def _parents(self):
+ """Dictionary of parent object->attribute name on the parent."""
+
+ return weakref.WeakKeyDictionary()
+
+ def __setattr__(self, key, value):
+ object.__setattr__(self, key, value)
+ self.on_change()
+
+ def on_change(self):
+ """Subclasses should call this method whenever change events occur."""
+
+ for parent, key in self._parents.items():
+
+ prop = object_mapper(parent).get_property(key)
+ for value, attr_name in zip(self.__composite_values__(), prop._attribute_keys):
+ setattr(parent, attr_name, value)
+
+ @classmethod
+ def _listen_on_attribute(cls, attribute):
+ """Establish this type as a mutation listener for the given
+ mapped descriptor.
+
+ """
+ key = attribute.key
+ parent_cls = attribute.class_
+
+ def on_load(state):
+ """Listen for objects loaded or refreshed.
+
+ Wrap the target data member's value with
+ ``TrackMutationsMixin``.
+
+ """
+
+ val = state.dict.get(key, None)
+ if val is not None:
+ val._parents[state.obj()] = key
+
+ def on_set(target, value, oldvalue, initiator):
+ """Listen for set/replace events on the target
+ data member.
+
+ Establish a weak reference to the parent object
+ on the incoming value, remove it for the one
+ outgoing.
+
+ """
+
+ value._parents[target.obj()] = key
+ if isinstance(oldvalue, cls):
+ oldvalue._parents.pop(state.obj(), None)
+ return value
+
+ event.listen(parent_cls, 'on_load', on_load, raw=True)
+ event.listen(parent_cls, 'on_refresh', on_load, raw=True)
+ event.listen(attribute, 'on_set', on_set, raw=True, retval=True)
+
+ @classmethod
+ def _setup_listeners(cls):
+ """Associate this wrapper with all future mapped compoistes
+ of the given type.
+
+ This is a convenience method that calls ``associate_with_attribute`` automatically.
+
+ """
+
+ def listen_for_type(mapper, class_):
+ for prop in mapper.iterate_properties:
+ if hasattr(prop, 'composite_class') and issubclass(prop.composite_class, cls):
+ cls._listen_on_attribute(getattr(class_, prop.key))
+
+ event.listen(mapper, 'on_mapper_configured', listen_for_type)
+
+
+if __name__ == '__main__':
+ from sqlalchemy import Column, Integer, create_engine
+ from sqlalchemy.orm import Session
+ from sqlalchemy.ext.declarative import declarative_base
+
+ class Point(CompositeMutationsMixin):
+ def __init__(self, x, y):
+ self.x = x
+ self.y = y
+
+ def __composite_values__(self):
+ return self.x, self.y
+
+ def __eq__(self, other):
+ return isinstance(other, Point) and \
+ other.x == self.x and \
+ other.y == self.y
+
+ Base = declarative_base()
+ class Foo(Base):
+ __tablename__ = 'foo'
+ id = Column(Integer, primary_key=True)
+ data = composite(Point, Column('x', Integer), Column('y', Integer))
+
+ e = create_engine('sqlite://', echo=True)
+
+ Base.metadata.create_all(e)
+
+ sess = Session(e)
+ d = Point(3, 4)
+ f1 = Foo(data=d)
+ sess.add(f1)
+ sess.commit()
+
+ f1.data.y = 5
+ sess.commit()
+
+ assert f1.data == Point(3, 5)
+
+
\ No newline at end of file
+# this example is probably moving to be an extension.
+
from sqlalchemy.orm.attributes import flag_modified
from sqlalchemy import event
from sqlalchemy.orm import mapper
"""Mixin that defines transparent propagation of change
events to a parent object.
+ This class might be moved to be a SQLA extension
+ due to its complexity and potential for widespread use.
+
"""
@memoized_property
def _parents(self):
def listen_for_type(mapper, class_):
for prop in mapper.iterate_properties:
if hasattr(prop, 'columns') and isinstance(prop.columns[0].type, type_):
- cls.listen(getattr(class_, prop.key))
+ cls.associate_with_attribute(getattr(class_, prop.key))
event.listen(mapper, 'on_mapper_configured', listen_for_type)
def __delitem__(self, key):
dict.__delitem__(self, key)
self.on_change()
-
+
+ # TODO: do the metaclass approach the same as composite
MutationDict.associate_with_type(JSONEncodedDict)
Base = declarative_base()
lambda: self._comparator_factory(mapper),
doc=self.doc
)
-
+ proxy_attr.property = self
proxy_attr.impl = _ProxyImpl(self.key)
mapper.class_manager.instrument_attribute(self.key, proxy_attr)
self.group = kwargs.get('group', None)
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):
"""Initialization which occurs after the :class:`.CompositeProperty`
"""
self._setup_arguments_on_columns()
- self._setup_event_handlers()
def _create_descriptor(self):
- """Create the actual Python descriptor that will serve as
- the access point on the mapped class.
+ """Create the Python descriptor that will serve as
+ the access point on instances of the mapped class.
"""
def fget(instance):
dict_ = attributes.instance_dict(instance)
- if self.key in dict_:
- return dict_[self.key]
- else:
- dict_[self.key] = composite = self.composite_class(
- *[getattr(instance, key) for key in self._attribute_keys]
- )
- return composite
+
+ # key not present, assume the columns aren't
+ # loaded. The load events will establish
+ # the item.
+ if self.key not in dict_:
+ for key in self._attribute_keys:
+ getattr(instance, 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.on_set:
+ value = fn(state, value, previous, attr.impl)
+ dict_[self.key] = value
if value is None:
- fdel(instance)
+ for key in self._attribute_keys:
+ setattr(instance, key, None)
else:
- dict_ = attributes.instance_dict(instance)
- dict_[self.key] = value
for key, value in zip(
self._attribute_keys,
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.on_remove(state, previous, attr.impl)
for key in self._attribute_keys:
setattr(instance, key, None)
self.descriptor = property(fget, fset, fdel)
def _setup_arguments_on_columns(self):
- """Propigate configuration arguments made on this composite
+ """Propagate configuration arguments made on this composite
to the target columns, for those that apply.
"""
prop.group = self.group
def _setup_event_handlers(self):
- """Establish events that will clear out the composite value
- whenever changes in state occur on the target columns.
+ """Establish events that populate/expire the composite attribute."""
- """
def load_handler(state):
- state.dict.pop(self.key, None)
+ dict_ = state.dict
+
+ if self.key in dict_:
+ return
+
+ # if column elements aren't loaded, skip.
+ # __get__() will initiate a load for those
+ # columns
+ for k in self._attribute_keys:
+ if k not in dict_:
+ return
+
+ dict_[self.key] = self.composite_class(
+ *[state.dict[key] for key in
+ 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):
- state.dict.pop(self.key, None)
+ state.dict[self.key] = self.composite_class(
+ *[state.dict.get(key, None) for key in
+ self._attribute_keys]
+ )
event.listen(self.parent, 'on_after_insert',
insert_update_handler, raw=True)
event.listen(self.parent, 'on_refresh', load_handler, raw=True)
event.listen(self.parent, "on_expire", expire_handler, raw=True)
- # TODO: add listeners to the column attributes, which
- # refresh the composite based on userland settings.
-
- # TODO: add a callable to the composite of the form
- # _on_change(self, attrname) which will send up a corresponding
- # refresh to the column attribute on all parents. Basically
- # a specialization of the scalars.py example.
-
@util.memoized_property
def _attribute_keys(self):
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 = getattr(mapper.class_, self.name).property
+ prop = self._proxied_property
if self.comparator_factory:
comp = self.comparator_factory(prop, mapper)
pass
- def compare(self, operator, value):
+ def compare(self, operator, value, **kw):
"""Return a compare operation for the columns represented by
this ``MapperProperty`` to the given value, which may be a
column value or an instance. 'operator' is an operator from
def get_property(self, key, _compile_mappers=True):
"""return a MapperProperty associated with the given key.
-
- Calls getattr() against the mapped class itself, so that class-level
- proxies will be resolved to the underlying property, if any.
-
"""
if _compile_mappers and _new_mappers:
configure_mappers()
try:
- return getattr(self.class_, key).property
- except AttributeError:
+ return self._props[key]
+ except KeyError:
raise sa_exc.InvalidRequestError(
"Mapper '%s' has no property '%s'" % (self, key))
class Graph(_base.BasicEntity):
pass
class Edge(_base.BasicEntity):
- def __init__(self, start, end):
- self.start = start
- self.end = end
+ def __init__(self, *args):
+ if args:
+ self.start, self.end = args
mapper(Graph, graphs, properties={
'edges':relationship(Edge)
assert g2.edges[-1].start.x is None
assert g2.edges[-1].start.y is None
+ @testing.resolve_artifact_names
+ def test_expire(self):
+ sess = self._fixture()
+ g = sess.query(Graph).first()
+ e = g.edges[0]
+ sess.expire(e)
+ assert 'start' not in e.__dict__
+ assert e.start == Point(3, 4)
+
+ @testing.resolve_artifact_names
+ def test_default_value(self):
+ e = Edge()
+ eq_(e.start, None)
+
class PrimaryKeyTest(_base.MappedTest):
@classmethod
def define_tables(cls, metadata):
class OptionsTest(_fixtures.FixtureTest):
-
+
+ @testing.fails_if(lambda: True, "0.7 regression, may not support "
+ "synonyms for relationship")
@testing.fails_on('maxdb', 'FIXME: unknown')
@testing.resolve_artifact_names
def test_synonym_options(self):
addresses = relationship(mapper(Address, addresses), lazy='select',
order_by=addresses.c.id),
adlist = synonym('addresses')))
-
-
+
def go():
sess = create_session()
u = (sess.query(User).
})
mapper(Keyword, keywords)
+ @testing.fails_if(lambda: True, "0.7 regression, may not support "
+ "synonyms for relationship")
def test_joins(self):
for j in (
['orders', 'items'],
result = create_session().query(User).join(*j).filter_by(id=3).all()
assert [User(id=7, name='jack'), User(id=9, name='fred')] == result
+ @testing.fails_if(lambda: True, "0.7 regression, may not support "
+ "synonyms for relationship")
def test_with_parent(self):
for nameprop, orderprop in (
('name', 'orders'),