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

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 

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 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 

28 

29 

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 ) 

51 

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 ) 

61 

62 

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 = () 

70 

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) 

94 

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) 

102 

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) 

116 

117 @util.memoized_property 

118 def _append_token(self): 

119 return attributes.Event(self, attributes.OP_APPEND) 

120 

121 @util.memoized_property 

122 def _remove_token(self): 

123 return attributes.Event(self, attributes.OP_REMOVE) 

124 

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_) 

130 

131 collection_history.add_added(value) 

132 

133 for fn in self.dispatch.append: 

134 value = fn(state, value, initiator or self._append_token) 

135 

136 if self.trackparent and value is not None: 

137 self.sethasparent(attributes.instance_state(value), state, True) 

138 

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_) 

144 

145 collection_history.add_removed(value) 

146 

147 if self.trackparent and value is not None: 

148 self.sethasparent(attributes.instance_state(value), state, False) 

149 

150 for fn in self.dispatch.remove: 

151 fn(state, value, initiator or self._remove_token) 

152 

153 def _modified_event(self, state, dict_): 

154 

155 if self.key not in state.committed_state: 

156 state.committed_state[self.key] = CollectionHistory(self, state) 

157 

158 state._modified_event(dict_, self, attributes.NEVER_SET) 

159 

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] 

164 

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 

178 

179 if pop and value is None: 

180 return 

181 

182 iterable = value 

183 new_values = list(iterable) 

184 if state.has_identity: 

185 old_collection = util.IdentitySet(self.get(state, dict_)) 

186 

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 ) 

194 

195 idset = util.IdentitySet 

196 constants = old_collection.intersection(new_values) 

197 additions = idset(new_values).difference(constants) 

198 removals = old_collection.difference(constants) 

199 

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 ) 

209 

210 for member in removals: 

211 self.fire_remove_event( 

212 state, 

213 dict_, 

214 member, 

215 None, 

216 collection_history=collection_history, 

217 ) 

218 

219 def delete(self, *args, **kwargs): 

220 raise NotImplementedError() 

221 

222 def set_committed_value(self, state, dict_, value): 

223 raise NotImplementedError( 

224 "Dynamic attributes don't support " "collection population." 

225 ) 

226 

227 def get_history(self, state, dict_, passive=attributes.PASSIVE_OFF): 

228 c = self._get_collection_history(state, passive) 

229 return c.as_history() 

230 

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] 

236 

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) 

242 

243 if state.has_identity and (passive & attributes.INIT_OK): 

244 return CollectionHistory(self, state, apply_to=c) 

245 else: 

246 return c 

247 

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) 

253 

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) 

259 

260 def pop( 

261 self, state, dict_, value, initiator, passive=attributes.PASSIVE_OFF 

262 ): 

263 self.remove(state, dict_, value, initiator, passive=passive) 

264 

265 

266class DynamicCollectionAdapter(object): 

267 """simplified CollectionAdapter for internal API consistency""" 

268 

269 def __init__(self, data): 

270 self.data = data 

271 

272 def __iter__(self): 

273 return iter(self.data) 

274 

275 def _reset_empty(self): 

276 pass 

277 

278 def __len__(self): 

279 return len(self.data) 

280 

281 def __bool__(self): 

282 return True 

283 

284 __nonzero__ = __bool__ 

285 

286 

287class AppenderMixin(object): 

288 query_class = None 

289 

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 

294 

295 mapper = object_mapper(instance) 

296 prop = mapper._props[self.attr.key] 

297 

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]. 

305 

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) 

309 

310 self._where_criteria = ( 

311 prop._with_parent(instance, alias_secondary=False), 

312 ) 

313 

314 if self.attr.order_by: 

315 self._order_by_clauses = self.attr.order_by 

316 

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 

330 

331 session = property(session, lambda s, x: None) 

332 

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 ) 

344 

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() 

355 

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) 

365 

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() 

377 

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 ) 

392 

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) 

397 

398 query._where_criteria = self._where_criteria 

399 query._from_obj = self._from_obj 

400 query._order_by_clauses = self._order_by_clauses 

401 

402 return query 

403 

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 ) 

412 

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 ) 

420 

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 ) 

428 

429 

430class AppenderQuery(AppenderMixin, Query): 

431 """A dynamic query that supports basic collection storage operations.""" 

432 

433 

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}) 

438 

439 

440class CollectionHistory(object): 

441 """Overrides AttributeHistory to receive append/remove events directly.""" 

442 

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 

455 

456 @property 

457 def added_plus_unchanged(self): 

458 return list(self.added_items.union(self.unchanged_items)) 

459 

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 ) 

467 

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)) 

480 

481 def indexed(self, index): 

482 return list(self.added_items)[index] 

483 

484 def add_added(self, value): 

485 self.added_items.add(value) 

486 

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)