Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/sqlalchemy/orm/dynamic.py: 32%
234 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-07 06:35 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-07 06:35 +0000
1# orm/dynamic.py
2# Copyright (C) 2005-2023 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: https://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 relationships
21from . import strategies
22from . import util as orm_util
23from .query import Query
24from .. import exc
25from .. import log
26from .. import util
27from ..engine import result
30@log.class_logger
31@relationships.RelationshipProperty.strategy_for(lazy="dynamic")
32class DynaLoader(strategies.AbstractRelationshipLoader):
33 def init_class_attribute(self, mapper):
34 self.is_class_level = True
35 if not self.uselist:
36 raise exc.InvalidRequestError(
37 "On relationship %s, 'dynamic' loaders cannot be used with "
38 "many-to-one/one-to-one relationships and/or "
39 "uselist=False." % self.parent_property
40 )
41 elif self.parent_property.direction not in (
42 interfaces.ONETOMANY,
43 interfaces.MANYTOMANY,
44 ):
45 util.warn(
46 "On relationship %s, 'dynamic' loaders cannot be used with "
47 "many-to-one/one-to-one relationships and/or "
48 "uselist=False. This warning will be an exception in a "
49 "future release." % self.parent_property
50 )
52 strategies._register_attribute(
53 self.parent_property,
54 mapper,
55 useobject=True,
56 impl_class=DynamicAttributeImpl,
57 target_mapper=self.parent_property.mapper,
58 order_by=self.parent_property.order_by,
59 query_class=self.parent_property.query_class,
60 )
63class DynamicAttributeImpl(attributes.AttributeImpl):
64 uses_objects = True
65 default_accepts_scalar_loader = False
66 supports_population = False
67 collection = False
68 dynamic = True
69 order_by = ()
71 def __init__(
72 self,
73 class_,
74 key,
75 typecallable,
76 dispatch,
77 target_mapper,
78 order_by,
79 query_class=None,
80 **kw
81 ):
82 super(DynamicAttributeImpl, self).__init__(
83 class_, key, typecallable, dispatch, **kw
84 )
85 self.target_mapper = target_mapper
86 if order_by:
87 self.order_by = tuple(order_by)
88 if not query_class:
89 self.query_class = AppenderQuery
90 elif AppenderMixin in query_class.mro():
91 self.query_class = query_class
92 else:
93 self.query_class = mixin_user_query(query_class)
95 def get(self, state, dict_, passive=attributes.PASSIVE_OFF):
96 if not passive & attributes.SQL_OK:
97 return self._get_collection_history(
98 state, attributes.PASSIVE_NO_INITIALIZE
99 ).added_items
100 else:
101 return self.query_class(self, state)
103 def get_collection(
104 self,
105 state,
106 dict_,
107 user_data=None,
108 passive=attributes.PASSIVE_NO_INITIALIZE,
109 ):
110 if not passive & attributes.SQL_OK:
111 data = self._get_collection_history(state, passive).added_items
112 else:
113 history = self._get_collection_history(state, passive)
114 data = history.added_plus_unchanged
115 return DynamicCollectionAdapter(data)
117 @util.memoized_property
118 def _append_token(self):
119 return attributes.Event(self, attributes.OP_APPEND)
121 @util.memoized_property
122 def _remove_token(self):
123 return attributes.Event(self, attributes.OP_REMOVE)
125 def fire_append_event(
126 self, state, dict_, value, initiator, collection_history=None
127 ):
128 if collection_history is None:
129 collection_history = self._modified_event(state, dict_)
131 collection_history.add_added(value)
133 for fn in self.dispatch.append:
134 value = fn(state, value, initiator or self._append_token)
136 if self.trackparent and value is not None:
137 self.sethasparent(attributes.instance_state(value), state, True)
139 def fire_remove_event(
140 self, state, dict_, value, initiator, collection_history=None
141 ):
142 if collection_history is None:
143 collection_history = self._modified_event(state, dict_)
145 collection_history.add_removed(value)
147 if self.trackparent and value is not None:
148 self.sethasparent(attributes.instance_state(value), state, False)
150 for fn in self.dispatch.remove:
151 fn(state, value, initiator or self._remove_token)
153 def _modified_event(self, state, dict_):
155 if self.key not in state.committed_state:
156 state.committed_state[self.key] = CollectionHistory(self, state)
158 state._modified_event(dict_, self, attributes.NEVER_SET)
160 # this is a hack to allow the fixtures.ComparableEntity fixture
161 # to work
162 dict_[self.key] = True
163 return state.committed_state[self.key]
165 def set(
166 self,
167 state,
168 dict_,
169 value,
170 initiator=None,
171 passive=attributes.PASSIVE_OFF,
172 check_old=None,
173 pop=False,
174 _adapt=True,
175 ):
176 if initiator and initiator.parent_token is self.parent_token:
177 return
179 if pop and value is None:
180 return
182 iterable = value
183 new_values = list(iterable)
184 if state.has_identity:
185 old_collection = util.IdentitySet(self.get(state, dict_))
187 collection_history = self._modified_event(state, dict_)
188 if not state.has_identity:
189 old_collection = collection_history.added_items
190 else:
191 old_collection = old_collection.union(
192 collection_history.added_items
193 )
195 idset = util.IdentitySet
196 constants = old_collection.intersection(new_values)
197 additions = idset(new_values).difference(constants)
198 removals = old_collection.difference(constants)
200 for member in new_values:
201 if member in additions:
202 self.fire_append_event(
203 state,
204 dict_,
205 member,
206 None,
207 collection_history=collection_history,
208 )
210 for member in removals:
211 self.fire_remove_event(
212 state,
213 dict_,
214 member,
215 None,
216 collection_history=collection_history,
217 )
219 def delete(self, *args, **kwargs):
220 raise NotImplementedError()
222 def set_committed_value(self, state, dict_, value):
223 raise NotImplementedError(
224 "Dynamic attributes don't support " "collection population."
225 )
227 def get_history(self, state, dict_, passive=attributes.PASSIVE_OFF):
228 c = self._get_collection_history(state, passive)
229 return c.as_history()
231 def get_all_pending(
232 self, state, dict_, passive=attributes.PASSIVE_NO_INITIALIZE
233 ):
234 c = self._get_collection_history(state, passive)
235 return [(attributes.instance_state(x), x) for x in c.all_items]
237 def _get_collection_history(self, state, passive=attributes.PASSIVE_OFF):
238 if self.key in state.committed_state:
239 c = state.committed_state[self.key]
240 else:
241 c = CollectionHistory(self, state)
243 if state.has_identity and (passive & attributes.INIT_OK):
244 return CollectionHistory(self, state, apply_to=c)
245 else:
246 return c
248 def append(
249 self, state, dict_, value, initiator, passive=attributes.PASSIVE_OFF
250 ):
251 if initiator is not self:
252 self.fire_append_event(state, dict_, value, initiator)
254 def remove(
255 self, state, dict_, value, initiator, passive=attributes.PASSIVE_OFF
256 ):
257 if initiator is not self:
258 self.fire_remove_event(state, dict_, value, initiator)
260 def pop(
261 self, state, dict_, value, initiator, passive=attributes.PASSIVE_OFF
262 ):
263 self.remove(state, dict_, value, initiator, passive=passive)
266class DynamicCollectionAdapter(object):
267 """simplified CollectionAdapter for internal API consistency"""
269 def __init__(self, data):
270 self.data = data
272 def __iter__(self):
273 return iter(self.data)
275 def _reset_empty(self):
276 pass
278 def __len__(self):
279 return len(self.data)
281 def __bool__(self):
282 return True
284 __nonzero__ = __bool__
287class AppenderMixin(object):
288 query_class = None
290 def __init__(self, attr, state):
291 super(AppenderMixin, self).__init__(attr.target_mapper, None)
292 self.instance = instance = state.obj()
293 self.attr = attr
295 mapper = object_mapper(instance)
296 prop = mapper._props[self.attr.key]
298 if prop.secondary is not None:
299 # this is a hack right now. The Query only knows how to
300 # make subsequent joins() without a given left-hand side
301 # from self._from_obj[0]. We need to ensure prop.secondary
302 # is in the FROM. So we purposely put the mapper selectable
303 # in _from_obj[0] to ensure a user-defined join() later on
304 # doesn't fail, and secondary is then in _from_obj[1].
306 # note also, we are using the official ORM-annotated selectable
307 # from __clause_element__(), see #7868
308 self._from_obj = (prop.mapper.__clause_element__(), prop.secondary)
310 self._where_criteria = (
311 prop._with_parent(instance, alias_secondary=False),
312 )
314 if self.attr.order_by:
315 self._order_by_clauses = self.attr.order_by
317 def session(self):
318 sess = object_session(self.instance)
319 if (
320 sess is not None
321 and self.autoflush
322 and sess.autoflush
323 and self.instance in sess
324 ):
325 sess.flush()
326 if not orm_util.has_identity(self.instance):
327 return None
328 else:
329 return sess
331 session = property(session, lambda s, x: None)
333 def _iter(self):
334 sess = self.session
335 if sess is None:
336 state = attributes.instance_state(self.instance)
337 if state.detached:
338 util.warn(
339 "Instance %s is detached, dynamic relationship cannot "
340 "return a correct result. This warning will become "
341 "a DetachedInstanceError in a future release."
342 % (orm_util.state_str(state))
343 )
345 return result.IteratorResult(
346 result.SimpleResultMetaData([self.attr.class_.__name__]),
347 self.attr._get_collection_history(
348 attributes.instance_state(self.instance),
349 attributes.PASSIVE_NO_INITIALIZE,
350 ).added_items,
351 _source_supports_scalars=True,
352 ).scalars()
353 else:
354 return self._generate(sess)._iter()
356 def __getitem__(self, index):
357 sess = self.session
358 if sess is None:
359 return self.attr._get_collection_history(
360 attributes.instance_state(self.instance),
361 attributes.PASSIVE_NO_INITIALIZE,
362 ).indexed(index)
363 else:
364 return self._generate(sess).__getitem__(index)
366 def count(self):
367 sess = self.session
368 if sess is None:
369 return len(
370 self.attr._get_collection_history(
371 attributes.instance_state(self.instance),
372 attributes.PASSIVE_NO_INITIALIZE,
373 ).added_items
374 )
375 else:
376 return self._generate(sess).count()
378 def _generate(self, sess=None):
379 # note we're returning an entirely new Query class instance
380 # here without any assignment capabilities; the class of this
381 # query is determined by the session.
382 instance = self.instance
383 if sess is None:
384 sess = object_session(instance)
385 if sess is None:
386 raise orm_exc.DetachedInstanceError(
387 "Parent instance %s is not bound to a Session, and no "
388 "contextual session is established; lazy load operation "
389 "of attribute '%s' cannot proceed"
390 % (orm_util.instance_str(instance), self.attr.key)
391 )
393 if self.query_class:
394 query = self.query_class(self.attr.target_mapper, session=sess)
395 else:
396 query = sess.query(self.attr.target_mapper)
398 query._where_criteria = self._where_criteria
399 query._from_obj = self._from_obj
400 query._order_by_clauses = self._order_by_clauses
402 return query
404 def extend(self, iterator):
405 for item in iterator:
406 self.attr.append(
407 attributes.instance_state(self.instance),
408 attributes.instance_dict(self.instance),
409 item,
410 None,
411 )
413 def append(self, item):
414 self.attr.append(
415 attributes.instance_state(self.instance),
416 attributes.instance_dict(self.instance),
417 item,
418 None,
419 )
421 def remove(self, item):
422 self.attr.remove(
423 attributes.instance_state(self.instance),
424 attributes.instance_dict(self.instance),
425 item,
426 None,
427 )
430class AppenderQuery(AppenderMixin, Query):
431 """A dynamic query that supports basic collection storage operations."""
434def mixin_user_query(cls):
435 """Return a new class with AppenderQuery functionality layered over."""
436 name = "Appender" + cls.__name__
437 return type(name, (AppenderMixin, cls), {"query_class": cls})
440class CollectionHistory(object):
441 """Overrides AttributeHistory to receive append/remove events directly."""
443 def __init__(self, attr, state, apply_to=None):
444 if apply_to:
445 coll = AppenderQuery(attr, state).autoflush(False)
446 self.unchanged_items = util.OrderedIdentitySet(coll)
447 self.added_items = apply_to.added_items
448 self.deleted_items = apply_to.deleted_items
449 self._reconcile_collection = True
450 else:
451 self.deleted_items = util.OrderedIdentitySet()
452 self.added_items = util.OrderedIdentitySet()
453 self.unchanged_items = util.OrderedIdentitySet()
454 self._reconcile_collection = False
456 @property
457 def added_plus_unchanged(self):
458 return list(self.added_items.union(self.unchanged_items))
460 @property
461 def all_items(self):
462 return list(
463 self.added_items.union(self.unchanged_items).union(
464 self.deleted_items
465 )
466 )
468 def as_history(self):
469 if self._reconcile_collection:
470 added = self.added_items.difference(self.unchanged_items)
471 deleted = self.deleted_items.intersection(self.unchanged_items)
472 unchanged = self.unchanged_items.difference(deleted)
473 else:
474 added, unchanged, deleted = (
475 self.added_items,
476 self.unchanged_items,
477 self.deleted_items,
478 )
479 return attributes.History(list(added), list(unchanged), list(deleted))
481 def indexed(self, index):
482 return list(self.added_items)[index]
484 def add_added(self, value):
485 self.added_items.add(value)
487 def add_removed(self, value):
488 if value in self.added_items:
489 self.added_items.remove(value)
490 else:
491 self.deleted_items.add(value)