1# orm/dynamic.py 2# Copyright (C) 2005-2018 the SQLAlchemy authors and contributors 3# <see AUTHORS file> 4# 5# This module is part of SQLAlchemy and is released under 6# the MIT License: http://www.opensource.org/licenses/mit-license.php 7 8"""Dynamic collection API. 9 10Dynamic collections act like Query() objects for read operations and support 11basic add/delete mutation. 12 13""" 14 15from .. import log, util, exc 16from ..sql import operators 17from . import ( 18 attributes, object_session, util as orm_util, strategies, 19 object_mapper, exc as orm_exc, properties 20) 21from .query import Query 22 23 24@log.class_logger 25@properties.RelationshipProperty.strategy_for(lazy="dynamic") 26class DynaLoader(strategies.AbstractRelationshipLoader): 27 def init_class_attribute(self, mapper): 28 self.is_class_level = True 29 if not self.uselist: 30 raise exc.InvalidRequestError( 31 "On relationship %s, 'dynamic' loaders cannot be used with " 32 "many-to-one/one-to-one relationships and/or " 33 "uselist=False." % self.parent_property) 34 strategies._register_attribute( 35 self.parent_property, 36 mapper, 37 useobject=True, 38 impl_class=DynamicAttributeImpl, 39 target_mapper=self.parent_property.mapper, 40 order_by=self.parent_property.order_by, 41 query_class=self.parent_property.query_class, 42 ) 43 44 45class DynamicAttributeImpl(attributes.AttributeImpl): 46 uses_objects = True 47 accepts_scalar_loader = False 48 supports_population = False 49 collection = False 50 51 def __init__(self, class_, key, typecallable, 52 dispatch, 53 target_mapper, order_by, query_class=None, **kw): 54 super(DynamicAttributeImpl, self).\ 55 __init__(class_, key, typecallable, dispatch, **kw) 56 self.target_mapper = target_mapper 57 self.order_by = order_by 58 if not query_class: 59 self.query_class = AppenderQuery 60 elif AppenderMixin in query_class.mro(): 61 self.query_class = query_class 62 else: 63 self.query_class = mixin_user_query(query_class) 64 65 def get(self, state, dict_, passive=attributes.PASSIVE_OFF): 66 if not passive & attributes.SQL_OK: 67 return self._get_collection_history( 68 state, attributes.PASSIVE_NO_INITIALIZE).added_items 69 else: 70 return self.query_class(self, state) 71 72 def get_collection(self, state, dict_, user_data=None, 73 passive=attributes.PASSIVE_NO_INITIALIZE): 74 if not passive & attributes.SQL_OK: 75 return self._get_collection_history(state, 76 passive).added_items 77 else: 78 history = self._get_collection_history(state, passive) 79 return history.added_plus_unchanged 80 81 @util.memoized_property 82 def _append_token(self): 83 return attributes.Event(self, attributes.OP_APPEND) 84 85 @util.memoized_property 86 def _remove_token(self): 87 return attributes.Event(self, attributes.OP_REMOVE) 88 89 def fire_append_event(self, state, dict_, value, initiator, 90 collection_history=None): 91 if collection_history is None: 92 collection_history = self._modified_event(state, dict_) 93 94 collection_history.add_added(value) 95 96 for fn in self.dispatch.append: 97 value = fn(state, value, initiator or self._append_token) 98 99 if self.trackparent and value is not None: 100 self.sethasparent(attributes.instance_state(value), state, True) 101 102 def fire_remove_event(self, state, dict_, value, initiator, 103 collection_history=None): 104 if collection_history is None: 105 collection_history = self._modified_event(state, dict_) 106 107 collection_history.add_removed(value) 108 109 if self.trackparent and value is not None: 110 self.sethasparent(attributes.instance_state(value), state, False) 111 112 for fn in self.dispatch.remove: 113 fn(state, value, initiator or self._remove_token) 114 115 def _modified_event(self, state, dict_): 116 117 if self.key not in state.committed_state: 118 state.committed_state[self.key] = CollectionHistory(self, state) 119 120 state._modified_event(dict_, 121 self, 122 attributes.NEVER_SET) 123 124 # this is a hack to allow the fixtures.ComparableEntity fixture 125 # to work 126 dict_[self.key] = True 127 return state.committed_state[self.key] 128 129 def set(self, state, dict_, value, initiator=None, 130 passive=attributes.PASSIVE_OFF, 131 check_old=None, pop=False, _adapt=True): 132 if initiator and initiator.parent_token is self.parent_token: 133 return 134 135 if pop and value is None: 136 return 137 138 iterable = value 139 new_values = list(iterable) 140 if state.has_identity: 141 old_collection = util.IdentitySet(self.get(state, dict_)) 142 143 collection_history = self._modified_event(state, dict_) 144 if not state.has_identity: 145 old_collection = collection_history.added_items 146 else: 147 old_collection = old_collection.union( 148 collection_history.added_items) 149 150 idset = util.IdentitySet 151 constants = old_collection.intersection(new_values) 152 additions = idset(new_values).difference(constants) 153 removals = old_collection.difference(constants) 154 155 for member in new_values: 156 if member in additions: 157 self.fire_append_event(state, dict_, member, None, 158 collection_history=collection_history) 159 160 for member in removals: 161 self.fire_remove_event(state, dict_, member, None, 162 collection_history=collection_history) 163 164 def delete(self, *args, **kwargs): 165 raise NotImplementedError() 166 167 def set_committed_value(self, state, dict_, value): 168 raise NotImplementedError("Dynamic attributes don't support " 169 "collection population.") 170 171 def get_history(self, state, dict_, passive=attributes.PASSIVE_OFF): 172 c = self._get_collection_history(state, passive) 173 return c.as_history() 174 175 def get_all_pending(self, state, dict_, 176 passive=attributes.PASSIVE_NO_INITIALIZE): 177 c = self._get_collection_history( 178 state, passive) 179 return [ 180 (attributes.instance_state(x), x) 181 for x in 182 c.all_items 183 ] 184 185 def _get_collection_history(self, state, passive=attributes.PASSIVE_OFF): 186 if self.key in state.committed_state: 187 c = state.committed_state[self.key] 188 else: 189 c = CollectionHistory(self, state) 190 191 if state.has_identity and (passive & attributes.INIT_OK): 192 return CollectionHistory(self, state, apply_to=c) 193 else: 194 return c 195 196 def append(self, state, dict_, value, initiator, 197 passive=attributes.PASSIVE_OFF): 198 if initiator is not self: 199 self.fire_append_event(state, dict_, value, initiator) 200 201 def remove(self, state, dict_, value, initiator, 202 passive=attributes.PASSIVE_OFF): 203 if initiator is not self: 204 self.fire_remove_event(state, dict_, value, initiator) 205 206 def pop(self, state, dict_, value, initiator, 207 passive=attributes.PASSIVE_OFF): 208 self.remove(state, dict_, value, initiator, passive=passive) 209 210 211class AppenderMixin(object): 212 query_class = None 213 214 def __init__(self, attr, state): 215 super(AppenderMixin, self).__init__(attr.target_mapper, None) 216 self.instance = instance = state.obj() 217 self.attr = attr 218 219 mapper = object_mapper(instance) 220 prop = mapper._props[self.attr.key] 221 self._criterion = prop._with_parent( 222 instance, 223 alias_secondary=False) 224 225 if self.attr.order_by: 226 self._order_by = self.attr.order_by 227 228 def session(self): 229 sess = object_session(self.instance) 230 if sess is not None and self.autoflush and sess.autoflush \ 231 and self.instance in sess: 232 sess.flush() 233 if not orm_util.has_identity(self.instance): 234 return None 235 else: 236 return sess 237 session = property(session, lambda s, x: None) 238 239 def __iter__(self): 240 sess = self.session 241 if sess is None: 242 return iter(self.attr._get_collection_history( 243 attributes.instance_state(self.instance), 244 attributes.PASSIVE_NO_INITIALIZE).added_items) 245 else: 246 return iter(self._clone(sess)) 247 248 def __getitem__(self, index): 249 sess = self.session 250 if sess is None: 251 return self.attr._get_collection_history( 252 attributes.instance_state(self.instance), 253 attributes.PASSIVE_NO_INITIALIZE).indexed(index) 254 else: 255 return self._clone(sess).__getitem__(index) 256 257 def count(self): 258 sess = self.session 259 if sess is None: 260 return len(self.attr._get_collection_history( 261 attributes.instance_state(self.instance), 262 attributes.PASSIVE_NO_INITIALIZE).added_items) 263 else: 264 return self._clone(sess).count() 265 266 def _clone(self, sess=None): 267 # note we're returning an entirely new Query class instance 268 # here without any assignment capabilities; the class of this 269 # query is determined by the session. 270 instance = self.instance 271 if sess is None: 272 sess = object_session(instance) 273 if sess is None: 274 raise orm_exc.DetachedInstanceError( 275 "Parent instance %s is not bound to a Session, and no " 276 "contextual session is established; lazy load operation " 277 "of attribute '%s' cannot proceed" % ( 278 orm_util.instance_str(instance), self.attr.key)) 279 280 if self.query_class: 281 query = self.query_class(self.attr.target_mapper, session=sess) 282 else: 283 query = sess.query(self.attr.target_mapper) 284 285 query._criterion = self._criterion 286 query._order_by = self._order_by 287 288 return query 289 290 def extend(self, iterator): 291 for item in iterator: 292 self.attr.append( 293 attributes.instance_state(self.instance), 294 attributes.instance_dict(self.instance), item, None) 295 296 def append(self, item): 297 self.attr.append( 298 attributes.instance_state(self.instance), 299 attributes.instance_dict(self.instance), item, None) 300 301 def remove(self, item): 302 self.attr.remove( 303 attributes.instance_state(self.instance), 304 attributes.instance_dict(self.instance), item, None) 305 306 307class AppenderQuery(AppenderMixin, Query): 308 """A dynamic query that supports basic collection storage operations.""" 309 310 311def mixin_user_query(cls): 312 """Return a new class with AppenderQuery functionality layered over.""" 313 name = 'Appender' + cls.__name__ 314 return type(name, (AppenderMixin, cls), {'query_class': cls}) 315 316 317class CollectionHistory(object): 318 """Overrides AttributeHistory to receive append/remove events directly.""" 319 320 def __init__(self, attr, state, apply_to=None): 321 if apply_to: 322 coll = AppenderQuery(attr, state).autoflush(False) 323 self.unchanged_items = util.OrderedIdentitySet(coll) 324 self.added_items = apply_to.added_items 325 self.deleted_items = apply_to.deleted_items 326 self._reconcile_collection = True 327 else: 328 self.deleted_items = util.OrderedIdentitySet() 329 self.added_items = util.OrderedIdentitySet() 330 self.unchanged_items = util.OrderedIdentitySet() 331 self._reconcile_collection = False 332 333 @property 334 def added_plus_unchanged(self): 335 return list(self.added_items.union(self.unchanged_items)) 336 337 @property 338 def all_items(self): 339 return list(self.added_items.union( 340 self.unchanged_items).union(self.deleted_items)) 341 342 def as_history(self): 343 if self._reconcile_collection: 344 added = self.added_items.difference(self.unchanged_items) 345 deleted = self.deleted_items.intersection(self.unchanged_items) 346 unchanged = self.unchanged_items.difference(deleted) 347 else: 348 added, unchanged, deleted = self.added_items,\ 349 self.unchanged_items,\ 350 self.deleted_items 351 return attributes.History( 352 list(added), 353 list(unchanged), 354 list(deleted), 355 ) 356 357 def indexed(self, index): 358 return list(self.added_items)[index] 359 360 def add_added(self, value): 361 self.added_items.add(value) 362 363 def add_removed(self, value): 364 if value in self.added_items: 365 self.added_items.remove(value) 366 else: 367 self.deleted_items.add(value) 368