Hide keyboard shortcuts

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/path_registry.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 

7"""Path tracking utilities, representing mapper graph traversals. 

8 

9""" 

10 

11from itertools import chain 

12import logging 

13 

14from .base import class_mapper 

15from .. import exc 

16from .. import inspection 

17from .. import util 

18 

19 

20log = logging.getLogger(__name__) 

21 

22 

23def _unreduce_path(path): 

24 return PathRegistry.deserialize(path) 

25 

26 

27_WILDCARD_TOKEN = "*" 

28_DEFAULT_TOKEN = "_sa_default" 

29 

30 

31class PathRegistry(object): 

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

33 

34 Basically represents structures like: 

35 

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

37 

38 These structures are generated by things like 

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

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

41 for various options. 

42 

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

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

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

46 dictionary, which is copied from query._attributes. 

47 

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

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

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

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

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

53 as-needed basis. 

54 

55 """ 

56 

57 __slots__ = () 

58 

59 is_token = False 

60 is_root = False 

61 

62 def __eq__(self, other): 

63 try: 

64 return other is not None and self.path == other.path 

65 except AttributeError: 

66 util.warn( 

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

68 % (type(other)) 

69 ) 

70 return False 

71 

72 def __ne__(self, other): 

73 try: 

74 return other is None or self.path != other.path 

75 except AttributeError: 

76 util.warn( 

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

78 % (type(other)) 

79 ) 

80 return True 

81 

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

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

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

85 

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

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

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

89 

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

91 key = (key, self.natural_path) 

92 if key in attributes: 

93 return attributes[key] 

94 else: 

95 return value 

96 

97 def __len__(self): 

98 return len(self.path) 

99 

100 @property 

101 def length(self): 

102 return len(self.path) 

103 

104 def pairs(self): 

105 path = self.path 

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

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

108 

109 def contains_mapper(self, mapper): 

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

111 if path_mapper.is_mapper and path_mapper.isa(mapper): 

112 return True 

113 else: 

114 return False 

115 

116 def contains(self, attributes, key): 

117 return (key, self.path) in attributes 

118 

119 def __reduce__(self): 

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

121 

122 @classmethod 

123 def _serialize_path(cls, path): 

124 return list( 

125 zip( 

126 [m.class_ for m in [path[i] for i in range(0, len(path), 2)]], 

127 [path[i].key for i in range(1, len(path), 2)] + [None], 

128 ) 

129 ) 

130 

131 @classmethod 

132 def _deserialize_path(cls, path): 

133 p = tuple( 

134 chain( 

135 *[ 

136 ( 

137 class_mapper(mcls), 

138 class_mapper(mcls).attrs[key] 

139 if key is not None 

140 else None, 

141 ) 

142 for mcls, key in path 

143 ] 

144 ) 

145 ) 

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

147 p = p[0:-1] 

148 return p 

149 

150 @classmethod 

151 def serialize_context_dict(cls, dict_, tokens): 

152 return [ 

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

154 for (key, path), value in [ 

155 (k, v) 

156 for k, v in dict_.items() 

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

158 ] 

159 ] 

160 

161 @classmethod 

162 def deserialize_context_dict(cls, serialized): 

163 return util.OrderedDict( 

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

165 for (key, path), value in serialized 

166 ) 

167 

168 def serialize(self): 

169 path = self.path 

170 return self._serialize_path(path) 

171 

172 @classmethod 

173 def deserialize(cls, path): 

174 if path is None: 

175 return None 

176 p = cls._deserialize_path(path) 

177 return cls.coerce(p) 

178 

179 @classmethod 

180 def per_mapper(cls, mapper): 

181 if mapper.is_mapper: 

182 return CachingEntityRegistry(cls.root, mapper) 

183 else: 

184 return SlotsEntityRegistry(cls.root, mapper) 

185 

186 @classmethod 

187 def coerce(cls, raw): 

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

189 

190 def token(self, token): 

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

192 return TokenRegistry(self, token) 

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

194 return TokenRegistry(self.root, token) 

195 else: 

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

197 

198 def __add__(self, other): 

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

200 

201 def __repr__(self): 

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

203 

204 

205class RootRegistry(PathRegistry): 

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

207 paths are maintained per-root-mapper. 

208 

209 """ 

210 

211 path = natural_path = () 

212 has_entity = False 

213 is_aliased_class = False 

214 is_root = True 

215 

216 def __getitem__(self, entity): 

217 return entity._path_registry 

218 

219 

220PathRegistry.root = RootRegistry() 

221 

222 

223class TokenRegistry(PathRegistry): 

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

225 

226 def __init__(self, parent, token): 

