[3] | 1 | # dynamic.py |
---|
| 2 | # Copyright (C) the SQLAlchemy authors and contributors |
---|
| 3 | # |
---|
| 4 | # This module is part of SQLAlchemy and is released under |
---|
| 5 | # the MIT License: http://www.opensource.org/licenses/mit-license.php |
---|
| 6 | |
---|
| 7 | """Dynamic collection API. |
---|
| 8 | |
---|
| 9 | Dynamic collections act like Query() objects for read operations and support |
---|
| 10 | basic add/delete mutation. |
---|
| 11 | |
---|
| 12 | """ |
---|
| 13 | |
---|
| 14 | from sqlalchemy import log, util |
---|
| 15 | import sqlalchemy.exceptions as sa_exc |
---|
| 16 | |
---|
| 17 | from sqlalchemy.orm import ( |
---|
| 18 | attributes, object_session, util as mapperutil, strategies, |
---|
| 19 | ) |
---|
| 20 | from sqlalchemy.orm.query import Query |
---|
| 21 | from sqlalchemy.orm.util import _state_has_identity, has_identity |
---|
| 22 | from sqlalchemy.orm import attributes, collections |
---|
| 23 | |
---|
| 24 | class DynaLoader(strategies.AbstractRelationLoader): |
---|
| 25 | def init_class_attribute(self, mapper): |
---|
| 26 | self.is_class_level = True |
---|
| 27 | |
---|
| 28 | strategies._register_attribute(self, |
---|
| 29 | mapper, |
---|
| 30 | useobject=True, |
---|
| 31 | impl_class=DynamicAttributeImpl, |
---|
| 32 | target_mapper=self.parent_property.mapper, |
---|
| 33 | order_by=self.parent_property.order_by, |
---|
| 34 | query_class=self.parent_property.query_class |
---|
| 35 | ) |
---|
| 36 | |
---|
| 37 | def create_row_processor(self, selectcontext, path, mapper, row, adapter): |
---|
| 38 | return (None, None) |
---|
| 39 | |
---|
| 40 | log.class_logger(DynaLoader) |
---|
| 41 | |
---|
| 42 | class DynamicAttributeImpl(attributes.AttributeImpl): |
---|
| 43 | uses_objects = True |
---|
| 44 | accepts_scalar_loader = False |
---|
| 45 | |
---|
| 46 | def __init__(self, class_, key, typecallable, |
---|
| 47 | target_mapper, order_by, query_class=None, **kwargs): |
---|
| 48 | super(DynamicAttributeImpl, self).__init__(class_, key, typecallable, **kwargs) |
---|
| 49 | self.target_mapper = target_mapper |
---|
| 50 | self.order_by = order_by |
---|
| 51 | if not query_class: |
---|
| 52 | self.query_class = AppenderQuery |
---|
| 53 | elif AppenderMixin in query_class.mro(): |
---|
| 54 | self.query_class = query_class |
---|
| 55 | else: |
---|
| 56 | self.query_class = mixin_user_query(query_class) |
---|
| 57 | |
---|
| 58 | def get(self, state, dict_, passive=False): |
---|
| 59 | if passive: |
---|
| 60 | return self._get_collection_history(state, passive=True).added_items |
---|
| 61 | else: |
---|
| 62 | return self.query_class(self, state) |
---|
| 63 | |
---|
| 64 | def get_collection(self, state, dict_, user_data=None, passive=True): |
---|
| 65 | if passive: |
---|
| 66 | return self._get_collection_history(state, passive=passive).added_items |
---|
| 67 | else: |
---|
| 68 | history = self._get_collection_history(state, passive=passive) |
---|
| 69 | return history.added_items + history.unchanged_items |
---|
| 70 | |
---|
| 71 | def fire_append_event(self, state, dict_, value, initiator): |
---|
| 72 | collection_history = self._modified_event(state, dict_) |
---|
| 73 | collection_history.added_items.append(value) |
---|
| 74 | |
---|
| 75 | for ext in self.extensions: |
---|
| 76 | ext.append(state, value, initiator or self) |
---|
| 77 | |
---|
| 78 | if self.trackparent and value is not None: |
---|
| 79 | self.sethasparent(attributes.instance_state(value), True) |
---|
| 80 | |
---|
| 81 | def fire_remove_event(self, state, dict_, value, initiator): |
---|
| 82 | collection_history = self._modified_event(state, dict_) |
---|
| 83 | collection_history.deleted_items.append(value) |
---|
| 84 | |
---|
| 85 | if self.trackparent and value is not None: |
---|
| 86 | self.sethasparent(attributes.instance_state(value), False) |
---|
| 87 | |
---|
| 88 | for ext in self.extensions: |
---|
| 89 | ext.remove(state, value, initiator or self) |
---|
| 90 | |
---|
| 91 | def _modified_event(self, state, dict_): |
---|
| 92 | |
---|
| 93 | if self.key not in state.committed_state: |
---|
| 94 | state.committed_state[self.key] = CollectionHistory(self, state) |
---|
| 95 | |
---|
| 96 | state.modified_event(dict_, self, False, attributes.NEVER_SET, passive=attributes.PASSIVE_NO_INITIALIZE) |
---|
| 97 | |
---|
| 98 | # this is a hack to allow the _base.ComparableEntity fixture |
---|
| 99 | # to work |
---|
| 100 | dict_[self.key] = True |
---|
| 101 | return state.committed_state[self.key] |
---|
| 102 | |
---|
| 103 | def set(self, state, dict_, value, initiator, passive=attributes.PASSIVE_OFF): |
---|
| 104 | if initiator is self: |
---|
| 105 | return |
---|
| 106 | |
---|
| 107 | self._set_iterable(state, dict_, value) |
---|
| 108 | |
---|
| 109 | def _set_iterable(self, state, dict_, iterable, adapter=None): |
---|
| 110 | |
---|
| 111 | collection_history = self._modified_event(state, dict_) |
---|
| 112 | new_values = list(iterable) |
---|
| 113 | |
---|
| 114 | if _state_has_identity(state): |
---|
| 115 | old_collection = list(self.get(state, dict_)) |
---|
| 116 | else: |
---|
| 117 | old_collection = [] |
---|
| 118 | |
---|
| 119 | collections.bulk_replace(new_values, DynCollectionAdapter(self, state, old_collection), DynCollectionAdapter(self, state, new_values)) |
---|
| 120 | |
---|
| 121 | def delete(self, *args, **kwargs): |
---|
| 122 | raise NotImplementedError() |
---|
| 123 | |
---|
| 124 | def get_history(self, state, dict_, passive=False): |
---|
| 125 | c = self._get_collection_history(state, passive) |
---|
| 126 | return attributes.History(c.added_items, c.unchanged_items, c.deleted_items) |
---|
| 127 | |
---|
| 128 | def _get_collection_history(self, state, passive=False): |
---|
| 129 | if self.key in state.committed_state: |
---|
| 130 | c = state.committed_state[self.key] |
---|
| 131 | else: |
---|
| 132 | c = CollectionHistory(self, state) |
---|
| 133 | |
---|
| 134 | if not passive: |
---|
| 135 | return CollectionHistory(self, state, apply_to=c) |
---|
| 136 | else: |
---|
| 137 | return c |
---|
| 138 | |
---|
| 139 | def append(self, state, dict_, value, initiator, passive=False): |
---|
| 140 | if initiator is not self: |
---|
| 141 | self.fire_append_event(state, dict_, value, initiator) |
---|
| 142 | |
---|
| 143 | def remove(self, state, dict_, value, initiator, passive=False): |
---|
| 144 | if initiator is not self: |
---|
| 145 | self.fire_remove_event(state, dict_, value, initiator) |
---|
| 146 | |
---|
| 147 | class DynCollectionAdapter(object): |
---|
| 148 | """the dynamic analogue to orm.collections.CollectionAdapter""" |
---|
| 149 | |
---|
| 150 | def __init__(self, attr, owner_state, data): |
---|
| 151 | self.attr = attr |
---|
| 152 | self.state = owner_state |
---|
| 153 | self.data = data |
---|
| 154 | |
---|
| 155 | def __iter__(self): |
---|
| 156 | return iter(self.data) |
---|
| 157 | |
---|
| 158 | def append_with_event(self, item, initiator=None): |
---|
| 159 | self.attr.append(self.state, self.state.dict, item, initiator) |
---|
| 160 | |
---|
| 161 | def remove_with_event(self, item, initiator=None): |
---|
| 162 | self.attr.remove(self.state, self.state.dict, item, initiator) |
---|
| 163 | |
---|
| 164 | def append_without_event(self, item): |
---|
| 165 | pass |
---|
| 166 | |
---|
| 167 | def remove_without_event(self, item): |
---|
| 168 | pass |
---|
| 169 | |
---|
| 170 | class AppenderMixin(object): |
---|
| 171 | query_class = None |
---|
| 172 | |
---|
| 173 | def __init__(self, attr, state): |
---|
| 174 | Query.__init__(self, attr.target_mapper, None) |
---|
| 175 | self.instance = state.obj() |
---|
| 176 | self.attr = attr |
---|
| 177 | |
---|
| 178 | def __session(self): |
---|
| 179 | sess = object_session(self.instance) |
---|
| 180 | if sess is not None and self.autoflush and sess.autoflush and self.instance in sess: |
---|
| 181 | sess.flush() |
---|
| 182 | if not has_identity(self.instance): |
---|
| 183 | return None |
---|
| 184 | else: |
---|
| 185 | return sess |
---|
| 186 | |
---|
| 187 | def session(self): |
---|
| 188 | return self.__session() |
---|
| 189 | session = property(session, lambda s, x:None) |
---|
| 190 | |
---|
| 191 | def __iter__(self): |
---|
| 192 | sess = self.__session() |
---|
| 193 | if sess is None: |
---|
| 194 | return iter(self.attr._get_collection_history( |
---|
| 195 | attributes.instance_state(self.instance), |
---|
| 196 | passive=True).added_items) |
---|
| 197 | else: |
---|
| 198 | return iter(self._clone(sess)) |
---|
| 199 | |
---|
| 200 | def __getitem__(self, index): |
---|
| 201 | sess = self.__session() |
---|
| 202 | if sess is None: |
---|
| 203 | return self.attr._get_collection_history( |
---|
| 204 | attributes.instance_state(self.instance), |
---|
| 205 | passive=True).added_items.__getitem__(index) |
---|
| 206 | else: |
---|
| 207 | return self._clone(sess).__getitem__(index) |
---|
| 208 | |
---|
| 209 | def count(self): |
---|
| 210 | sess = self.__session() |
---|
| 211 | if sess is None: |
---|
| 212 | return len(self.attr._get_collection_history( |
---|
| 213 | attributes.instance_state(self.instance), |
---|
| 214 | passive=True).added_items) |
---|
| 215 | else: |
---|
| 216 | return self._clone(sess).count() |
---|
| 217 | |
---|
| 218 | def _clone(self, sess=None): |
---|
| 219 | # note we're returning an entirely new Query class instance |
---|
| 220 | # here without any assignment capabilities; the class of this |
---|
| 221 | # query is determined by the session. |
---|
| 222 | instance = self.instance |
---|
| 223 | if sess is None: |
---|
| 224 | sess = object_session(instance) |
---|
| 225 | if sess is None: |
---|
| 226 | raise sa_exc.UnboundExecutionError( |
---|
| 227 | "Parent instance %s is not bound to a Session, and no " |
---|
| 228 | "contextual session is established; lazy load operation " |
---|
| 229 | "of attribute '%s' cannot proceed" % ( |
---|
| 230 | mapperutil.instance_str(instance), self.attr.key)) |
---|
| 231 | |
---|
| 232 | if self.query_class: |
---|
| 233 | query = self.query_class(self.attr.target_mapper, session=sess) |
---|
| 234 | else: |
---|
| 235 | query = sess.query(self.attr.target_mapper) |
---|
| 236 | query = query.with_parent(instance, self.attr.key) |
---|
| 237 | |
---|
| 238 | if self.attr.order_by: |
---|
| 239 | query = query.order_by(self.attr.order_by) |
---|
| 240 | return query |
---|
| 241 | |
---|
| 242 | def append(self, item): |
---|
| 243 | self.attr.append(attributes.instance_state(self.instance), attributes.instance_dict(self.instance), item, None) |
---|
| 244 | |
---|
| 245 | def remove(self, item): |
---|
| 246 | self.attr.remove(attributes.instance_state(self.instance), attributes.instance_dict(self.instance), item, None) |
---|
| 247 | |
---|
| 248 | |
---|
| 249 | class AppenderQuery(AppenderMixin, Query): |
---|
| 250 | """A dynamic query that supports basic collection storage operations.""" |
---|
| 251 | |
---|
| 252 | |
---|
| 253 | def mixin_user_query(cls): |
---|
| 254 | """Return a new class with AppenderQuery functionality layered over.""" |
---|
| 255 | name = 'Appender' + cls.__name__ |
---|
| 256 | return type(name, (AppenderMixin, cls), {'query_class': cls}) |
---|
| 257 | |
---|
| 258 | class CollectionHistory(object): |
---|
| 259 | """Overrides AttributeHistory to receive append/remove events directly.""" |
---|
| 260 | |
---|
| 261 | def __init__(self, attr, state, apply_to=None): |
---|
| 262 | if apply_to: |
---|
| 263 | deleted = util.IdentitySet(apply_to.deleted_items) |
---|
| 264 | added = apply_to.added_items |
---|
| 265 | coll = AppenderQuery(attr, state).autoflush(False) |
---|
| 266 | self.unchanged_items = [o for o in util.IdentitySet(coll) if o not in deleted] |
---|
| 267 | self.added_items = apply_to.added_items |
---|
| 268 | self.deleted_items = apply_to.deleted_items |
---|
| 269 | else: |
---|
| 270 | self.deleted_items = [] |
---|
| 271 | self.added_items = [] |
---|
| 272 | self.unchanged_items = [] |
---|
| 273 | |
---|