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

8"""Defines SQLAlchemy's system of class instrumentation. 

9 

10This module is usually not directly visible to user applications, but 

11defines a large part of the ORM's interactivity. 

12 

13instrumentation.py deals with registration of end-user classes 

14for state tracking. It interacts closely with state.py 

15and attributes.py which establish per-instance and per-class-attribute 

16instrumentation, respectively. 

17 

18The class instrumentation system can be customized on a per-class 

19or global basis using the :mod:`sqlalchemy.ext.instrumentation` 

20module, which provides the means to build and specify 

21alternate instrumentation forms. 

22 

23.. versionchanged: 0.8 

24 The instrumentation extension system was moved out of the 

25 ORM and into the external :mod:`sqlalchemy.ext.instrumentation` 

26 package. When that package is imported, it installs 

27 itself within sqlalchemy.orm so that its more comprehensive 

28 resolution mechanics take effect. 

29 

30""" 

31 

32 

33from . import base 

34from . import collections 

35from . import exc 

36from . import interfaces 

37from . import state 

38from .. import util 

39 

40 

41_memoized_key_collection = util.group_expirable_memoized_property() 

42 

43 

44class ClassManager(dict): 

45 """tracks state information at the class level.""" 

46 

47 MANAGER_ATTR = base.DEFAULT_MANAGER_ATTR 

48 STATE_ATTR = base.DEFAULT_STATE_ATTR 

49 

50 _state_setter = staticmethod(util.attrsetter(STATE_ATTR)) 

51 

52 deferred_scalar_loader = None 

53 

54 original_init = object.__init__ 

55 

56 factory = None 

57 

58 def __init__(self, class_): 

59 self.class_ = class_ 

60 self.info = {} 

61 self.new_init = None 

62 self.local_attrs = {} 

63 self.originals = {} 

64 

65 self._bases = [ 

66 mgr 

67 for mgr in [ 

68 manager_of_class(base) 

69 for base in self.class_.__bases__ 

70 if isinstance(base, type) 

71 ] 

72 if mgr is not None 

73 ] 

74 

75 for base_ in self._bases: 

76 self.update(base_) 

77 

78 self.dispatch._events._new_classmanager_instance(class_, self) 

79 # events._InstanceEventsHold.populate(class_, self) 

80 

81 for basecls in class_.__mro__: 

82 mgr = manager_of_class(basecls) 

83 if mgr is not None: 

84 self.dispatch._update(mgr.dispatch) 

85 self.manage() 

86 self._instrument_init() 

87 

88 if "__del__" in class_.__dict__: 

89 util.warn( 

90 "__del__() method on class %s will " 

91 "cause unreachable cycles and memory leaks, " 

92 "as SQLAlchemy instrumentation often creates " 

93 "reference cycles. Please remove this method." % class_ 

94 ) 

95 

96 def __hash__(self): 

97 return id(self) 

98 

99 def __eq__(self, other): 

100 return other is self 

101 

102 @property 

103 def is_mapped(self): 

104 return "mapper" in self.__dict__ 

105 

106 @_memoized_key_collection 

107 def _all_key_set(self): 

108 return frozenset(self) 

109 

110 @_memoized_key_collection 

111 def _collection_impl_keys(self): 

112 return frozenset( 

113 [attr.key for attr in self.values() if attr.impl.collection] 

114 ) 

115 

116 @_memoized_key_collection 

117 def _scalar_loader_impls(self): 

118 return frozenset( 

119 [ 

120 attr.impl 

121 for attr in self.values() 

122 if attr.impl.accepts_scalar_loader 

123 ] 

124 ) 

125 

126 @util.memoized_property 

127 def mapper(self): 

128 # raises unless self.mapper has been assigned 

129 raise exc.UnmappedClassError(self.class_) 

130 

131 def _all_sqla_attributes(self, exclude=None): 

132 """return an iterator of all classbound attributes that are 

133 implement :class:`.InspectionAttr`. 

134 

135 This includes :class:`.QueryableAttribute` as well as extension 

136 types such as :class:`.hybrid_property` and 

137 :class:`.AssociationProxy`. 

138 

139 """ 

140 if exclude is None: 

141 exclude = set() 

142 for supercls in self.class_.__mro__: 

143 for key in set(supercls.__dict__).difference(exclude): 

144 exclude.add(key) 

145 val = supercls.__dict__[key] 

146 if ( 

147 isinstance(val, interfaces.InspectionAttr) 

148 and val.is_attribute 

149 ): 

150 yield key, val 

151 

152 def _get_class_attr_mro(self, key, default=None): 

153 """return an attribute on the class without tripping it.""" 

