1"""Extensible class instrumentation. 2 3The :mod:`sqlalchemy.ext.instrumentation` package provides for alternate 4systems of class instrumentation within the ORM. Class instrumentation 5refers to how the ORM places attributes on the class which maintain 6data and track changes to that data, as well as event hooks installed 7on the class. 8 9.. note:: 10 The extension package is provided for the benefit of integration 11 with other object management packages, which already perform 12 their own instrumentation. It is not intended for general use. 13 14For examples of how the instrumentation extension is used, 15see the example :ref:`examples_instrumentation`. 16 17""" 18import weakref 19 20from .. import util 21from ..orm import attributes 22from ..orm import base as orm_base 23from ..orm import collections 24from ..orm import exc as orm_exc 25from ..orm import instrumentation as orm_instrumentation 26from ..orm.instrumentation import _default_dict_getter 27from ..orm.instrumentation import _default_manager_getter 28from ..orm.instrumentation import _default_state_getter 29from ..orm.instrumentation import ClassManager 30from ..orm.instrumentation import InstrumentationFactory 31 32INSTRUMENTATION_MANAGER = "__sa_instrumentation_manager__" 33"""Attribute, elects custom instrumentation when present on a mapped class. 34 35Allows a class to specify a slightly or wildly different technique for 36tracking changes made to mapped attributes and collections. 37 38Only one instrumentation implementation is allowed in a given object 39inheritance hierarchy. 40 41The value of this attribute must be a callable and will be passed a class 42object. The callable must return one of: 43 44 - An instance of an InstrumentationManager or subclass 45 - An object implementing all or some of InstrumentationManager (TODO) 46 - A dictionary of callables, implementing all or some of the above (TODO) 47 - An instance of a ClassManager or subclass 48 49This attribute is consulted by SQLAlchemy instrumentation 50resolution, once the :mod:`sqlalchemy.ext.instrumentation` module 51has been imported. If custom finders are installed in the global 52instrumentation_finders list, they may or may not choose to honor this 53attribute. 54 55""" 56 57 58def find_native_user_instrumentation_hook(cls): 59 """Find user-specified instrumentation management for a class.""" 60 return getattr(cls, INSTRUMENTATION_MANAGER, None) 61 62 63instrumentation_finders = [find_native_user_instrumentation_hook] 64"""An extensible sequence of callables which return instrumentation 65implementations 66 67When a class is registered, each callable will be passed a class object. 68If None is returned, the 69next finder in the sequence is consulted. Otherwise the return must be an 70instrumentation factory that follows the same guidelines as 71sqlalchemy.ext.instrumentation.INSTRUMENTATION_MANAGER. 72 73By default, the only finder is find_native_user_instrumentation_hook, which 74searches for INSTRUMENTATION_MANAGER. If all finders return None, standard 75ClassManager instrumentation is used. 76 77""" 78 79 80class ExtendedInstrumentationRegistry(InstrumentationFactory): 81 """Extends :class:`.InstrumentationFactory` with additional 82 bookkeeping, to accommodate multiple types of 83 class managers. 84 85 """ 86 87 _manager_finders = weakref.WeakKeyDictionary() 88 _state_finders = weakref.WeakKeyDictionary() 89 _dict_finders = weakref.WeakKeyDictionary() 90 _extended = False 91 92 def _locate_extended_factory(self, class_): 93 for finder in instrumentation_finders: 94 factory = finder(class_) 95 if factory is not None: 96 manager = self._extended_class_manager(class_, factory) 97 return manager, factory 98 else: 99 return None, None 100 101 def _check_conflicts(self, class_, factory): 102 existing_factories = self._collect_management_factories_for( 103 class_ 104 ).difference([factory]) 105 if existing_factories: 106 raise TypeError( 107 "multiple instrumentation implementations specified " 108 "in %s inheritance hierarchy: %r" 109 % (class_.__name__, list(existing_factories)) 110 ) 111 112 def _extended_class_manager(self, class_, factory): 113 manager = factory(class_) 114 if not isinstance(manager, ClassManager): 115 manager = _ClassInstrumentationAdapter(class_, manager) 116 117 if factory != ClassManager and not self._extended: 118 # somebody invoked a custom ClassManager. 119 # reinstall global "getter" functions with the more 120 # expensive ones. 121 self._extended = True 122 _install_instrumented_lookups() 123 124 self._manager_finders[class_] = manager.manager_getter() 125 self._state_finders[class_] = manager.state_getter() 126 self._dict_finders[class_] = manager.dict_getter() 127 return manager 128 129 def _collect_management_factories_for(self, cls): 130 """Return a collection of factories in play or specified for a 131 hierarchy. 132 133 Traverses the entire inheritance graph of a cls and returns a 134 collection of instrumentation factories for those classes. Factories 135 are extracted from active ClassManagers, if available, otherwise 136 instrumentation_finders is consulted. 137 138 """ 139 hierarchy = util.class_hierarchy(cls) 140 factories = set() 141 for member in hierarchy: 142 manager = self.manager_of_class(member) 143 if manager is not None: 144 factories.add(manager.factory) 145 else: 146 for finder in instrumentation_finders: 147 factory = finder(member) 148 if factory is not None: 149 break 150 else: 151 factory = None 152 factories.add(factory) 153 factories.discard(None) 154 return factories 155 156 def unregister(self, class_): 157 if class_ in self._manager_finders: 158 del self._manager_finders[class_] 159 del self._state_finders[class_] 160 del self._dict_finders[class_] 161 super(ExtendedInstrumentationRegistry, self).unregister(class_) 162 163 def manager_of_class(self, cls): 164 if cls is None: 165 return None 166 try: 167 finder = self._manager_finders.get(cls, _default_manager_getter) 168 except TypeError: 169 # due to weakref lookup on invalid object 170 return None 171 else: 172 return finder(cls) 173 174 def state_of(self, instance): 175 if instance is None: 176 raise AttributeError("None has no persistent state.") 177 return self._state_finders.get( 178 instance.__class__, _default_state_getter 179 )(instance) 180 181 def dict_of(self, instance): 182 if instance is None: 183 raise AttributeError("None has no persistent state.") 184 return self._dict_finders.get( 185 instance.__class__, _default_dict_getter 186 )(instance) 187 188 189orm_instrumentation._instrumentation_factory = ( 190 _instrumentation_factory 191) = ExtendedInstrumentationRegistry() 192orm_instrumentation.instrumentation_finders = instrumentation_finders 193 194 195class InstrumentationManager(object): 196 """User-defined class instrumentation extension. 197 198 :class:`.InstrumentationManager` can be subclassed in order 199 to change 200 how class instrumentation proceeds. This class exists for 201 the purposes of integration with other object management 202 frameworks which would like to entirely modify the 203 instrumentation methodology of the ORM, and is not intended 204 for regular usage. For interception of class instrumentation 205 events, see :class:`.InstrumentationEvents`. 206 207 The API for this class should be considered as semi-stable, 208 and may change slightly with new releases. 209 210 """ 211 212 # r4361 added a mandatory (cls) constructor to this interface. 213 # given that, perhaps class_ should be dropped from all of these 214 # signatures. 215 216 def __init__(self, class_): 217 pass 218 219 def manage(self, class_, manager): 220 setattr(class_, "_default_class_manager", manager) 221 222 def dispose(self, class_, manager): 223 delattr(class_, "_default_class_manager") 224 225 def manager_getter(self, class_): 226 def get(cls): 227 return cls._default_class_manager 228 229 return get 230 231 def instrument_attribute(self, class_, key, inst): 232 pass 233 234 def post_configure_attribute(self, class_, key, inst): 235 pass 236 237 def install_descriptor(self, class_, key, inst): 238 setattr(class_, key, inst) 239 240 def uninstall_descriptor(self, class_, key): 241 delattr(class_, key) 242 243 def install_member(self, class_, key, implementation): 244 setattr(class_, key, implementation) 245 246 def uninstall_member(self, class_, key): 247 delattr(class_, key) 248 249 def instrument_collection_class(self, class_, key, collection_class): 250 return collections.prepare_instrumentation(collection_class) 251 252 def get_instance_dict(self, class_, instance): 253 return instance.__dict__ 254 255 def initialize_instance_dict(self, class_, instance): 256 pass 257 258 def install_state(self, class_, instance, state): 259 setattr(instance, "_default_state", state) 260 261 def remove_state(self, class_, instance): 262 delattr(instance, "_default_state") 263 264 def state_getter(self, class_): 265 return lambda instance: getattr(instance, "_default_state") 266 267 def dict_getter(self, class_): 268 return lambda inst: self.get_instance_dict(class_, inst) 269 270 271class _ClassInstrumentationAdapter(ClassManager): 272 """Adapts a user-defined InstrumentationManager to a ClassManager.""" 273 274 def __init__(self, class_, override): 275 self._adapted = override 276 self._get_state = self._adapted.state_getter(class_) 277 self._get_dict = self._adapted.dict_getter(class_) 278 279 ClassManager.__init__(self, class_) 280 281 def manage(self): 282 self._adapted.manage(self.class_, self) 283 284 def dispose(self): 285 self._adapted.dispose(self.class_) 286 287 def manager_getter(self): 288 return self._adapted.manager_getter(self.class_) 289 290 def instrument_attribute(self, key, inst, propagated=False): 291 ClassManager.instrument_attribute(self, key, inst, propagated) 292 if not propagated: 293 self._adapted.instrument_attribute(self.class_, key, inst) 294 295 def post_configure_attribute(self, key): 296 super(_ClassInstrumentationAdapter, self).post_configure_attribute(key) 297 self._adapted.post_configure_attribute(self.class_, key, self[key]) 298 299 def install_descriptor(self, key, inst): 300 self._adapted.install_descriptor(self.class_, key, inst) 301 302 def uninstall_descriptor(self, key): 303 self._adapted.uninstall_descriptor(self.class_, key) 304 305 def install_member(self, key, implementation): 306 self._adapted.install_member(self.class_, key, implementation) 307 308 def uninstall_member(self, key): 309 self._adapted.uninstall_member(self.class_, key) 310 311 def instrument_collection_class(self, key, collection_class): 312 return self._adapted.instrument_collection_class( 313 self.class_, key, collection_class 314 ) 315 316 def initialize_collection(self, key, state, factory): 317 delegate = getattr(self._adapted, "initialize_collection", None) 318 if delegate: 319 return delegate(key, state, factory) 320 else: 321 return ClassManager.initialize_collection( 322 self, key, state, factory 323 ) 324 325 def new_instance(self, state=None): 326 instance = self.class_.__new__(self.class_) 327 self.setup_instance(instance, state) 328 return instance 329 330 def _new_state_if_none(self, instance): 331 """Install a default InstanceState if none is present. 332 333 A private convenience method used by the __init__ decorator. 334 """ 335 if self.has_state(instance): 336 return False 337 else: 338 return self.setup_instance(instance) 339 340 def setup_instance(self, instance, state=None): 341 self._adapted.initialize_instance_dict(self.class_, instance) 342 343 if state is None: 344 state = self._state_constructor(instance, self) 345 346 # the given instance is assumed to have no state 347 self._adapted.install_state(self.class_, instance, state) 348 return state 349 350 def teardown_instance(self, instance): 351 self._adapted.remove_state(self.class_, instance) 352 353 def has_state(self, instance): 354 try: 355 self._get_state(instance) 356 except orm_exc.NO_STATE: 357 return False 358 else: 359 return True 360 361 def state_getter(self): 362 return self._get_state 363 364 def dict_getter(self): 365 return self._get_dict 366 367 368def _install_instrumented_lookups(): 369 """Replace global class/object management functions 370 with ExtendedInstrumentationRegistry implementations, which 371 allow multiple types of class managers to be present, 372 at the cost of performance. 373 374 This function is called only by ExtendedInstrumentationRegistry 375 and unit tests specific to this behavior. 376 377 The _reinstall_default_lookups() function can be called 378 after this one to re-establish the default functions. 379 380 """ 381 _install_lookups( 382 dict( 383 instance_state=_instrumentation_factory.state_of, 384 instance_dict=_instrumentation_factory.dict_of, 385 manager_of_class=_instrumentation_factory.manager_of_class, 386 ) 387 ) 388 389 390def _reinstall_default_lookups(): 391 """Restore simplified lookups.""" 392 _install_lookups( 393 dict( 394 instance_state=_default_state_getter, 395 instance_dict=_default_dict_getter, 396 manager_of_class=_default_manager_getter, 397 ) 398 ) 399 _instrumentation_factory._extended = False 400 401 402def _install_lookups(lookups): 403 global instance_state, instance_dict, manager_of_class 404 instance_state = lookups["instance_state"] 405 instance_dict = lookups["instance_dict"] 406 manager_of_class = lookups["manager_of_class"] 407 orm_base.instance_state = ( 408 attributes.instance_state 409 ) = orm_instrumentation.instance_state = instance_state 410 orm_base.instance_dict = ( 411 attributes.instance_dict 412 ) = orm_instrumentation.instance_dict = instance_dict 413 orm_base.manager_of_class = ( 414 attributes.manager_of_class 415 ) = orm_instrumentation.manager_of_class = manager_of_class 416