227 self.token = token 

228 self.parent = parent 

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

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

231 

232 has_entity = False 

233 

234 is_token = True 

235 

236 def generate_for_superclasses(self): 

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

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

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

240 elif ( 

241 self.parent.is_aliased_class 

242 and self.parent.entity._is_with_polymorphic 

243 ): 

244 yield self 

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

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

247 else: 

248 yield self 

249 

250 def __getitem__(self, entity): 

251 raise NotImplementedError() 

252 

253 

254class PropRegistry(PathRegistry): 

255 is_unnatural = False 

256 

257 def __init__(self, parent, prop): 

258 # restate this path in terms of the 

259 # given MapperProperty's parent. 

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

261 natural_parent = parent 

262 

263 if not insp.is_aliased_class or insp._use_mapper_path: 

264 parent = natural_parent = parent.parent[prop.parent] 

265 elif ( 

266 insp.is_aliased_class 

267 and insp.with_polymorphic_mappers 

268 and prop.parent in insp.with_polymorphic_mappers 

269 ): 

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

271 parent = parent.parent[subclass_entity] 

272 

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

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

275 # entities are used. 

276 # 

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

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

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

280 # middle using of_type(): 

281 # 

282 # # as in test_polymorphic_rel-> 

283 # # test_subqueryload_on_subclass_uses_path_correctly 

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

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

286 # 

287 # vs 

288 # 

289 # # as in test_relationship->JoinedloadWPolyOfTypeContinued 

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

291 # sess.query(RegularEntity).options( 

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

293 # .someload(wp.SubFoo.bar) 

294 # ) 

295 # 

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

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

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

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

300 # use a separate natural path. 

301 # 

302 # 

303 if parent.parent: 

304 natural_parent = parent.parent[subclass_entity.mapper] 

305 self.is_unnatural = True 

306 else: 

307 natural_parent = parent 

308 elif ( 

309 natural_parent.parent 

310 and insp.is_aliased_class 

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

312 is not insp.mapper 

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

314 ): 

315 natural_parent = parent.parent[prop.parent] 

316 

317 self.prop = prop 

318 self.parent = parent 

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

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

321 

322 self._wildcard_path_loader_key = ( 

323 "loader", 

324 parent.path + self.prop._wildcard_token, 

325 ) 

326 self._default_path_loader_key = self.prop._default_path_loader_key 

327 self._loader_key = ("loader", self.path) 

328 

329 def __str__(self): 

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

331 

332 @util.memoized_property 

333 def has_entity(self): 

334 return hasattr(self.prop, "mapper") 

335 

336 @util.memoized_property 

337 def entity(self): 

338 return self.prop.mapper 

339 

340 @property 

341 def mapper(self): 

342 return self.entity 

343 

344 @property 

345 def entity_path(self): 

346 return self[self.entity] 

347 

348 def __getitem__(self, entity): 

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

350 return self.path[entity] 

351 else: 

352 return SlotsEntityRegistry(self, entity) 

353 

354 

355class AbstractEntityRegistry(PathRegistry): 

356 __slots__ = () 

357 

358 has_entity = True 

359 

360 def __init__(self, parent, entity): 

361 self.key = entity 

362 self.parent = parent 

363 self.is_aliased_class = entity.is_aliased_class 

364 self.entity = entity 

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

366 

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

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

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

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

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

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

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

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

375 

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

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

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

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

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

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

382 # that also make use of of_type(). 

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

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

385 else: 

386 self.natural_path = parent.natural_path + ( 

387 parent.natural_path[-1].entity, 

388 ) 

389 else: 

390 self.natural_path = self.path 

391 

392 @property 

393 def entity_path(self): 

394 return self 

395 

396 @property 

397 def mapper(self): 

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

399 

400 def __bool__(self): 

401 return True 

402 

403 __nonzero__ = __bool__ 

404 

405 def __getitem__(self, entity): 

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

407 return self.path[entity] 

408 else: 

409 return PropRegistry(self, entity) 

410 

411 

412class SlotsEntityRegistry(AbstractEntityRegistry): 

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

414 # version 

415 

416 __slots__ = ( 

417 "key", 

418 "parent", 

419 "is_aliased_class", 

420 "entity", 

421 "path", 

422 "natural_path", 

423 ) 

424 

425 

426class CachingEntityRegistry(AbstractEntityRegistry, dict): 

427 # for long lived mapper, return dict based caching 

428 # version that creates reference cycles 

429 

430 def __getitem__(self, entity): 

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

432 return self.path[entity] 

433 else: 

434 return dict.__getitem__(self, entity) 

435 

436 def __missing__(self, key): 

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

438 

439 return item