154 

155 for supercls in self.class_.__mro__: 

156 if key in supercls.__dict__: 

157 return supercls.__dict__[key] 

158 else: 

159 return default 

160 

161 def _attr_has_impl(self, key): 

162 """Return True if the given attribute is fully initialized. 

163 

164 i.e. has an impl. 

165 """ 

166 

167 return key in self and self[key].impl is not None 

168 

169 def _subclass_manager(self, cls): 

170 """Create a new ClassManager for a subclass of this ClassManager's 

171 class. 

172 

173 This is called automatically when attributes are instrumented so that 

174 the attributes can be propagated to subclasses against their own 

175 class-local manager, without the need for mappers etc. to have already 

176 pre-configured managers for the full class hierarchy. Mappers 

177 can post-configure the auto-generated ClassManager when needed. 

178 

179 """ 

180 manager = manager_of_class(cls) 

181 if manager is None: 

182 manager = _instrumentation_factory.create_manager_for_cls(cls) 

183 return manager 

184 

185 def _instrument_init(self): 

186 # TODO: self.class_.__init__ is often the already-instrumented 

187 # __init__ from an instrumented superclass. We still need to make 

188 # our own wrapper, but it would 

189 # be nice to wrap the original __init__ and not our existing wrapper 

190 # of such, since this adds method overhead. 

191 self.original_init = self.class_.__init__ 

192 self.new_init = _generate_init(self.class_, self) 

193 self.install_member("__init__", self.new_init) 

194 

195 def _uninstrument_init(self): 

196 if self.new_init: 

197 self.uninstall_member("__init__") 

198 self.new_init = None 

199 

200 @util.memoized_property 

201 def _state_constructor(self): 

202 self.dispatch.first_init(self, self.class_) 

203 return state.InstanceState 

204 

205 def manage(self): 

206 """Mark this instance as the manager for its class.""" 

207 

208 setattr(self.class_, self.MANAGER_ATTR, self) 

209 

210 def dispose(self): 

211 """Dissasociate this manager from its class.""" 

212 

213 delattr(self.class_, self.MANAGER_ATTR) 

214 

215 @util.hybridmethod 

216 def manager_getter(self): 

217 return _default_manager_getter 

218 

219 @util.hybridmethod 

220 def state_getter(self): 

221 """Return a (instance) -> InstanceState callable. 

222 

223 "state getter" callables should raise either KeyError or 

224 AttributeError if no InstanceState could be found for the 

225 instance. 

226 """ 

227 

228 return _default_state_getter 

229 

230 @util.hybridmethod 

231 def dict_getter(self): 

232 return _default_dict_getter 

233 

234 def instrument_attribute(self, key, inst, propagated=False): 

235 if propagated: 

236 if key in self.local_attrs: 

237 return # don't override local attr with inherited attr 

238 else: 

239 self.local_attrs[key] = inst 

240 self.install_descriptor(key, inst) 

241 _memoized_key_collection.expire_instance(self) 

242 self[key] = inst 

243 

244 for cls in self.class_.__subclasses__(): 

245 manager = self._subclass_manager(cls) 

246 manager.instrument_attribute(key, inst, True) 

247 

248 def subclass_managers(self, recursive): 

249 for cls in self.class_.__subclasses__(): 

250 mgr = manager_of_class(cls) 

251 if mgr is not None and mgr is not self: 

252 yield mgr 

253 if recursive: 

254 for m in mgr.subclass_managers(True): 

255 yield m 

256 

257 def post_configure_attribute(self, key): 

258 _instrumentation_factory.dispatch.attribute_instrument( 

259 self.class_, key, self[key] 

260 ) 

261 

262 def uninstrument_attribute(self, key, propagated=False): 

263 if key not in self: 

264 return 

265 if propagated: 

266 if key in self.local_attrs: 

267 return # don't get rid of local attr 

268 else: 

269 del self.local_attrs[key] 

270 self.uninstall_descriptor(key) 

271 _memoized_key_collection.expire_instance(self) 

272 del self[key] 

273 for cls in self.class_.__subclasses__(): 

274 manager = manager_of_class(cls) 

275 if manager: 

276 manager.uninstrument_attribute(key, True) 

277 

278 def unregister(self): 

279 """remove all instrumentation established by this ClassManager.""" 

280 

281 self._uninstrument_init() 

282 

283 self.mapper = self.dispatch = None 

284 self.info.clear() 

285 

286 for key in list(self): 

287 if key in self.local_attrs: 

288 self.uninstrument_attribute(key) 

289 

290 def install_descriptor(self, key, inst): 

