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# ext/declarative/clsregistry.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"""Routines to handle the string class registry used by declarative. 

8 

9This system allows specification of classes and expressions used in 

10:func:`_orm.relationship` using strings. 

11 

12""" 

13import weakref 

14 

15from ... import exc 

16from ... import inspection 

17from ... import util 

18from ...orm import class_mapper 

19from ...orm import interfaces 

20from ...orm.properties import ColumnProperty 

21from ...orm.properties import RelationshipProperty 

22from ...orm.properties import SynonymProperty 

23from ...schema import _get_table_key 

24 

25 

26# strong references to registries which we place in 

27# the _decl_class_registry, which is usually weak referencing. 

28# the internal registries here link to classes with weakrefs and remove 

29# themselves when all references to contained classes are removed. 

30_registries = set() 

31 

32 

33def add_class(classname, cls): 

34 """Add a class to the _decl_class_registry associated with the 

35 given declarative class. 

36 

37 """ 

38 if classname in cls._decl_class_registry: 

39 # class already exists. 

40 existing = cls._decl_class_registry[classname] 

41 if not isinstance(existing, _MultipleClassMarker): 

42 existing = cls._decl_class_registry[ 

43 classname 

44 ] = _MultipleClassMarker([cls, existing]) 

45 else: 

46 cls._decl_class_registry[classname] = cls 

47 

48 try: 

49 root_module = cls._decl_class_registry["_sa_module_registry"] 

50 except KeyError: 

51 cls._decl_class_registry[ 

52 "_sa_module_registry" 

53 ] = root_module = _ModuleMarker("_sa_module_registry", None) 

54 

55 tokens = cls.__module__.split(".") 

56 

57 # build up a tree like this: 

58 # modulename: myapp.snacks.nuts 

59 # 

60 # myapp->snack->nuts->(classes) 

61 # snack->nuts->(classes) 

62 # nuts->(classes) 

63 # 

64 # this allows partial token paths to be used. 

65 while tokens: 

66 token = tokens.pop(0) 

67 module = root_module.get_module(token) 

68 for token in tokens: 

69 module = module.get_module(token) 

70 module.add_class(classname, cls) 

71 

72 

73class _MultipleClassMarker(object): 

74 """refers to multiple classes of the same name 

75 within _decl_class_registry. 

76 

77 """ 

78 

79 __slots__ = "on_remove", "contents", "__weakref__" 

80 

81 def __init__(self, classes, on_remove=None): 

82 self.on_remove = on_remove 

83 self.contents = set( 

84 [weakref.ref(item, self._remove_item) for item in classes] 

85 ) 

86 _registries.add(self) 

87 

88 def __iter__(self): 

89 return (ref() for ref in self.contents) 

90 

91 def attempt_get(self, path, key): 

92 if len(self.contents) > 1: 

93 raise exc.InvalidRequestError( 

94 'Multiple classes found for path "%s" ' 

95 "in the registry of this declarative " 

96 "base. Please use a fully module-qualified path." 

97 % (".".join(path + [key])) 

98 ) 

99 else: 

100 ref = list(self.contents)[0] 

101 cls = ref() 

102 if cls is None: 

103 raise NameError(key) 

104 return cls 

105 

106 def _remove_item(self, ref): 

107 self.contents.remove(ref) 

108 if not self.contents: 

109 _registries.discard(self) 

110 if self.on_remove: 

111 self.on_remove() 

112 

113 def add_item(self, item): 

114 # protect against class registration race condition against 

115 # asynchronous garbage collection calling _remove_item, 

116 # [ticket:3208] 

117 modules = set( 

118 [ 

119 cls.__module__ 

120 for cls in [ref() for ref in self.contents] 

121 if cls is not None 

122 ] 

123 ) 

124 if item.__module__ in modules: 

125 util.warn( 

126 "This declarative base already contains a class with the " 

127 "same class name and module name as %s.%s, and will " 

128 "be replaced in the string-lookup table." 

129 % (item.__module__, item.__name__) 

130 ) 

131 self.contents.add(weakref.ref(item, self._remove_item)) 

132 

133 

134class _ModuleMarker(object): 

135 """"refers to a module name within 

