1# orm/instrumentation.py 2# Copyright (C) 2005-2021 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"""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 39from ..util import HasMemoized 40 41 42DEL_ATTR = util.symbol("DEL_ATTR") 43 44 45class ClassManager(HasMemoized, dict): 46 """Tracks state information at the class level.""" 47 48 MANAGER_ATTR = base.DEFAULT_MANAGER_ATTR 49 STATE_ATTR = base.DEFAULT_STATE_ATTR 50 51 _state_setter = staticmethod(util.attrsetter(STATE_ATTR)) 52 53 expired_attribute_loader = None 54 "previously known as deferred_scalar_loader" 55 56 init_method = None 57 58 factory = None 59 mapper = None 60 declarative_scan = None 61 registry = None 62 63 @property 64 @util.deprecated( 65 "1.4", 66 message="The ClassManager.deferred_scalar_loader attribute is now " 67 "named expired_attribute_loader", 68 ) 69 def deferred_scalar_loader(self): 70 return self.expired_attribute_loader 71 72 @deferred_scalar_loader.setter 73 @util.deprecated( 74 "1.4", 75 message="The ClassManager.deferred_scalar_loader attribute is now " 76 "named expired_attribute_loader", 77 ) 78 def deferred_scalar_loader(self, obj): 79 self.expired_attribute_loader = obj 80 81 def __init__(self, class_): 82 self.class_ = class_ 83 self.info = {} 84 self.new_init = None 85 self.local_attrs = {} 86 self.originals = {} 87 self._finalized = False 88 89 self._bases = [ 90 mgr 91 for mgr in [ 92 manager_of_class(base) 93 for base in self.class_.__bases__ 94 if isinstance(base, type) 95 ] 96 if mgr is not None 97 ] 98 99 for base_ in self._bases: 100 self.update(base_) 101 102 self.dispatch._events._new_classmanager_instance(class_, self) 103 104 for basecls in class_.__mro__: 105 mgr = manager_of_class(basecls) 106 if mgr is not None: 107 self.dispatch._update(mgr.dispatch) 108 109 self.manage() 110 111 if "__del__" in class_.__dict__: 112 util.warn( 113 "__del__() method on class %s will " 114 "cause unreachable cycles and memory leaks, " 115 "as SQLAlchemy instrumentation often creates " 116 "reference cycles. Please remove this method." % class_ 117 ) 118 119 def _update_state( 120 self, 121 finalize=False, 122 mapper=None, 123 registry=None, 124 declarative_scan=None, 125 expired_attribute_loader=None, 126 init_method=None, 127 ): 128 129 if mapper: 130 self.mapper = mapper 131 if registry: 132 registry._add_manager(self) 133 if declarative_scan: 134 self.declarative_scan = declarative_scan 135 if expired_attribute_loader: 136 self.expired_attribute_loader = expired_attribute_loader 137 138 if init_method: 139 assert not self._finalized, ( 140 "class is already instrumented, " 141 "init_method %s can't be applied" % init_method 142 ) 143 self.init_method = init_method 144 145 if not self._finalized: 146 self.original_init = ( 147 self.init_method 148 if self.init_method is not None 149 and self.class_.__init__ is object.__init__ 150 else self.class_.__init__ 151 ) 152 153 if finalize and not self._finalized: 154 self._finalize() 155 156 def _finalize(self): 157 if self._finalized: 158 return 159 self._finalized = True 160 161 self._instrument_init() 162 163 _instrumentation_factory.dispatch.class_instrument(self.class_) 164 165 def __hash__(self): 166 return id(self) 167 168 def __eq__(self, other): 169 return other is self 170 171 @property 172 def is_mapped(self): 173 return "mapper" in self.__dict__ 174 175 @HasMemoized.memoized_attribute 176 def _all_key_set(self): 177 return frozenset(self) 178 179 @HasMemoized.memoized_attribute 180 def _collection_impl_keys(self): 181 return frozenset( 182 [attr.key for attr in self.values() if attr.impl.collection] 183 ) 184 185 @HasMemoized.memoized_attribute 186 def _scalar_loader_impls(self): 187 return frozenset( 188 [ 189 attr.impl 190 for attr in self.values() 191 if attr.impl.accepts_scalar_loader 192 ] 193 ) 194 195 @HasMemoized.memoized_attribute 196 def _loader_impls(self): 197 return frozenset([attr.impl for attr in self.values()]) 198 199 @util.memoized_property 200 def mapper(self): 201 # raises unless self.mapper has been assigned 202 raise exc.UnmappedClassError(self.class_) 203 204 def _all_sqla_attributes(self, exclude=None): 205 """return an iterator of all classbound attributes that are 206 implement :class:`.InspectionAttr`. 207 208 This includes :class:`.QueryableAttribute` as well as extension 209 types such as :class:`.hybrid_property` and 210 :class:`.AssociationProxy`. 211 212 """ 213 214 found = {} 215 216 # constraints: 217 # 1. yield keys in cls.__dict__ order 218 # 2. if a subclass has the same key as a superclass, include that 219 # key as part of the ordering of the superclass, because an 220 # overridden key is usually installed by the mapper which is going 221 # on a different ordering 222 # 3. don't use getattr() as this fires off descriptors 223 224 for supercls in self.class_.__mro__[0:-1]: 225 inherits = supercls.__mro__[1] 226 for key in supercls.__dict__: 227 found.setdefault(key, supercls) 228 if key in inherits.__dict__: 229 continue 230 val = found[key].__dict__[key] 231 if ( 232 isinstance(val, interfaces.InspectionAttr) 233 and val.is_attribute 234 ): 235 yield key, val 236 237 def _get_class_attr_mro(self, key, default=None): 238 """return an attribute on the class without tripping it.""" 239 240 for supercls in self.class_.__mro__: 241 if key in supercls.__dict__: 242 return supercls.__dict__[key] 243 else: 244 return default 245 246 def _attr_has_impl(self, key): 247 """Return True if the given attribute is fully initialized. 248 249 i.e. has an impl. 250 """ 251 252 return key in self and self[key].impl is not None 253 254 def _subclass_manager(self, cls): 255 """Create a new ClassManager for a subclass of this ClassManager's 256 class. 257 258 This is called automatically when attributes are instrumented so that 259 the attributes can be propagated to subclasses against their own 260 class-local manager, without the need for mappers etc. to have already 261 pre-configured managers for the full class hierarchy. Mappers 262 can post-configure the auto-generated ClassManager when needed. 263 264 """ 265 return register_class(cls, finalize=False) 266 267 def _instrument_init(self): 268 self.new_init = _generate_init(self.class_, self, self.original_init) 269 self.install_member("__init__", self.new_init) 270 271 @util.memoized_property 272 def _state_constructor(self): 273 self.dispatch.first_init(self, self.class_) 274 return state.InstanceState 275 276 def manage(self): 277 """Mark this instance as the manager for its class.""" 278 279 setattr(self.class_, self.MANAGER_ATTR, self) 280 281 @util.hybridmethod 282 def manager_getter(self): 283 return _default_manager_getter 284 285 @util.hybridmethod 286 def state_getter(self): 287 """Return a (instance) -> InstanceState callable. 288 289 "state getter" callables should raise either KeyError or 290 AttributeError if no InstanceState could be found for the 291 instance. 292 """ 293 294 return _default_state_getter 295 296 @util.hybridmethod 297 def dict_getter(self): 298 return _default_dict_getter 299 300 def instrument_attribute(self, key, inst, propagated=False): 301 if propagated: 302 if key in self.local_attrs: 303 return # don't override local attr with inherited attr 304 else: 305 self.local_attrs[key] = inst 306 self.install_descriptor(key, inst) 307 self._reset_memoizations() 308 self[key] = inst 309 310 for cls in self.class_.__subclasses__(): 311 manager = self._subclass_manager(cls) 312 manager.instrument_attribute(key, inst, True) 313 314 def subclass_managers(self, recursive): 315 for cls in self.class_.__subclasses__(): 316 mgr = manager_of_class(cls) 317 if mgr is not None and mgr is not self: 318 yield mgr 319 if recursive: 320 for m in mgr.subclass_managers(True): 321 yield m 322 323 def post_configure_attribute(self, key): 324 _instrumentation_factory.dispatch.attribute_instrument( 325 self.class_, key, self[key] 326 ) 327 328 def uninstrument_attribute(self, key, propagated=False): 329 if key not in self: 330 return 331 if propagated: 332 if key in self.local_attrs: 333 return # don't get rid of local attr 334 else: 335 del self.local_attrs[key] 336 self.uninstall_descriptor(key) 337 self._reset_memoizations() 338 del self[key] 339 for cls in self.class_.__subclasses__(): 340 manager = manager_of_class(cls) 341 if manager: 342 manager.uninstrument_attribute(key, True) 343 344 def unregister(self): 345 """remove all instrumentation established by this ClassManager.""" 346 347 for key in list(self.originals): 348 self.uninstall_member(key) 349 350 self.mapper = self.dispatch = self.new_init = None 351 self.info.clear() 352 353 for key in list(self): 354 if key in self.local_attrs: 355 self.uninstrument_attribute(key) 356 357 if self.MANAGER_ATTR in self.class_.__dict__: 358 delattr(self.class_, self.MANAGER_ATTR) 359 360 def install_descriptor(self, key, inst): 361 if key in (self.STATE_ATTR, self.MANAGER_ATTR): 362 raise KeyError( 363 "%r: requested attribute name conflicts with " 364 "instrumentation attribute of the same name." % key 365 ) 366 setattr(self.class_, key, inst) 367 368 def uninstall_descriptor(self, key): 369 delattr(self.class_, key) 370 371 def install_member(self, key, implementation): 372 if key in (self.STATE_ATTR, self.MANAGER_ATTR): 373 raise KeyError( 374 "%r: requested attribute name conflicts with " 375 "instrumentation attribute of the same name." % key 376 ) 377 self.originals.setdefault(key, self.class_.__dict__.get(key, DEL_ATTR)) 378 setattr(self.class_, key, implementation) 379 380 def uninstall_member(self, key): 381 original = self.originals.pop(key, None) 382 if original is not DEL_ATTR: 383 setattr(self.class_, key, original) 384 else: 385 delattr(self.class_, key) 386 387 def instrument_collection_class(self, key, collection_class): 388 return collections.prepare_instrumentation(collection_class) 389 390 def initialize_collection(self, key, state, factory): 391 user_data = factory() 392 adapter = collections.CollectionAdapter( 393 self.get_impl(key), state, user_data 394 ) 395 return adapter, user_data 396 397 def is_instrumented(self, key, search=False): 398 if search: 399 return key in self 400 else: 401 return key in self.local_attrs 402 403 def get_impl(self, key): 404 return self[key].impl 405 406 @property 407 def attributes(self): 408 return iter(self.values()) 409 410 # InstanceState management 411 412 def new_instance(self, state=None): 413 instance = self.class_.__new__(self.class_) 414 if state is None: 415 state = self._state_constructor(instance, self) 416 self._state_setter(instance, state) 417 return instance 418 419 def setup_instance(self, instance, state=None): 420 if state is None: 421 state = self._state_constructor(instance, self) 422 self._state_setter(instance, state) 423 424 def teardown_instance(self, instance): 425 delattr(instance, self.STATE_ATTR) 426 427 def _serialize(self, state, state_dict): 428 return _SerializeManager(state, state_dict) 429 430 def _new_state_if_none(self, instance): 431 """Install a default InstanceState if none is present. 432 433 A private convenience method used by the __init__ decorator. 434 435 """ 436 if hasattr(instance, self.STATE_ATTR): 437 return False 438 elif self.class_ is not instance.__class__ and self.is_mapped: 439 # this will create a new ClassManager for the 440 # subclass, without a mapper. This is likely a 441 # user error situation but allow the object 442 # to be constructed, so that it is usable 443 # in a non-ORM context at least. 444 return self._subclass_manager( 445 instance.__class__ 446 )._new_state_if_none(instance) 447 else: 448 state = self._state_constructor(instance, self) 449 self._state_setter(instance, state) 450 return state 451 452 def has_state(self, instance): 453 return hasattr(instance, self.STATE_ATTR) 454 455 def has_parent(self, state, key, optimistic=False): 456 """TODO""" 457 return self.get_impl(key).hasparent(state, optimistic=optimistic) 458 459 def __bool__(self): 460 """All ClassManagers are non-zero regardless of attribute state.""" 461 return True 462 463 __nonzero__ = __bool__ 464 465 def __repr__(self): 466 return "<%s of %r at %x>" % ( 467 self.__class__.__name__, 468 self.class_, 469 id(self), 470 ) 471 472 473class _SerializeManager(object): 474 """Provide serialization of a :class:`.ClassManager`. 475 476 The :class:`.InstanceState` uses ``__init__()`` on serialize 477 and ``__call__()`` on deserialize. 478 479 """ 480 481 def __init__(self, state, d): 482 self.class_ = state.class_ 483 manager = state.manager 484 manager.dispatch.pickle(state, d) 485 486 def __call__(self, state, inst, state_dict): 487 state.manager = manager = manager_of_class(self.class_) 488 if manager is None: 489 raise exc.UnmappedInstanceError( 490 inst, 491 "Cannot deserialize object of type %r - " 492 "no mapper() has " 493 "been configured for this class within the current " 494 "Python process!" % self.class_, 495 ) 496 elif manager.is_mapped and not manager.mapper.configured: 497 manager.mapper._check_configure() 498 499 # setup _sa_instance_state ahead of time so that 500 # unpickle events can access the object normally. 501 # see [ticket:2362] 502 if inst is not None: 503 manager.setup_instance(inst, state) 504 manager.dispatch.unpickle(state, state_dict) 505 506 507class InstrumentationFactory(object): 508 """Factory for new ClassManager instances.""" 509 510 def create_manager_for_cls(self, class_): 511 assert class_ is not None 512 assert manager_of_class(class_) is None 513 514 # give a more complicated subclass 515 # a chance to do what it wants here 516 manager, factory = self._locate_extended_factory(class_) 517 518 if factory is None: 519 factory = ClassManager 520 manager = factory(class_) 521 522 self._check_conflicts(class_, factory) 523 524 manager.factory = factory 525 526 return manager 527 528 def _locate_extended_factory(self, class_): 529 """Overridden by a subclass to do an extended lookup.""" 530 return None, None 531 532 def _check_conflicts(self, class_, factory): 533 """Overridden by a subclass to test for conflicting factories.""" 534 return 535 536 def unregister(self, class_): 537 manager = manager_of_class(class_) 538 manager.unregister() 539 self.dispatch.class_uninstrument(class_) 540 541 542# this attribute is replaced by sqlalchemy.ext.instrumentation 543# when imported. 544_instrumentation_factory = InstrumentationFactory() 545 546# these attributes are replaced by sqlalchemy.ext.instrumentation 547# when a non-standard InstrumentationManager class is first 548# used to instrument a class. 549instance_state = _default_state_getter = base.instance_state 550 551instance_dict = _default_dict_getter = base.instance_dict 552 553manager_of_class = _default_manager_getter = base.manager_of_class 554 555 556def register_class( 557 class_, 558 finalize=True, 559 mapper=None, 560 registry=None, 561 declarative_scan=None, 562 expired_attribute_loader=None, 563 init_method=None, 564): 565 """Register class instrumentation. 566 567 Returns the existing or newly created class manager. 568 569 """ 570 571 manager = manager_of_class(class_) 572 if manager is None: 573 manager = _instrumentation_factory.create_manager_for_cls(class_) 574 manager._update_state( 575 mapper=mapper, 576 registry=registry, 577 declarative_scan=declarative_scan, 578 expired_attribute_loader=expired_attribute_loader, 579 init_method=init_method, 580 finalize=finalize, 581 ) 582 583 return manager 584 585 586def unregister_class(class_): 587 """Unregister class instrumentation.""" 588 589 _instrumentation_factory.unregister(class_) 590 591 592def is_instrumented(instance, key): 593 """Return True if the given attribute on the given instance is 594 instrumented by the attributes package. 595 596 This function may be used regardless of instrumentation 597 applied directly to the class, i.e. no descriptors are required. 598 599 """ 600 return manager_of_class(instance.__class__).is_instrumented( 601 key, search=True 602 ) 603 604 605def _generate_init(class_, class_manager, original_init): 606 """Build an __init__ decorator that triggers ClassManager events.""" 607 608 # TODO: we should use the ClassManager's notion of the 609 # original '__init__' method, once ClassManager is fixed 610 # to always reference that. 611 612 if original_init is None: 613 original_init = class_.__init__ 614 615 # Go through some effort here and don't change the user's __init__ 616 # calling signature, including the unlikely case that it has 617 # a return value. 618 # FIXME: need to juggle local names to avoid constructor argument 619 # clashes. 620 func_body = """\ 621def __init__(%(apply_pos)s): 622 new_state = class_manager._new_state_if_none(%(self_arg)s) 623 if new_state: 624 return new_state._initialize_instance(%(apply_kw)s) 625 else: 626 return original_init(%(apply_kw)s) 627""" 628 func_vars = util.format_argspec_init(original_init, grouped=False) 629 func_text = func_body % func_vars 630 631 if util.py2k: 632 func = getattr(original_init, "im_func", original_init) 633 func_defaults = getattr(func, "func_defaults", None) 634 else: 635 func_defaults = getattr(original_init, "__defaults__", None) 636 func_kw_defaults = getattr(original_init, "__kwdefaults__", None) 637 638 env = locals().copy() 639 env["__name__"] = __name__ 640 exec(func_text, env) 641 __init__ = env["__init__"] 642 __init__.__doc__ = original_init.__doc__ 643 __init__._sa_original_init = original_init 644 645 if func_defaults: 646 __init__.__defaults__ = func_defaults 647 if not util.py2k and func_kw_defaults: 648 __init__.__kwdefaults__ = func_kw_defaults 649 650 return __init__ 651