291 if key in (self.STATE_ATTR, self.MANAGER_ATTR): 

292 raise KeyError( 

293 "%r: requested attribute name conflicts with " 

294 "instrumentation attribute of the same name." % key 

295 ) 

296 setattr(self.class_, key, inst) 

297 

298 def uninstall_descriptor(self, key): 

299 delattr(self.class_, key) 

300 

301 def install_member(self, key, implementation): 

302 if key in (self.STATE_ATTR, self.MANAGER_ATTR): 

303 raise KeyError( 

304 "%r: requested attribute name conflicts with " 

305 "instrumentation attribute of the same name." % key 

306 ) 

307 self.originals.setdefault(key, getattr(self.class_, key, None)) 

308 setattr(self.class_, key, implementation) 

309 

310 def uninstall_member(self, key): 

311 original = self.originals.pop(key, None) 

312 if original is not None: 

313 setattr(self.class_, key, original) 

314 

315 def instrument_collection_class(self, key, collection_class): 

316 return collections.prepare_instrumentation(collection_class) 

317 

318 def initialize_collection(self, key, state, factory): 

319 user_data = factory() 

320 adapter = collections.CollectionAdapter( 

321 self.get_impl(key), state, user_data 

322 ) 

323 return adapter, user_data 

324 

325 def is_instrumented(self, key, search=False): 

326 if search: 

327 return key in self 

328 else: 

329 return key in self.local_attrs 

330 

331 def get_impl(self, key): 

332 return self[key].impl 

333 

334 @property 

335 def attributes(self): 

336 return iter(self.values()) 

337 

338 # InstanceState management 

339 

340 def new_instance(self, state=None): 

341 instance = self.class_.__new__(self.class_) 

342 if state is None: 

343 state = self._state_constructor(instance, self) 

344 self._state_setter(instance, state) 

345 return instance 

346 

347 def setup_instance(self, instance, state=None): 

348 if state is None: 

349 state = self._state_constructor(instance, self) 

350 self._state_setter(instance, state) 

351 

352 def teardown_instance(self, instance): 

353 delattr(instance, self.STATE_ATTR) 

354 

355 def _serialize(self, state, state_dict): 

356 return _SerializeManager(state, state_dict) 

357 

358 def _new_state_if_none(self, instance): 

359 """Install a default InstanceState if none is present. 

360 

361 A private convenience method used by the __init__ decorator. 

362 

363 """ 

364 if hasattr(instance, self.STATE_ATTR): 

365 return False 

366 elif self.class_ is not instance.__class__ and self.is_mapped: 

367 # this will create a new ClassManager for the 

368 # subclass, without a mapper. This is likely a 

369 # user error situation but allow the object 

370 # to be constructed, so that it is usable 

371 # in a non-ORM context at least. 

372 return self._subclass_manager( 

373 instance.__class__ 

374 )._new_state_if_none(instance) 

375 else: 

376 state = self._state_constructor(instance, self) 

377 self._state_setter(instance, state) 

378 return state 

379 

380 def has_state(self, instance): 

381 return hasattr(instance, self.STATE_ATTR) 

382 

383 def has_parent(self, state, key, optimistic=False): 

384 """TODO""" 

385 return self.get_impl(key).hasparent(state, optimistic=optimistic) 

386 

387 def __bool__(self): 

388 """All ClassManagers are non-zero regardless of attribute state.""" 

389 return True 

390 

391 __nonzero__ = __bool__ 

392 

393 def __repr__(self): 

394 return "<%s of %r at %x>" % ( 

395 self.__class__.__name__, 

396 self.class_, 

397 id(self), 

398 ) 

399 

400 

401class _SerializeManager(object): 

402 """Provide serialization of a :class:`.ClassManager`. 

403 

404 The :class:`.InstanceState` uses ``__init__()`` on serialize 

405 and ``__call__()`` on deserialize. 

406 

407 """ 

408 

409 def __init__(self, state, d): 

410 self.class_ = state.class_ 

411 manager = state.manager 

412 manager.dispatch.pickle(state, d) 

413 

414 def __call__(self, state, inst, state_dict): 

415 state.manager = manager = manager_of_class(self.class_) 

416 if manager is None: 

417 raise exc.UnmappedInstanceError( 

418 inst, 

419 "Cannot deserialize object of type %r - " 

420 "no mapper() has " 

421 "been configured for this class within the current " 

422 "Python process!" % self.class_, 

423 ) 

424 elif manager.is_mapped and not manager.mapper.configured: 

425 manager.mapper._configure_all() 

426 

427 # setup _sa_instance_state ahead of time so that 

428 # unpickle events can access the object normally. 