136 _decl_class_registry. 

137 

138 """ 

139 

140 __slots__ = "parent", "name", "contents", "mod_ns", "path", "__weakref__" 

141 

142 def __init__(self, name, parent): 

143 self.parent = parent 

144 self.name = name 

145 self.contents = {} 

146 self.mod_ns = _ModNS(self) 

147 if self.parent: 

148 self.path = self.parent.path + [self.name] 

149 else: 

150 self.path = [] 

151 _registries.add(self) 

152 

153 def __contains__(self, name): 

154 return name in self.contents 

155 

156 def __getitem__(self, name): 

157 return self.contents[name] 

158 

159 def _remove_item(self, name): 

160 self.contents.pop(name, None) 

161 if not self.contents and self.parent is not None: 

162 self.parent._remove_item(self.name) 

163 _registries.discard(self) 

164 

165 def resolve_attr(self, key): 

166 return getattr(self.mod_ns, key) 

167 

168 def get_module(self, name): 

169 if name not in self.contents: 

170 marker = _ModuleMarker(name, self) 

171 self.contents[name] = marker 

172 else: 

173 marker = self.contents[name] 

174 return marker 

175 

176 def add_class(self, name, cls): 

177 if name in self.contents: 

178 existing = self.contents[name] 

179 existing.add_item(cls) 

180 else: 

181 existing = self.contents[name] = _MultipleClassMarker( 

182 [cls], on_remove=lambda: self._remove_item(name) 

183 ) 

184 

185 

186class _ModNS(object): 

187 __slots__ = ("__parent",) 

188 

189 def __init__(self, parent): 

190 self.__parent = parent 

191 

192 def __getattr__(self, key): 

193 try: 

194 value = self.__parent.contents[key] 

195 except KeyError: 

196 pass 

197 else: 

198 if value is not None: 

199 if isinstance(value, _ModuleMarker): 

200 return value.mod_ns 

201 else: 

202 assert isinstance(value, _MultipleClassMarker) 

203 return value.attempt_get(self.__parent.path, key) 

204 raise AttributeError( 

205 "Module %r has no mapped classes " 

206 "registered under the name %r" % (self.__parent.name, key) 

207 ) 

208 

209 

210class _GetColumns(object): 

211 __slots__ = ("cls",) 

212 

213 def __init__(self, cls): 

214 self.cls = cls 

215 

216 def __getattr__(self, key): 

217 mp = class_mapper(self.cls, configure=False) 

218 if mp: 

219 if key not in mp.all_orm_descriptors: 

220 raise exc.InvalidRequestError( 

221 "Class %r does not have a mapped column named %r" 

222 % (self.cls, key) 

223 ) 

224 

225 desc = mp.all_orm_descriptors[key] 

226 if desc.extension_type is interfaces.NOT_EXTENSION: 

227 prop = desc.property 

228 if isinstance(prop, SynonymProperty): 

229 key = prop.name 

230 elif not isinstance(prop, ColumnProperty): 

231 raise exc.InvalidRequestError( 

232 "Property %r is not an instance of" 

233 " ColumnProperty (i.e. does not correspond" 

234 " directly to a Column)." % key 

235 ) 

236 return getattr(self.cls, key) 

237 

238 

239inspection._inspects(_GetColumns)( 

240 lambda target: inspection.inspect(target.cls) 

241) 

242 

243 

244class _GetTable(object): 

245 __slots__ = "key", "metadata" 

246 

247 def __init__(self, key, metadata): 

248 self.key = key 

249 self.metadata = metadata 

250 

251 def __getattr__(self, key): 

252 return self.metadata.tables[_get_table_key(key, self.key)] 

253 

254 

255def _determine_container(key, value): 

256 if isinstance(value, _MultipleClassMarker): 

257 value = value.attempt_get([], key) 

258 return _GetColumns(value) 

259 

260 

261class _class_resolver(object): 

262 def __init__(self, cls, prop, fallback, arg): 

263 self.cls = cls 

264 self.prop = prop 

265 self.arg = self._declarative_arg = arg 

266 self.fallback = fallback 

267 self._dict = util.PopulateDict(self._access_cls) 

268 self._resolvers = () 

269 

270 def _access_cls(self, key): 

271 cls = self.cls 

272 if key in cls._decl_class_registry: 

273 return _determine_container(key, cls._decl_class_registry[key]) 

274 elif key in cls.metadata.tables: 

275 return cls.metadata.tables[key] 

276 elif key in cls.metadata._schemas: 

277 return _GetTable(key, cls.metadata) 

278 elif ( 

279 "_sa_module_registry" in cls._decl_class_registry 

280 and key in cls._decl_class_registry["_sa_module_registry"] 

281 ): 

282 registry = cls._decl_class_registry["_sa_module_registry"] 

283 return registry.resolve_attr(key) 

284 elif self._resolvers: 

285 for resolv in self._resolvers: 

286 value = resolv(key) 

287 if value is not None: 

288 return value 

289 

290 return self.fallback[key] 

291 

292 def _raise_for_name(self, name, err): 

293 util.raise_( 

294 exc.InvalidRequestError( 

295 "When initializing mapper %s, expression %r failed to " 

296 "locate a name (%r). If this is a class name, consider " 

297 "adding this relationship() to the %r class after " 

298 "both dependent classes have been defined." 

299 % (self.prop.parent, self.arg, name, self.cls) 

300 ), 

301 from_=err, 

302 ) 

303 

304 def _resolve_name(self): 

305 name = self.arg 

306 d = self._dict 

307 rval = None 

308 try: 

309 for token in name.split("."): 

310 if rval is None: 

311 rval = d[token] 

312 else: 

313 rval = getattr(rval, token) 

314 except KeyError as err: 

315 self._raise_for_name(name, err) 

316 except NameError as n: 

317 self._raise_for_name(n.args[0], n) 

318 else: 

319 if isinstance(rval, _GetColumns): 

320 return rval.cls 

321 else: 

322 return rval 

323 

324 def __call__(self): 

325 try: 

326 x = eval(self.arg, globals(), self._dict) 

327 

328 if isinstance(x, _GetColumns): 

329 return x.cls 

330 else: 

331 return x 

332 except NameError as n: 

333 self._raise_for_name(n.args[0], n) 

334 

335 

336def _resolver(cls, prop): 

337 import sqlalchemy 

338 from sqlalchemy.orm import foreign, remote 

339 

340 fallback = sqlalchemy.__dict__.copy() 

341 fallback.update({"foreign": foreign, "remote": remote}) 

342 

343 def resolve_arg(arg): 

344 return _class_resolver(cls, prop, fallback, arg) 

345 

346 def resolve_name(arg): 

347 return _class_resolver(cls, prop, fallback, arg)._resolve_name 

348 

349 return resolve_name, resolve_arg 

350 

351 

352def _deferred_relationship(cls, prop): 

353 

354 if isinstance(prop, RelationshipProperty): 

355 resolve_name, resolve_arg = _resolver(cls, prop) 

356 

357 for attr in ( 

358 "order_by", 

359 "primaryjoin", 

360 "secondaryjoin", 

361 "secondary", 

362 "_user_defined_foreign_keys", 

363 "remote_side", 

364 ): 

365 v = getattr(prop, attr) 

366 if isinstance(v, util.string_types): 

367 setattr(prop, attr, resolve_arg(v)) 

368 

369 for attr in ("argument",): 

370 v = getattr(prop, attr) 

371 if isinstance(v, util.string_types): 

372 setattr(prop, attr, resolve_name(v)) 

373 

374 if prop.backref and isinstance(prop.backref, tuple): 

375 key, kwargs = prop.backref 

376 for attr in ( 

377 "primaryjoin", 

378 "secondaryjoin", 

379 "secondary", 

380 "foreign_keys", 

381 "remote_side", 

382 "order_by", 

383 ): 

384 if attr in kwargs and isinstance( 

385 kwargs[attr], util.string_types 

386 ): 

387 kwargs[attr] = resolve_arg(kwargs[attr]) 

388 

389 return prop