Coverage for /home/martinb/.local/share/virtualenvs/camcops/lib/python3.6/site-packages/sqlalchemy/orm/dynamic.py : 29%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# orm/dynamic.py
2# Copyright (C) 2005-2020 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
8"""Dynamic collection API.
10Dynamic collections act like Query() objects for read operations and support
11basic add/delete mutation.
13"""
15from . import attributes
16from . import exc as orm_exc
17from . import interfaces
18from . import object_mapper
19from . import object_session
20from . import properties
21from . import strategies
22from . import util as orm_util
23from .query import Query
24from .. import exc
25from .. import log
26from .. import util
29@log.class_logger
30@properties.RelationshipProperty.strategy_for(lazy="dynamic")
31class DynaLoader(strategies.AbstractRelationshipLoader):
32 def init_class_attribute(self, mapper):
33 self.is_class_level = True
34 if not self.uselist:
35 raise exc.InvalidRequestError(
36 "On relationship %s, 'dynamic' loaders cannot be used with "
37 "many-to-one/one-to-one relationships and/or "
38 "uselist=False." % self.parent_property
39 )
40 elif self.parent_property.direction not in (
41 interfaces.ONETOMANY,
42 interfaces.MANYTOMANY,
43 ):
44 util.warn(
45 "On relationship %s, 'dynamic' loaders cannot be used with "
46 "many-to-one/one-to-one relationships and/or "
47 "uselist=False. This warning will be an exception in a "
48 "future release." % self.parent_property
49 )
51 strategies._register_attribute(
52 self.parent_property,
53 mapper,
54 useobject=True,
55 impl_class=DynamicAttributeImpl,
56 target_mapper=self.parent_property.mapper,
57 order_by=self.parent_property.order_by,
58 query_class=self.parent_property.query_class,
59 )
62class DynamicAttributeImpl(attributes.AttributeImpl):
63 uses_objects = True
64 default_accepts_scalar_loader = False
65 supports_population = False
66 collection = False
67 dynamic = True
69 def __init__(
70 self,
71 class_,
72 key,
73 typecallable,
74 dispatch,
75 target_mapper,
76 order_by,
77 query_class=None,
78 **kw
79 ):
80 super(DynamicAttributeImpl, self).__init__(
81 class_, key, typecallable, dispatch, **kw
82 )
83 self.target_mapper = target_mapper
84 self.order_by = order_by
85 if not query_class:
86 self.query_class = AppenderQuery
87 elif AppenderMixin in query_class.mro():
88 self.query_class = query_class
89 else:
90 self.query_class = mixin_user_query(query_class)
92 def get(self, state, dict_, passive=attributes.PASSIVE_OFF):
93 if not passive & attributes.SQL_OK:
94 return self._get_collection_history(
95 state, attributes.PASSIVE_NO_INITIALIZE
96 ).added_items
97 else:
98 return self.query_class(self, state)
100 def get_collection(
101 self,
102 state,
103 dict_,
104 user_data=None,
105 passive=attributes.PASSIVE_NO_INITIALIZE,
106 ):
107 if not passive & attributes.SQL_OK:
108 return self._get_collection_history(state, passive).added_items
109 else:
110 history = self._get_collection_history(state, passive)
111 return history.added_plus_unchanged
113 @util.memoized_property
114 def _append_token(self):
115 return attributes.Event(self, attributes.OP_APPEND)
117 @util.memoized_property
118 def _remove_token(self):
119 return attributes.Event(self, attributes.OP_REMOVE)
121 def fire_append_event(
122 self, state, dict_, value, initiator, collection_history=None
123 ):
124 if collection_history is None:
125 collection_history = self._modified_event(state, dict_)
127 collection_history.add_added(value)
129 for fn in self.dispatch.append:
130 value = fn(state, value, initiator or self._append_token)
132 if self.trackparent and value is not None:
133 self.sethasparent(attributes.instance_state(value), state, True)
135 def fire_remove_event(
136 self, state, dict_, value, initiator, collection_history=None
137 ):
138 if collection_history is None:
139 collection_history = self._modified_event(state, dict_)
141 collection_history.add_removed(value)
143 if self.trackparent and value is not None:
144 self.sethasparent(attributes.instance_state(value), state, False)
146 for fn in self.dispatch.remove:
147 fn(state, value, initiator or self._remove_token)
149 def _modified_event(self, state, dict_):
151 if self.key not in state.committed_state:
152 state.committed_state[self.key] = CollectionHistory(self, state)
154 state._modified_event(dict_, self, attributes.NEVER_SET)
156 # this is a hack to allow the fixtures.ComparableEntity fixture
157 # to work
158 dict_[self.key] = True
159 return state.committed_state[self.key]
161 def set(
162 self,
163 state,
164 dict_,
165 value,
166 initiator=None,
167 passive=attributes.PASSIVE_OFF,
168 check_old=None,
169 pop=False,
170 _adapt=True,
171 ):
172 if initiator and initiator.parent_token is self.parent_token:
173 return
175 if pop and value is None:
176 return
178 iterable = value
179 new_values = list(iterable)
180 if state.has_identity:
181 old_collection = util.IdentitySet(self.get(state, dict_))
183 collection_history = self._modified_event(state, dict_)
184 if not state.has_identity:
185 old_collection = collection_history.added_items
186 else:
187 old_collection = old_collection.union(
188 collection_history.added_items
189 )
191 idset = util.IdentitySet
192 constants = old_collection.intersection(new_values)
193 additions = idset(new_values).difference(constants)
194 removals = old_collection.difference(constants)
196 for member in new_values:
197 if member in additions:
198 self.fire_append_event(
199 state,
200 dict_,
201 member,
202 None,
203 collection_history=collection_history,
204 )
206 for member in removals:
207 self.fire_remove_event(
208 state,
209 dict_,
210 member,
211 None,
212 collection_history=collection_history,
213 )
215 def delete(self, *args, **kwargs):
216 raise NotImplementedError()
218 def set_committed_value(self, state, dict_, value):
219 raise NotImplementedError(
220 "Dynamic attributes don't support " "collection population."
221 )
223 def get_history(self, state, dict_, passive=attributes.PASSIVE_OFF):
224 c = self._get_collection_history(state, passive)
225 return c.as_history()
227 def get_all_pending(
228 self, state, dict_, passive=attributes.PASSIVE_NO_INITIALIZE
229 ):
230 c = self._get_collection_history(state, passive)
231 return [(attributes.instance_state(x), x) for x in c.all_items]
233 def _get_collection_history(self, state, passive=attributes.PASSIVE_OFF):
234 if self.key in state.committed_state:
235 c = state.committed_state[self.key]
236 else:
237 c = CollectionHistory(self, state)
239 if state.has_identity and (passive & attributes.INIT_OK):
240 return CollectionHistory(self, state, apply_to=c)
241 else:
242 return c
244 def append(
245 self, state, dict_, value, initiator, passive=attributes.PASSIVE_OFF
246 ):
247 if initiator is not self:
248 self.fire_append_event(state, dict_, value, initiator)
250 def remove(
251 self, state, dict_, value, initiator, passive=attributes.PASSIVE_OFF
252 ):
253 if initiator is not self:
254 self.fire_remove_event(state, dict_, value, initiator)
256 def pop(
257 self, state, dict_, value, initiator, passive=attributes.PASSIVE_OFF
258 ):
259 self.remove(state, dict_, value, initiator, passive=passive)
262class AppenderMixin(object):
263 query_class = None
265 def __init__(self, attr, state):
266 super(AppenderMixin, self).__init__(attr.target_mapper, None)
267 self.instance = instance = state.obj()
268 self.attr = attr
270 mapper = object_mapper(instance)
271 prop = mapper._props[self.attr.key]
273 if prop.secondary is not None:
274 # this is a hack right now. The Query only knows how to
275 # make subsequent joins() without a given left-hand side
276 # from self._from_obj[0]. We need to ensure prop.secondary
277 # is in the FROM. So we purposly put the mapper selectable
278 # in _from_obj[0] to ensure a user-defined join() later on
279 # doesn't fail, and secondary is then in _from_obj[1].
280 self._from_obj = (prop.mapper.selectable, prop.secondary)
282 self._criterion = prop._with_parent(instance, alias_secondary=False)
284 if self.attr.order_by:
285 self._order_by = self.attr.order_by
287 def session(self):
288 sess = object_session(self.instance)
289 if (
290 sess is not None
291 and self.autoflush
292 and sess.autoflush
293 and self.instance in sess
294 ):
295 sess.flush()
296 if not orm_util.has_identity(self.instance):
297 return None
298 else:
299 return sess
301 session = property(session, lambda s, x: None)
303 def __iter__(self):
304 sess = self.session
305 if sess is None:
306 return iter(
307 self.attr._get_collection_history(
308 attributes.instance_state(self.instance),
309 attributes.PASSIVE_NO_INITIALIZE,
310 ).added_items
311 )
312 else:
313 return iter(self._clone(sess))
315 def __getitem__(self, index):
316 sess = self.session
317 if sess is None:
318 return self.attr._get_collection_history(
319 attributes.instance_state(self.instance),
320 attributes.PASSIVE_NO_INITIALIZE,
321 ).indexed(index)
322 else:
323 return self._clone(sess).__getitem__(index)
325 def count(self):
326 sess = self.session
327 if sess is None:
328 return len(
329 self.attr._get_collection_history(
330 attributes.instance_state(self.instance),
331 attributes.PASSIVE_NO_INITIALIZE,
332 ).added_items
333 )
334 else:
335 return self._clone(sess).count()
337 def _clone(self, sess=None):
338 # note we're returning an entirely new Query class instance
339 # here without any assignment capabilities; the class of this
340 # query is determined by the session.
341 instance = self.instance
342 if sess is None:
343 sess = object_session(instance)
344 if sess is None:
345 raise orm_exc.DetachedInstanceError(
346 "Parent instance %s is not bound to a Session, and no "
347 "contextual session is established; lazy load operation "
348 "of attribute '%s' cannot proceed"
349 % (orm_util.instance_str(instance), self.attr.key)
350 )
352 if self.query_class:
353 query = self.query_class(self.attr.target_mapper, session=sess)
354 else:
355 query = sess.query(self.attr.target_mapper)
357 query._criterion = self._criterion
358 query._from_obj = self._from_obj
359 query._order_by = self._order_by
361 return query
363 def extend(self, iterator):
364 for item in iterator:
365 self.attr.append(
366 attributes.instance_state(self.instance),
367 attributes.instance_dict(self.instance),
368 item,
369 None,
370 )
372 def append(self, item):
373 self.attr.append(
374 attributes.instance_state(self.instance),
375 attributes.instance_dict(self.instance),
376 item,
377 None,
378 )
380 def remove(self, item):
381 self.attr.remove(
382 attributes.instance_state(self.instance),
383 attributes.instance_dict(self.instance),
384 item,
385 None,
386 )
389class AppenderQuery(AppenderMixin, Query):
390 """A dynamic query that supports basic collection storage operations."""
393def mixin_user_query(cls):
394 """Return a new class with AppenderQuery functionality layered over."""
395 name = "Appender" + cls.__name__
396 return type(name, (AppenderMixin, cls), {"query_class": cls})
399class CollectionHistory(object):
400 """Overrides AttributeHistory to receive append/remove events directly."""
402 def __init__(self, attr, state, apply_to=None):
403 if apply_to:
404 coll = AppenderQuery(attr, state).autoflush(False)
405 self.unchanged_items = util.OrderedIdentitySet(coll)
406 self.added_items = apply_to.added_items
407 self.deleted_items = apply_to.deleted_items
408 self._reconcile_collection = True
409 else:
410 self.deleted_items = util.OrderedIdentitySet()
411 self.added_items = util.OrderedIdentitySet()
412 self.unchanged_items = util.OrderedIdentitySet()
413 self._reconcile_collection = False
415 @property
416 def added_plus_unchanged(self):
417 return list(self.added_items.union(self.unchanged_items))
419 @property
420 def all_items(self):
421 return list(
422 self.added_items.union(self.unchanged_items).union(
423 self.deleted_items
424 )
425 )
427 def as_history(self):
428 if self._reconcile_collection:
429 added = self.added_items.difference(self.unchanged_items)
430 deleted = self.deleted_items.intersection(self.unchanged_items)
431 unchanged = self.unchanged_items.difference(deleted)
432 else:
433 added, unchanged, deleted = (
434 self.added_items,
435 self.unchanged_items,
436 self.deleted_items,
437 )
438 return attributes.History(list(added), list(unchanged), list(deleted))
440 def indexed(self, index):
441 return list(self.added_items)[index]
443 def add_added(self, value):
444 self.added_items.add(value)
446 def add_removed(self, value):
447 if value in self.added_items:
448 self.added_items.remove(value)
449 else:
450 self.deleted_items.add(value)