Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/sqlalchemy/orm/path_registry.py: 43%

244 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-06-07 06:35 +0000

1# orm/path_registry.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"""Path tracking utilities, representing mapper graph traversals. 

8 

9""" 

10 

11from itertools import chain 

12import logging 

13 

14from . import base as orm_base 

15from .. import exc 

16from .. import inspection 

17from .. import util 

18from ..sql import visitors 

19from ..sql.traversals import HasCacheKey 

20 

21log = logging.getLogger(__name__) 

22 

23 

24def _unreduce_path(path): 

25 return PathRegistry.deserialize(path) 

26 

27 

28_WILDCARD_TOKEN = "*" 

29_DEFAULT_TOKEN = "_sa_default" 

30 

31 

32class PathRegistry(HasCacheKey): 

33 """Represent query load paths and registry functions. 

34 

35 Basically represents structures like: 

36 

37 (<User mapper>, "orders", <Order mapper>, "items", <Item mapper>) 

38 

39 These structures are generated by things like 

40 query options (joinedload(), subqueryload(), etc.) and are 

41 used to compose keys stored in the query._attributes dictionary 

42 for various options. 

43 

44 They are then re-composed at query compile/result row time as 

45 the query is formed and as rows are fetched, where they again 

46 serve to compose keys to look up options in the context.attributes 

47 dictionary, which is copied from query._attributes. 

48 

49 The path structure has a limited amount of caching, where each 

50 "root" ultimately pulls from a fixed registry associated with 

51 the first mapper, that also contains elements for each of its 

52 property keys. However paths longer than two elements, which 

53 are the exception rather than the rule, are generated on an 

54 as-needed basis. 

55 