429 # see [ticket:2362] 

430 if inst is not None: 

431 manager.setup_instance(inst, state) 

432 manager.dispatch.unpickle(state, state_dict) 

433 

434 

435class InstrumentationFactory(object): 

436 """Factory for new ClassManager instances.""" 

437 

438 def create_manager_for_cls(self, class_): 

439 assert class_ is not None 

440 assert manager_of_class(class_) is None 

441 

442 # give a more complicated subclass 

443 # a chance to do what it wants here 

444 manager, factory = self._locate_extended_factory(class_) 

445 

446 if factory is None: 

447 factory = ClassManager 

448 manager = factory(class_) 

449 

450 self._check_conflicts(class_, factory) 

451 

452 manager.factory = factory 

453 

454 self.dispatch.class_instrument(class_) 

455 return manager 

456 

457 def _locate_extended_factory(self, class_): 

458 """Overridden by a subclass to do an extended lookup.""" 

459 return None, None 

460 

461 def _check_conflicts(self, class_, factory): 

462 """Overridden by a subclass to test for conflicting factories.""" 

463 return 

464 

465 def unregister(self, class_): 

466 manager = manager_of_class(class_) 

467 manager.unregister() 

468 manager.dispose() 

469 self.dispatch.class_uninstrument(class_) 

470 if ClassManager.MANAGER_ATTR in class_.__dict__: 

471 delattr(class_, ClassManager.MANAGER_ATTR) 

472 

473 

474# this attribute is replaced by sqlalchemy.ext.instrumentation 

475# when importred. 

476_instrumentation_factory = InstrumentationFactory() 

477 

478# these attributes are replaced by sqlalchemy.ext.instrumentation 

479# when a non-standard InstrumentationManager class is first 

480# used to instrument a class. 

481instance_state = _default_state_getter = base.instance_state 

482 

483instance_dict = _default_dict_getter = base.instance_dict 

484 

485manager_of_class = _default_manager_getter = base.manager_of_class 

486 

487 

488def register_class(class_): 

489 """Register class instrumentation. 

490 

491 Returns the existing or newly created class manager. 

492 

493 """ 

494 

495 manager = manager_of_class(class_) 

496 if manager is None: 

497 manager = _instrumentation_factory.create_manager_for_cls(class_) 

498 return manager 

499 

500 

501def unregister_class(class_): 

502 """Unregister class instrumentation.""" 

503 

504 _instrumentation_factory.unregister(class_) 

505 

506 

507def is_instrumented(instance, key): 

508 """Return True if the given attribute on the given instance is 

509 instrumented by the attributes package. 

510 

511 This function may be used regardless of instrumentation 

512 applied directly to the class, i.e. no descriptors are required. 

513 

514 """ 

515 return manager_of_class(instance.__class__).is_instrumented( 

516 key, search=True 

517 ) 

518 

519 

520def _generate_init(class_, class_manager): 

521 """Build an __init__ decorator that triggers ClassManager events.""" 

522 

523 # TODO: we should use the ClassManager's notion of the 

524 # original '__init__' method, once ClassManager is fixed 

525 # to always reference that. 

526 original__init__ = class_.__init__ 

527 assert original__init__ 

528 

529 # Go through some effort here and don't change the user's __init__ 

530 # calling signature, including the unlikely case that it has 

531 # a return value. 

532 # FIXME: need to juggle local names to avoid constructor argument 

533 # clashes. 

534 func_body = """\ 

535def __init__(%(apply_pos)s): 

536 new_state = class_manager._new_state_if_none(%(self_arg)s) 

537 if new_state: 

538 return new_state._initialize_instance(%(apply_kw)s) 

539 else: 

540 return original__init__(%(apply_kw)s) 

541""" 

542 func_vars = util.format_argspec_init(original__init__, grouped=False) 

543 func_text = func_body % func_vars 

544 

545 if util.py2k: 

546 func = getattr(original__init__, "im_func", original__init__) 

547 func_defaults = getattr(func, "func_defaults", None) 

548 else: 

549 func_defaults = getattr(original__init__, "__defaults__", None) 

550 func_kw_defaults = getattr(original__init__, "__kwdefaults__", None) 

551 

552 env = locals().copy() 

553 exec(func_text, env) 

554 __init__ = env["__init__"] 

555 __init__.__doc__ = original__init__.__doc__ 

556 __init__._sa_original_init = original__init__ 

557 

558 if func_defaults: 

559 __init__.__defaults__ = func_defaults 

560 if not util.py2k and func_kw_defaults: 

561 __init__.__kwdefaults__ = func_kw_defaults 

562 

563 return __init__