56 """ 

57 

58 __slots__ = () 

59 

60 is_token = False 

61 is_root = False 

62 

63 _cache_key_traversal = [ 

64 ("path", visitors.ExtendedInternalTraversal.dp_has_cache_key_list) 

65 ] 

66 

67 def __eq__(self, other): 

68 try: 

69 return other is not None and self.path == other._path_for_compare 

70 except AttributeError: 

71 util.warn( 

72 "Comparison of PathRegistry to %r is not supported" 

73 % (type(other)) 

74 ) 

75 return False 

76 

77 def __ne__(self, other): 

78 try: 

79 return other is None or self.path != other._path_for_compare 

80 except AttributeError: 

81 util.warn( 

82 "Comparison of PathRegistry to %r is not supported" 

83 % (type(other)) 

84 ) 

85 return True 

86 

87 @property 

88 def _path_for_compare(self): 

89 return self.path 

90 

91 def set(self, attributes, key, value): 

92 log.debug("set '%s' on path '%s' to '%s'", key, self, value) 

93 attributes[(key, self.natural_path)] = value 

94 

95 def setdefault(self, attributes, key, value): 

96 log.debug("setdefault '%s' on path '%s' to '%s'", key, self, value) 

97 attributes.setdefault((key, self.natural_path), value) 

98 

99 def get(self, attributes, key, value=None): 

100 key = (key, self.natural_path) 

101 if key in attributes: 

102 return attributes[key] 

103 else: 

104 return value 

105 

106 def __len__(self): 

107 return len(self.path) 

108 

109 def __hash__(self): 

110 return id(self) 

111 

112 @property 

113 def length(self): 

114 return len(self.path) 

115 

116 def pairs(self): 

117 path = self.path 

118 for i in range(0, len(path), 2): 

119 yield path[i], path[i + 1] 

120 

121 def contains_mapper(self, mapper): 

122 for path_mapper in [self.path[i] for i in range(0, len(self.path), 2)]: 

123 if path_mapper.mapper.isa(mapper): 

124 return True 

125 else: 

126 return False 

127 

128 def contains(self, attributes, key): 

129 return (key, self.path) in attributes 

130 

131 def __reduce__(self): 

132 return _unreduce_path, (self.serialize(),) 

133 

134 @classmethod 

135 def _serialize_path(cls, path): 

136 return list( 

137 zip( 

138 [ 

139 m.class_ if (m.is_mapper or m.is_aliased_class) else str(m) 

140 for m in [path[i] for i in range(0, len(path), 2)] 

141 ], 

142 [ 

143 path[i].key if (path[i].is_property) else str(path[i]) 

144 for i in range(1, len(path), 2) 

145 ] 

146 + [None], 

147 ) 

148 ) 

149 

150 @classmethod 

151 def _deserialize_path(cls, path): 

152 def _deserialize_mapper_token(mcls): 

153 return ( 

154 # note: we likely dont want configure=True here however 

155 # this is maintained at the moment for backwards compatibility 

156 orm_base._inspect_mapped_class(mcls, configure=True) 

157 if mcls not in PathToken._intern 

158 else PathToken._intern[mcls] 

159 ) 

160 

161 def _deserialize_key_token(mcls, key): 

162 if key is None: 

163 return None 

164 elif key in PathToken._intern: 

165 return PathToken._intern[key] 

166 else: 

167 return orm_base._inspect_mapped_class( 

168 mcls, configure=True 

169 ).attrs[key] 

170 

171 p = tuple( 

172 chain( 

173 *[ 

174 ( 

175 _deserialize_mapper_token(mcls), 

176 _deserialize_key_token(mcls, key), 

177 ) 

178 for mcls, key in path 

179 ] 

180 ) 

181 ) 

182 if p and p[-1] is None: 

183 p = p[0:-1] 

184 return p 

185 

186 @classmethod 

187 def serialize_context_dict(cls, dict_, tokens): 

188 return [ 

189 ((key, cls._serialize_path(path)), value) 

190 for (key, path), value in [ 

191 (k, v) 

192 for k, v in dict_.items() 

193 if isinstance(k, tuple) and k[0] in tokens 

194 ] 

195 ] 

196 

197 @classmethod 

198 def deserialize_context_dict(cls, serialized): 

199 return util.OrderedDict( 

200 ((key, tuple(cls._deserialize_path(path))), value) 

201 for (key, path), value in serialized 

202 ) 

203 

204 def serialize(self): 

205 path = self.path 

206 return self._serialize_path(path) 

207 

208 @classmethod 

209 def deserialize(cls, path): 

210 if path is None: 

211 return None 

212 p = cls._deserialize_path(path) 

213 return cls.coerce(p) 

214 

215 @classmethod 

216 def per_mapper(cls, mapper): 

217 if mapper.is_mapper: 

218 return CachingEntityRegistry(cls.root, mapper) 

219 else: 

220 return SlotsEntityRegistry(cls.root, mapper) 

221 

222 @classmethod 

223 def coerce(cls, raw): 

224 return util.reduce(lambda prev, next: prev[next], raw, cls.root) 

225 

226 def token(self, token): 

227 if token.endswith(":" + _WILDCARD_TOKEN): 

228 return TokenRegistry(self, token) 

229 elif token.endswith(":" + _DEFAULT_TOKEN): 

230 return TokenRegistry(self.root, token) 

231 else: 

232 raise exc.ArgumentError("invalid token: %s" % token) 

233 

234 def __add__(self, other): 

235 return util.reduce(lambda prev, next: prev[next], other.path, self) 

236 

237 def __repr__(self): 

238 return "%s(%r)" % (self.__class__.__name__, self.path) 

239 

240 

241class RootRegistry(PathRegistry): 

242 """Root registry, defers to mappers so that 

243 paths are maintained per-root-mapper. 

244 

245 """ 

246 

247 inherit_cache = True 

248 

249 path = natural_path = () 

250 has_entity = False 

251 is_aliased_class = False 

252 is_root = True 

253 

254 def __getitem__(self, entity): 

255 if entity in PathToken._intern: 

256 return PathToken._intern[entity] 

257 else: 

258 return entity._path_registry 

259 

260 

261PathRegistry.root = RootRegistry() 

262 

263 

264class PathToken(orm_base.InspectionAttr, HasCacheKey, str): 

265 """cacheable string token""" 

266 

267 _intern = {} 

268 

269 def _gen_cache_key(self, anon_map, bindparams): 

270 return (str(self),) 

271 

272 @property 

273 def _path_for_compare(self): 

274 return None 

275 

276 @classmethod 

277 def intern(cls, strvalue): 

278 if strvalue in cls._intern: 

279 return cls._intern[strvalue] 

280 else: 

281 cls._intern[strvalue] = result = PathToken(strvalue) 

282 return result 

283 

284 

285class TokenRegistry(PathRegistry): 

286 __slots__ = ("token", "parent", "path", "natural_path") 

287 

288 inherit_cache = True 

289 

290 def __init__(self, parent, token): 

291 token = PathToken.intern(token) 

292 

293 self.token = token 

294 self.parent = parent 

295 self.path = parent.path + (token,) 

296 self.natural_path = parent.natural_path + (token,) 

297 

298 has_entity = False 

299 

300 is_token = True 

301 

302 def generate_for_superclasses(self): 

303 if not self.parent.is_aliased_class and not self.parent.is_root: 

304 for ent in self.parent.mapper.iterate_to_root(): 

305 yield TokenRegistry(self.parent.parent[ent], self.token) 

306 elif ( 

307 self.parent.is_aliased_class 

308 and self.parent.entity._is_with_polymorphic 

309 ): 

310 yield self 

311 for ent in self.parent.entity._with_polymorphic_entities: 

312 yield TokenRegistry(self.parent.parent[ent], self.token) 

313 else: 

314 yield self 

315 

316 def __getitem__(self, entity): 

317 raise NotImplementedError() 

318 

319 

320class PropRegistry(PathRegistry): 

321 is_unnatural = False 

322 inherit_cache = True 

323 

324 def __init__(self, parent, prop): 

325 # restate this path in terms of the 

326 # given MapperProperty's parent. 

327 insp = inspection.inspect(parent[-1]) 

328 natural_parent = parent 

329 

330 if not insp.is_aliased_class or insp._use_mapper_path: 

331 parent = natural_parent = parent.parent[prop.parent] 

332 elif ( 

333 insp.is_aliased_class 

334 and insp.with_polymorphic_mappers 

335 and prop.parent in insp.with_polymorphic_mappers 

336 ): 

337 subclass_entity = parent[-1]._entity_for_mapper(prop.parent) 

338 parent = parent.parent[subclass_entity] 

339 

340 # when building a path where with_polymorphic() is in use, 

341 # special logic to determine the "natural path" when subclass 

342 # entities are used. 

343 # 

344 # here we are trying to distinguish between a path that starts 

345 # on a the with_polymorhpic entity vs. one that starts on a 

346 # normal entity that introduces a with_polymorphic() in the 

347 # middle using of_type(): 

348 # 

349 # # as in test_polymorphic_rel-> 

350 # # test_subqueryload_on_subclass_uses_path_correctly 

351 # wp = with_polymorphic(RegularEntity, "*") 

352 # sess.query(wp).options(someload(wp.SomeSubEntity.foos)) 

353 # 

354 # vs 

355 # 

356 # # as in test_relationship->JoinedloadWPolyOfTypeContinued 

357 # wp = with_polymorphic(SomeFoo, "*") 

358 # sess.query(RegularEntity).options( 

359 # someload(RegularEntity.foos.of_type(wp)) 

360 # .someload(wp.SubFoo.bar) 

361 # ) 

362 # 

363 # in the former case, the Query as it generates a path that we 

364 # want to match will be in terms of the with_polymorphic at the 

365 # beginning. in the latter case, Query will generate simple 

366 # paths that don't know about this with_polymorphic, so we must 

367 # use a separate natural path. 

368 # 

369 # 

370 if parent.parent: 

371 natural_parent = parent.parent[subclass_entity.mapper] 

372 self.is_unnatural = True 

373 else: 

374 natural_parent = parent 

375 elif ( 

376 natural_parent.parent 

377 and insp.is_aliased_class 

378 and prop.parent # this should always be the case here 

379 is not insp.mapper 

380 and insp.mapper.isa(prop.parent) 

381 ): 

382 natural_parent = parent.parent[prop.parent] 

383 

384 self.prop = prop 

385 self.parent = parent 

386 self.path = parent.path + (prop,) 

387 self.natural_path = natural_parent.natural_path + (prop,) 

388 

389 self._wildcard_path_loader_key = ( 

390 "loader", 

391 parent.path + self.prop._wildcard_token, 

392 ) 

393 self._default_path_loader_key = self.prop._default_path_loader_key 

394 self._loader_key = ("loader", self.natural_path) 

395 

396 def __str__(self): 

397 return " -> ".join(str(elem) for elem in self.path) 

398 

399 @util.memoized_property 

400 def has_entity(self): 

401 return self.prop._links_to_entity 

402 

403 @util.memoized_property 

404 def entity(self): 

405 return self.prop.entity 

406 

407 @property 

408 def mapper(self): 

409 return self.prop.mapper 

410 

411 @property 

412 def entity_path(self): 

413 return self[self.entity] 

414 

415 def __getitem__(self, entity): 

416 if isinstance(entity, (int, slice)): 

417 return self.path[entity] 

418 else: 

419 return SlotsEntityRegistry(self, entity) 

420 

421 

422class AbstractEntityRegistry(PathRegistry): 

423 __slots__ = () 

424 

425 has_entity = True 

426 

427 def __init__(self, parent, entity): 

428 self.key = entity 

429 self.parent = parent 

430 self.is_aliased_class = entity.is_aliased_class 

431 self.entity = entity 

432 self.path = parent.path + (entity,) 

433 

434 # the "natural path" is the path that we get when Query is traversing 

435 # from the lead entities into the various relationships; it corresponds 

436 # to the structure of mappers and relationships. when we are given a 

437 # path that comes from loader options, as of 1.3 it can have ac-hoc 

438 # with_polymorphic() and other AliasedInsp objects inside of it, which 

439 # are usually not present in mappings. So here we track both the 

440 # "enhanced" path in self.path and the "natural" path that doesn't 

441 # include those objects so these two traversals can be matched up. 

442 

443 # the test here for "(self.is_aliased_class or parent.is_unnatural)" 

444 # are to avoid the more expensive conditional logic that follows if we 

445 # know we don't have to do it. This conditional can just as well be 

446 # "if parent.path:", it just is more function calls. 

447 if parent.path and (self.is_aliased_class or parent.is_unnatural): 

448 # this is an infrequent code path used only for loader strategies 

449 # that also make use of of_type(). 

450 if entity.mapper.isa(parent.natural_path[-1].entity): 

451 self.natural_path = parent.natural_path + (entity.mapper,) 

452 else: 

453 self.natural_path = parent.natural_path + ( 

454 parent.natural_path[-1].entity, 

455 ) 

456 # it seems to make sense that since these paths get mixed up 

457 # with statements that are cached or not, we should make 

458 # sure the natural path is cacheable across different occurrences 

459 # of equivalent AliasedClass objects. however, so far this 

460 # does not seem to be needed for whatever reason. 

461 # elif not parent.path and self.is_aliased_class: 

462 # self.natural_path = (self.entity._generate_cache_key()[0], ) 

463 else: 

464 # self.natural_path = parent.natural_path + (entity, ) 

465 self.natural_path = self.path 

466 

467 @property 

468 def entity_path(self): 

469 return self 

470 

471 @property 

472 def mapper(self): 

473 return inspection.inspect(self.entity).mapper 

474 

475 def __bool__(self): 

476 return True 

477 

478 __nonzero__ = __bool__ 

479 

480 def __getitem__(self, entity): 

481 if isinstance(entity, (int, slice)): 

482 return self.path[entity] 

483 elif entity in PathToken._intern: 

484 return TokenRegistry(self, PathToken._intern[entity]) 

485 else: 

486 return PropRegistry(self, entity) 

487 

488 

489class SlotsEntityRegistry(AbstractEntityRegistry): 

490 # for aliased class, return lightweight, no-cycles created 

491 # version 

492 inherit_cache = True 

493 

494 __slots__ = ( 

495 "key", 

496 "parent", 

497 "is_aliased_class", 

498 "entity", 

499 "path", 

500 "natural_path", 

501 ) 

502 

503 

504class CachingEntityRegistry(AbstractEntityRegistry, dict): 

505 # for long lived mapper, return dict based caching 

506 # version that creates reference cycles 

507 

508 inherit_cache = True 

509 

510 def __getitem__(self, entity): 

511 if isinstance(entity, (int, slice)): 

512 return self.path[entity] 

513 else: 

514 return dict.__getitem__(self, entity) 

515 

516 def __missing__(self, key): 

517 self[key] = item = PropRegistry(self, key) 

518 

519 return item