| 1 | # interfaces.py |
|---|
| 2 | # Copyright (C) 2005, 2006, 2007, 2008, 2009 Michael Bayer mike_mp@zzzcomputing.com |
|---|
| 3 | # |
|---|
| 4 | # This module is part of SQLAlchemy and is released under |
|---|
| 5 | # the MIT License: http://www.opensource.org/licenses/mit-license.php |
|---|
| 6 | |
|---|
| 7 | """ |
|---|
| 8 | |
|---|
| 9 | Semi-private module containing various base classes used throughout the ORM. |
|---|
| 10 | |
|---|
| 11 | Defines the extension classes :class:`MapperExtension`, |
|---|
| 12 | :class:`SessionExtension`, and :class:`AttributeExtension` as |
|---|
| 13 | well as other user-subclassable extension objects. |
|---|
| 14 | |
|---|
| 15 | """ |
|---|
| 16 | |
|---|
| 17 | from itertools import chain |
|---|
| 18 | |
|---|
| 19 | import sqlalchemy.exceptions as sa_exc |
|---|
| 20 | from sqlalchemy import log, util |
|---|
| 21 | from sqlalchemy.sql import expression |
|---|
| 22 | |
|---|
| 23 | class_mapper = None |
|---|
| 24 | collections = None |
|---|
| 25 | |
|---|
| 26 | __all__ = ( |
|---|
| 27 | 'AttributeExtension', |
|---|
| 28 | 'EXT_CONTINUE', |
|---|
| 29 | 'EXT_STOP', |
|---|
| 30 | 'ExtensionOption', |
|---|
| 31 | 'InstrumentationManager', |
|---|
| 32 | 'LoaderStrategy', |
|---|
| 33 | 'MapperExtension', |
|---|
| 34 | 'MapperOption', |
|---|
| 35 | 'MapperProperty', |
|---|
| 36 | 'PropComparator', |
|---|
| 37 | 'PropertyOption', |
|---|
| 38 | 'SessionExtension', |
|---|
| 39 | 'StrategizedOption', |
|---|
| 40 | 'StrategizedProperty', |
|---|
| 41 | 'build_path', |
|---|
| 42 | ) |
|---|
| 43 | |
|---|
| 44 | EXT_CONTINUE = util.symbol('EXT_CONTINUE') |
|---|
| 45 | EXT_STOP = util.symbol('EXT_STOP') |
|---|
| 46 | |
|---|
| 47 | ONETOMANY = util.symbol('ONETOMANY') |
|---|
| 48 | MANYTOONE = util.symbol('MANYTOONE') |
|---|
| 49 | MANYTOMANY = util.symbol('MANYTOMANY') |
|---|
| 50 | |
|---|
| 51 | class MapperExtension(object): |
|---|
| 52 | """Base implementation for customizing Mapper behavior. |
|---|
| 53 | |
|---|
| 54 | For each method in MapperExtension, returning a result of EXT_CONTINUE |
|---|
| 55 | will allow processing to continue to the next MapperExtension in line or |
|---|
| 56 | use the default functionality if there are no other extensions. |
|---|
| 57 | |
|---|
| 58 | Returning EXT_STOP will halt processing of further extensions handling |
|---|
| 59 | that method. Some methods such as ``load`` have other return |
|---|
| 60 | requirements, see the individual documentation for details. Other than |
|---|
| 61 | these exception cases, any return value other than EXT_CONTINUE or |
|---|
| 62 | EXT_STOP will be interpreted as equivalent to EXT_STOP. |
|---|
| 63 | |
|---|
| 64 | """ |
|---|
| 65 | def instrument_class(self, mapper, class_): |
|---|
| 66 | return EXT_CONTINUE |
|---|
| 67 | |
|---|
| 68 | def init_instance(self, mapper, class_, oldinit, instance, args, kwargs): |
|---|
| 69 | return EXT_CONTINUE |
|---|
| 70 | |
|---|
| 71 | def init_failed(self, mapper, class_, oldinit, instance, args, kwargs): |
|---|
| 72 | return EXT_CONTINUE |
|---|
| 73 | |
|---|
| 74 | def translate_row(self, mapper, context, row): |
|---|
| 75 | """Perform pre-processing on the given result row and return a |
|---|
| 76 | new row instance. |
|---|
| 77 | |
|---|
| 78 | This is called when the mapper first receives a row, before |
|---|
| 79 | the object identity or the instance itself has been derived |
|---|
| 80 | from that row. |
|---|
| 81 | |
|---|
| 82 | """ |
|---|
| 83 | return EXT_CONTINUE |
|---|
| 84 | |
|---|
| 85 | def create_instance(self, mapper, selectcontext, row, class_): |
|---|
| 86 | """Receive a row when a new object instance is about to be |
|---|
| 87 | created from that row. |
|---|
| 88 | |
|---|
| 89 | The method can choose to create the instance itself, or it can return |
|---|
| 90 | EXT_CONTINUE to indicate normal object creation should take place. |
|---|
| 91 | |
|---|
| 92 | mapper |
|---|
| 93 | The mapper doing the operation |
|---|
| 94 | |
|---|
| 95 | selectcontext |
|---|
| 96 | SelectionContext corresponding to the instances() call |
|---|
| 97 | |
|---|
| 98 | row |
|---|
| 99 | The result row from the database |
|---|
| 100 | |
|---|
| 101 | class\_ |
|---|
| 102 | The class we are mapping. |
|---|
| 103 | |
|---|
| 104 | return value |
|---|
| 105 | A new object instance, or EXT_CONTINUE |
|---|
| 106 | |
|---|
| 107 | """ |
|---|
| 108 | return EXT_CONTINUE |
|---|
| 109 | |
|---|
| 110 | def append_result(self, mapper, selectcontext, row, instance, result, **flags): |
|---|
| 111 | """Receive an object instance before that instance is appended |
|---|
| 112 | to a result list. |
|---|
| 113 | |
|---|
| 114 | If this method returns EXT_CONTINUE, result appending will proceed |
|---|
| 115 | normally. if this method returns any other value or None, |
|---|
| 116 | result appending will not proceed for this instance, giving |
|---|
| 117 | this extension an opportunity to do the appending itself, if |
|---|
| 118 | desired. |
|---|
| 119 | |
|---|
| 120 | mapper |
|---|
| 121 | The mapper doing the operation. |
|---|
| 122 | |
|---|
| 123 | selectcontext |
|---|
| 124 | SelectionContext corresponding to the instances() call. |
|---|
| 125 | |
|---|
| 126 | row |
|---|
| 127 | The result row from the database. |
|---|
| 128 | |
|---|
| 129 | instance |
|---|
| 130 | The object instance to be appended to the result. |
|---|
| 131 | |
|---|
| 132 | result |
|---|
| 133 | List to which results are being appended. |
|---|
| 134 | |
|---|
| 135 | \**flags |
|---|
| 136 | extra information about the row, same as criterion in |
|---|
| 137 | ``create_row_processor()`` method of :class:`~sqlalchemy.orm.interfaces.MapperProperty` |
|---|
| 138 | """ |
|---|
| 139 | |
|---|
| 140 | return EXT_CONTINUE |
|---|
| 141 | |
|---|
| 142 | def populate_instance(self, mapper, selectcontext, row, instance, **flags): |
|---|
| 143 | """Receive an instance before that instance has |
|---|
| 144 | its attributes populated. |
|---|
| 145 | |
|---|
| 146 | This usually corresponds to a newly loaded instance but may |
|---|
| 147 | also correspond to an already-loaded instance which has |
|---|
| 148 | unloaded attributes to be populated. The method may be called |
|---|
| 149 | many times for a single instance, as multiple result rows are |
|---|
| 150 | used to populate eagerly loaded collections. |
|---|
| 151 | |
|---|
| 152 | If this method returns EXT_CONTINUE, instance population will |
|---|
| 153 | proceed normally. If any other value or None is returned, |
|---|
| 154 | instance population will not proceed, giving this extension an |
|---|
| 155 | opportunity to populate the instance itself, if desired. |
|---|
| 156 | |
|---|
| 157 | As of 0.5, most usages of this hook are obsolete. For a |
|---|
| 158 | generic "object has been newly created from a row" hook, use |
|---|
| 159 | ``reconstruct_instance()``, or the ``@orm.reconstructor`` |
|---|
| 160 | decorator. |
|---|
| 161 | |
|---|
| 162 | """ |
|---|
| 163 | return EXT_CONTINUE |
|---|
| 164 | |
|---|
| 165 | def reconstruct_instance(self, mapper, instance): |
|---|
| 166 | """Receive an object instance after it has been created via |
|---|
| 167 | ``__new__``, and after initial attribute population has |
|---|
| 168 | occurred. |
|---|
| 169 | |
|---|
| 170 | This typically occurs when the instance is created based on |
|---|
| 171 | incoming result rows, and is only called once for that |
|---|
| 172 | instance's lifetime. |
|---|
| 173 | |
|---|
| 174 | Note that during a result-row load, this method is called upon |
|---|
| 175 | the first row received for this instance. If eager loaders are |
|---|
| 176 | set to further populate collections on the instance, those |
|---|
| 177 | will *not* yet be completely loaded. |
|---|
| 178 | |
|---|
| 179 | """ |
|---|
| 180 | return EXT_CONTINUE |
|---|
| 181 | |
|---|
| 182 | def before_insert(self, mapper, connection, instance): |
|---|
| 183 | """Receive an object instance before that instance is INSERTed |
|---|
| 184 | into its table. |
|---|
| 185 | |
|---|
| 186 | This is a good place to set up primary key values and such |
|---|
| 187 | that aren't handled otherwise. |
|---|
| 188 | |
|---|
| 189 | Column-based attributes can be modified within this method |
|---|
| 190 | which will result in the new value being inserted. However |
|---|
| 191 | *no* changes to the overall flush plan can be made; this means |
|---|
| 192 | any collection modification or save() operations which occur |
|---|
| 193 | within this method will not take effect until the next flush |
|---|
| 194 | call. |
|---|
| 195 | |
|---|
| 196 | """ |
|---|
| 197 | |
|---|
| 198 | return EXT_CONTINUE |
|---|
| 199 | |
|---|
| 200 | def after_insert(self, mapper, connection, instance): |
|---|
| 201 | """Receive an object instance after that instance is INSERTed.""" |
|---|
| 202 | |
|---|
| 203 | return EXT_CONTINUE |
|---|
| 204 | |
|---|
| 205 | def before_update(self, mapper, connection, instance): |
|---|
| 206 | """Receive an object instance before that instance is UPDATEed. |
|---|
| 207 | |
|---|
| 208 | Note that this method is called for all instances that are marked as |
|---|
| 209 | "dirty", even those which have no net changes to their column-based |
|---|
| 210 | attributes. An object is marked as dirty when any of its column-based |
|---|
| 211 | attributes have a "set attribute" operation called or when any of its |
|---|
| 212 | collections are modified. If, at update time, no column-based attributes |
|---|
| 213 | have any net changes, no UPDATE statement will be issued. This means |
|---|
| 214 | that an instance being sent to before_update is *not* a guarantee that |
|---|
| 215 | an UPDATE statement will be issued (although you can affect the outcome |
|---|
| 216 | here). |
|---|
| 217 | |
|---|
| 218 | To detect if the column-based attributes on the object have net changes, |
|---|
| 219 | and will therefore generate an UPDATE statement, use |
|---|
| 220 | ``object_session(instance).is_modified(instance, include_collections=False)``. |
|---|
| 221 | |
|---|
| 222 | Column-based attributes can be modified within this method which will |
|---|
| 223 | result in their being updated. However *no* changes to the overall |
|---|
| 224 | flush plan can be made; this means any collection modification or |
|---|
| 225 | save() operations which occur within this method will not take effect |
|---|
| 226 | until the next flush call. |
|---|
| 227 | |
|---|
| 228 | """ |
|---|
| 229 | |
|---|
| 230 | return EXT_CONTINUE |
|---|
| 231 | |
|---|
| 232 | def after_update(self, mapper, connection, instance): |
|---|
| 233 | """Receive an object instance after that instance is UPDATEed.""" |
|---|
| 234 | |
|---|
| 235 | return EXT_CONTINUE |
|---|
| 236 | |
|---|
| 237 | def before_delete(self, mapper, connection, instance): |
|---|
| 238 | """Receive an object instance before that instance is DELETEed. |
|---|
| 239 | |
|---|
| 240 | Note that *no* changes to the overall |
|---|
| 241 | flush plan can be made here; this means any collection modification, |
|---|
| 242 | save() or delete() operations which occur within this method will |
|---|
| 243 | not take effect until the next flush call. |
|---|
| 244 | |
|---|
| 245 | """ |
|---|
| 246 | |
|---|
| 247 | return EXT_CONTINUE |
|---|
| 248 | |
|---|
| 249 | def after_delete(self, mapper, connection, instance): |
|---|
| 250 | """Receive an object instance after that instance is DELETEed.""" |
|---|
| 251 | |
|---|
| 252 | return EXT_CONTINUE |
|---|
| 253 | |
|---|
| 254 | class SessionExtension(object): |
|---|
| 255 | """An extension hook object for Sessions. Subclasses may be installed into a Session |
|---|
| 256 | (or sessionmaker) using the ``extension`` keyword argument. |
|---|
| 257 | """ |
|---|
| 258 | |
|---|
| 259 | def before_commit(self, session): |
|---|
| 260 | """Execute right before commit is called. |
|---|
| 261 | |
|---|
| 262 | Note that this may not be per-flush if a longer running transaction is ongoing.""" |
|---|
| 263 | |
|---|
| 264 | def after_commit(self, session): |
|---|
| 265 | """Execute after a commit has occured. |
|---|
| 266 | |
|---|
| 267 | Note that this may not be per-flush if a longer running transaction is ongoing.""" |
|---|
| 268 | |
|---|
| 269 | def after_rollback(self, session): |
|---|
| 270 | """Execute after a rollback has occured. |
|---|
| 271 | |
|---|
| 272 | Note that this may not be per-flush if a longer running transaction is ongoing.""" |
|---|
| 273 | |
|---|
| 274 | def before_flush(self, session, flush_context, instances): |
|---|
| 275 | """Execute before flush process has started. |
|---|
| 276 | |
|---|
| 277 | `instances` is an optional list of objects which were passed to the ``flush()`` |
|---|
| 278 | method. |
|---|
| 279 | """ |
|---|
| 280 | |
|---|
| 281 | def after_flush(self, session, flush_context): |
|---|
| 282 | """Execute after flush has completed, but before commit has been called. |
|---|
| 283 | |
|---|
| 284 | Note that the session's state is still in pre-flush, i.e. 'new', 'dirty', |
|---|
| 285 | and 'deleted' lists still show pre-flush state as well as the history |
|---|
| 286 | settings on instance attributes.""" |
|---|
| 287 | |
|---|
| 288 | def after_flush_postexec(self, session, flush_context): |
|---|
| 289 | """Execute after flush has completed, and after the post-exec state occurs. |
|---|
| 290 | |
|---|
| 291 | This will be when the 'new', 'dirty', and 'deleted' lists are in their final |
|---|
| 292 | state. An actual commit() may or may not have occured, depending on whether or not |
|---|
| 293 | the flush started its own transaction or participated in a larger transaction. |
|---|
| 294 | """ |
|---|
| 295 | |
|---|
| 296 | def after_begin(self, session, transaction, connection): |
|---|
| 297 | """Execute after a transaction is begun on a connection |
|---|
| 298 | |
|---|
| 299 | `transaction` is the SessionTransaction. This method is called after an |
|---|
| 300 | engine level transaction is begun on a connection. |
|---|
| 301 | """ |
|---|
| 302 | |
|---|
| 303 | def after_attach(self, session, instance): |
|---|
| 304 | """Execute after an instance is attached to a session. |
|---|
| 305 | |
|---|
| 306 | This is called after an add, delete or merge. |
|---|
| 307 | """ |
|---|
| 308 | |
|---|
| 309 | def after_bulk_update(self, session, query, query_context, result): |
|---|
| 310 | """Execute after a bulk update operation to the session. |
|---|
| 311 | |
|---|
| 312 | This is called after a session.query(...).update() |
|---|
| 313 | |
|---|
| 314 | `query` is the query object that this update operation was called on. |
|---|
| 315 | `query_context` was the query context object. |
|---|
| 316 | `result` is the result object returned from the bulk operation. |
|---|
| 317 | """ |
|---|
| 318 | |
|---|
| 319 | def after_bulk_delete(self, session, query, query_context, result): |
|---|
| 320 | """Execute after a bulk delete operation to the session. |
|---|
| 321 | |
|---|
| 322 | This is called after a session.query(...).delete() |
|---|
| 323 | |
|---|
| 324 | `query` is the query object that this delete operation was called on. |
|---|
| 325 | `query_context` was the query context object. |
|---|
| 326 | `result` is the result object returned from the bulk operation. |
|---|
| 327 | """ |
|---|
| 328 | |
|---|
| 329 | class MapperProperty(object): |
|---|
| 330 | """Manage the relationship of a ``Mapper`` to a single class |
|---|
| 331 | attribute, as well as that attribute as it appears on individual |
|---|
| 332 | instances of the class, including attribute instrumentation, |
|---|
| 333 | attribute access, loading behavior, and dependency calculations. |
|---|
| 334 | """ |
|---|
| 335 | |
|---|
| 336 | def setup(self, context, entity, path, adapter, **kwargs): |
|---|
| 337 | """Called by Query for the purposes of constructing a SQL statement. |
|---|
| 338 | |
|---|
| 339 | Each MapperProperty associated with the target mapper processes the |
|---|
| 340 | statement referenced by the query context, adding columns and/or |
|---|
| 341 | criterion as appropriate. |
|---|
| 342 | """ |
|---|
| 343 | |
|---|
| 344 | pass |
|---|
| 345 | |
|---|
| 346 | def create_row_processor(self, selectcontext, path, mapper, row, adapter): |
|---|
| 347 | """Return a 2-tuple consiting of two row processing functions and an instance post-processing function. |
|---|
| 348 | |
|---|
| 349 | Input arguments are the query.SelectionContext and the *first* |
|---|
| 350 | applicable row of a result set obtained within |
|---|
| 351 | query.Query.instances(), called only the first time a particular |
|---|
| 352 | mapper's populate_instance() method is invoked for the overall result. |
|---|
| 353 | |
|---|
| 354 | The settings contained within the SelectionContext as well as the |
|---|
| 355 | columns present in the row (which will be the same columns present in |
|---|
| 356 | all rows) are used to determine the presence and behavior of the |
|---|
| 357 | returned callables. The callables will then be used to process all |
|---|
| 358 | rows and instances. |
|---|
| 359 | |
|---|
| 360 | Callables are of the following form:: |
|---|
| 361 | |
|---|
| 362 | def new_execute(state, dict_, row, **flags): |
|---|
| 363 | # process incoming instance state and given row. the instance is |
|---|
| 364 | # "new" and was just created upon receipt of this row. |
|---|
| 365 | # flags is a dictionary containing at least the following |
|---|
| 366 | # attributes: |
|---|
| 367 | # isnew - indicates if the instance was newly created as a |
|---|
| 368 | # result of reading this row |
|---|
| 369 | # instancekey - identity key of the instance |
|---|
| 370 | |
|---|
| 371 | def existing_execute(state, dict_, row, **flags): |
|---|
| 372 | # process incoming instance state and given row. the instance is |
|---|
| 373 | # "existing" and was created based on a previous row. |
|---|
| 374 | |
|---|
| 375 | return (new_execute, existing_execute) |
|---|
| 376 | |
|---|
| 377 | Either of the three tuples can be ``None`` in which case no function |
|---|
| 378 | is called. |
|---|
| 379 | """ |
|---|
| 380 | |
|---|
| 381 | raise NotImplementedError() |
|---|
| 382 | |
|---|
| 383 | def cascade_iterator(self, type_, state, visited_instances=None, halt_on=None): |
|---|
| 384 | """Iterate through instances related to the given instance for |
|---|
| 385 | a particular 'cascade', starting with this MapperProperty. |
|---|
| 386 | |
|---|
| 387 | See PropertyLoader for the related instance implementation. |
|---|
| 388 | """ |
|---|
| 389 | |
|---|
| 390 | return iter(()) |
|---|
| 391 | |
|---|
| 392 | def set_parent(self, parent): |
|---|
| 393 | self.parent = parent |
|---|
| 394 | |
|---|
| 395 | def instrument_class(self, mapper): |
|---|
| 396 | raise NotImplementedError() |
|---|
| 397 | |
|---|
| 398 | _compile_started = False |
|---|
| 399 | _compile_finished = False |
|---|
| 400 | |
|---|
| 401 | def init(self): |
|---|
| 402 | """Called after all mappers are created to assemble |
|---|
| 403 | relationships between mappers and perform other post-mapper-creation |
|---|
| 404 | initialization steps. |
|---|
| 405 | |
|---|
| 406 | """ |
|---|
| 407 | self._compile_started = True |
|---|
| 408 | self.do_init() |
|---|
| 409 | self._compile_finished = True |
|---|
| 410 | |
|---|
| 411 | def do_init(self): |
|---|
| 412 | """Perform subclass-specific initialization post-mapper-creation steps. |
|---|
| 413 | |
|---|
| 414 | This is a *template* method called by the |
|---|
| 415 | ``MapperProperty`` object's init() method. |
|---|
| 416 | |
|---|
| 417 | """ |
|---|
| 418 | pass |
|---|
| 419 | |
|---|
| 420 | def post_instrument_class(self, mapper): |
|---|
| 421 | """Perform instrumentation adjustments that need to occur |
|---|
| 422 | after init() has completed. |
|---|
| 423 | |
|---|
| 424 | """ |
|---|
| 425 | pass |
|---|
| 426 | |
|---|
| 427 | def register_dependencies(self, *args, **kwargs): |
|---|
| 428 | """Called by the ``Mapper`` in response to the UnitOfWork |
|---|
| 429 | calling the ``Mapper``'s register_dependencies operation. |
|---|
| 430 | Establishes a topological dependency between two mappers |
|---|
| 431 | which will affect the order in which mappers persist data. |
|---|
| 432 | |
|---|
| 433 | """ |
|---|
| 434 | |
|---|
| 435 | pass |
|---|
| 436 | |
|---|
| 437 | def register_processors(self, *args, **kwargs): |
|---|
| 438 | """Called by the ``Mapper`` in response to the UnitOfWork |
|---|
| 439 | calling the ``Mapper``'s register_processors operation. |
|---|
| 440 | Establishes a processor object between two mappers which |
|---|
| 441 | will link data and state between parent/child objects. |
|---|
| 442 | |
|---|
| 443 | """ |
|---|
| 444 | |
|---|
| 445 | pass |
|---|
| 446 | |
|---|
| 447 | def is_primary(self): |
|---|
| 448 | """Return True if this ``MapperProperty``'s mapper is the |
|---|
| 449 | primary mapper for its class. |
|---|
| 450 | |
|---|
| 451 | This flag is used to indicate that the ``MapperProperty`` can |
|---|
| 452 | define attribute instrumentation for the class at the class |
|---|
| 453 | level (as opposed to the individual instance level). |
|---|
| 454 | """ |
|---|
| 455 | |
|---|
| 456 | return not self.parent.non_primary |
|---|
| 457 | |
|---|
| 458 | def merge(self, session, source, dest, dont_load, _recursive): |
|---|
| 459 | """Merge the attribute represented by this ``MapperProperty`` |
|---|
| 460 | from source to destination object""" |
|---|
| 461 | |
|---|
| 462 | raise NotImplementedError() |
|---|
| 463 | |
|---|
| 464 | def compare(self, operator, value): |
|---|
| 465 | """Return a compare operation for the columns represented by |
|---|
| 466 | this ``MapperProperty`` to the given value, which may be a |
|---|
| 467 | column value or an instance. 'operator' is an operator from |
|---|
| 468 | the operators module, or from sql.Comparator. |
|---|
| 469 | |
|---|
| 470 | By default uses the PropComparator attached to this MapperProperty |
|---|
| 471 | under the attribute name "comparator". |
|---|
| 472 | """ |
|---|
| 473 | |
|---|
| 474 | return operator(self.comparator, value) |
|---|
| 475 | |
|---|
| 476 | class PropComparator(expression.ColumnOperators): |
|---|
| 477 | """defines comparison operations for MapperProperty objects. |
|---|
| 478 | |
|---|
| 479 | PropComparator instances should also define an accessor 'property' |
|---|
| 480 | which returns the MapperProperty associated with this |
|---|
| 481 | PropComparator. |
|---|
| 482 | """ |
|---|
| 483 | |
|---|
| 484 | def __init__(self, prop, mapper, adapter=None): |
|---|
| 485 | self.prop = self.property = prop |
|---|
| 486 | self.mapper = mapper |
|---|
| 487 | self.adapter = adapter |
|---|
| 488 | |
|---|
| 489 | def __clause_element__(self): |
|---|
| 490 | raise NotImplementedError("%r" % self) |
|---|
| 491 | |
|---|
| 492 | def adapted(self, adapter): |
|---|
| 493 | """Return a copy of this PropComparator which will use the given adaption function |
|---|
| 494 | on the local side of generated expressions. |
|---|
| 495 | |
|---|
| 496 | """ |
|---|
| 497 | return self.__class__(self.prop, self.mapper, adapter) |
|---|
| 498 | |
|---|
| 499 | @staticmethod |
|---|
| 500 | def any_op(a, b, **kwargs): |
|---|
| 501 | return a.any(b, **kwargs) |
|---|
| 502 | |
|---|
| 503 | @staticmethod |
|---|
| 504 | def has_op(a, b, **kwargs): |
|---|
| 505 | return a.has(b, **kwargs) |
|---|
| 506 | |
|---|
| 507 | @staticmethod |
|---|
| 508 | def of_type_op(a, class_): |
|---|
| 509 | return a.of_type(class_) |
|---|
| 510 | |
|---|
| 511 | def of_type(self, class_): |
|---|
| 512 | """Redefine this object in terms of a polymorphic subclass. |
|---|
| 513 | |
|---|
| 514 | Returns a new PropComparator from which further criterion can be evaluated. |
|---|
| 515 | |
|---|
| 516 | e.g.:: |
|---|
| 517 | |
|---|
| 518 | query.join(Company.employees.of_type(Engineer)).\\ |
|---|
| 519 | filter(Engineer.name=='foo') |
|---|
| 520 | |
|---|
| 521 | \class_ |
|---|
| 522 | a class or mapper indicating that criterion will be against |
|---|
| 523 | this specific subclass. |
|---|
| 524 | |
|---|
| 525 | |
|---|
| 526 | """ |
|---|
| 527 | |
|---|
| 528 | return self.operate(PropComparator.of_type_op, class_) |
|---|
| 529 | |
|---|
| 530 | def any(self, criterion=None, **kwargs): |
|---|
| 531 | """Return true if this collection contains any member that meets the given criterion. |
|---|
| 532 | |
|---|
| 533 | criterion |
|---|
| 534 | an optional ClauseElement formulated against the member class' table |
|---|
| 535 | or attributes. |
|---|
| 536 | |
|---|
| 537 | \**kwargs |
|---|
| 538 | key/value pairs corresponding to member class attribute names which |
|---|
| 539 | will be compared via equality to the corresponding values. |
|---|
| 540 | """ |
|---|
| 541 | |
|---|
| 542 | return self.operate(PropComparator.any_op, criterion, **kwargs) |
|---|
| 543 | |
|---|
| 544 | def has(self, criterion=None, **kwargs): |
|---|
| 545 | """Return true if this element references a member which meets the given criterion. |
|---|
| 546 | |
|---|
| 547 | criterion |
|---|
| 548 | an optional ClauseElement formulated against the member class' table |
|---|
| 549 | or attributes. |
|---|
| 550 | |
|---|
| 551 | \**kwargs |
|---|
| 552 | key/value pairs corresponding to member class attribute names which |
|---|
| 553 | will be compared via equality to the corresponding values. |
|---|
| 554 | """ |
|---|
| 555 | |
|---|
| 556 | return self.operate(PropComparator.has_op, criterion, **kwargs) |
|---|
| 557 | |
|---|
| 558 | |
|---|
| 559 | class StrategizedProperty(MapperProperty): |
|---|
| 560 | """A MapperProperty which uses selectable strategies to affect |
|---|
| 561 | loading behavior. |
|---|
| 562 | |
|---|
| 563 | There is a single default strategy selected by default. Alternate |
|---|
| 564 | strategies can be selected at Query time through the usage of |
|---|
| 565 | ``StrategizedOption`` objects via the Query.options() method. |
|---|
| 566 | """ |
|---|
| 567 | |
|---|
| 568 | def __get_context_strategy(self, context, path): |
|---|
| 569 | cls = context.attributes.get(("loaderstrategy", path), None) |
|---|
| 570 | if cls: |
|---|
| 571 | try: |
|---|
| 572 | return self.__all_strategies[cls] |
|---|
| 573 | except KeyError: |
|---|
| 574 | return self.__init_strategy(cls) |
|---|
| 575 | else: |
|---|
| 576 | return self.strategy |
|---|
| 577 | |
|---|
| 578 | def _get_strategy(self, cls): |
|---|
| 579 | try: |
|---|
| 580 | return self.__all_strategies[cls] |
|---|
| 581 | except KeyError: |
|---|
| 582 | return self.__init_strategy(cls) |
|---|
| 583 | |
|---|
| 584 | def __init_strategy(self, cls): |
|---|
| 585 | self.__all_strategies[cls] = strategy = cls(self) |
|---|
| 586 | strategy.init() |
|---|
| 587 | return strategy |
|---|
| 588 | |
|---|
| 589 | def setup(self, context, entity, path, adapter, **kwargs): |
|---|
| 590 | self.__get_context_strategy(context, path + (self.key,)).setup_query(context, entity, path, adapter, **kwargs) |
|---|
| 591 | |
|---|
| 592 | def create_row_processor(self, context, path, mapper, row, adapter): |
|---|
| 593 | return self.__get_context_strategy(context, path + (self.key,)).create_row_processor(context, path, mapper, row, adapter) |
|---|
| 594 | |
|---|
| 595 | def do_init(self): |
|---|
| 596 | self.__all_strategies = {} |
|---|
| 597 | self.strategy = self.__init_strategy(self.strategy_class) |
|---|
| 598 | |
|---|
| 599 | def post_instrument_class(self, mapper): |
|---|
| 600 | if self.is_primary(): |
|---|
| 601 | self.strategy.init_class_attribute(mapper) |
|---|
| 602 | |
|---|
| 603 | def build_path(entity, key, prev=None): |
|---|
| 604 | if prev: |
|---|
| 605 | return prev + (entity, key) |
|---|
| 606 | else: |
|---|
| 607 | return (entity, key) |
|---|
| 608 | |
|---|
| 609 | def serialize_path(path): |
|---|
| 610 | if path is None: |
|---|
| 611 | return None |
|---|
| 612 | |
|---|
| 613 | return [ |
|---|
| 614 | (mapper.class_, key) |
|---|
| 615 | for mapper, key in [(path[i], path[i+1]) for i in range(0, len(path)-1, 2)] |
|---|
| 616 | ] |
|---|
| 617 | |
|---|
| 618 | def deserialize_path(path): |
|---|
| 619 | if path is None: |
|---|
| 620 | return None |
|---|
| 621 | |
|---|
| 622 | global class_mapper |
|---|
| 623 | if class_mapper is None: |
|---|
| 624 | from sqlalchemy.orm import class_mapper |
|---|
| 625 | |
|---|
| 626 | return tuple( |
|---|
| 627 | chain(*[(class_mapper(cls), key) for cls, key in path]) |
|---|
| 628 | ) |
|---|
| 629 | |
|---|
| 630 | class MapperOption(object): |
|---|
| 631 | """Describe a modification to a Query.""" |
|---|
| 632 | |
|---|
| 633 | propagate_to_loaders = False |
|---|
| 634 | """if True, indicate this option should be carried along |
|---|
| 635 | Query object generated by scalar or object lazy loaders. |
|---|
| 636 | """ |
|---|
| 637 | |
|---|
| 638 | def process_query(self, query): |
|---|
| 639 | pass |
|---|
| 640 | |
|---|
| 641 | def process_query_conditionally(self, query): |
|---|
| 642 | """same as process_query(), except that this option may not apply |
|---|
| 643 | to the given query. |
|---|
| 644 | |
|---|
| 645 | Used when secondary loaders resend existing options to a new |
|---|
| 646 | Query.""" |
|---|
| 647 | self.process_query(query) |
|---|
| 648 | |
|---|
| 649 | class ExtensionOption(MapperOption): |
|---|
| 650 | """a MapperOption that applies a MapperExtension to a query operation.""" |
|---|
| 651 | |
|---|
| 652 | def __init__(self, ext): |
|---|
| 653 | self.ext = ext |
|---|
| 654 | |
|---|
| 655 | def process_query(self, query): |
|---|
| 656 | entity = query._generate_mapper_zero() |
|---|
| 657 | entity.extension = entity.extension.copy() |
|---|
| 658 | entity.extension.push(self.ext) |
|---|
| 659 | |
|---|
| 660 | class PropertyOption(MapperOption): |
|---|
| 661 | """A MapperOption that is applied to a property off the mapper or |
|---|
| 662 | one of its child mappers, identified by a dot-separated key. |
|---|
| 663 | """ |
|---|
| 664 | |
|---|
| 665 | def __init__(self, key, mapper=None): |
|---|
| 666 | self.key = key |
|---|
| 667 | self.mapper = mapper |
|---|
| 668 | |
|---|
| 669 | def process_query(self, query): |
|---|
| 670 | self._process(query, True) |
|---|
| 671 | |
|---|
| 672 | def process_query_conditionally(self, query): |
|---|
| 673 | self._process(query, False) |
|---|
| 674 | |
|---|
| 675 | def _process(self, query, raiseerr): |
|---|
| 676 | paths, mappers = self.__get_paths(query, raiseerr) |
|---|
| 677 | if paths: |
|---|
| 678 | self.process_query_property(query, paths, mappers) |
|---|
| 679 | |
|---|
| 680 | def process_query_property(self, query, paths, mappers): |
|---|
| 681 | pass |
|---|
| 682 | |
|---|
| 683 | def __find_entity(self, query, mapper, raiseerr): |
|---|
| 684 | from sqlalchemy.orm.util import _class_to_mapper, _is_aliased_class |
|---|
| 685 | |
|---|
| 686 | if _is_aliased_class(mapper): |
|---|
| 687 | searchfor = mapper |
|---|
| 688 | else: |
|---|
| 689 | searchfor = _class_to_mapper(mapper).base_mapper |
|---|
| 690 | |
|---|
| 691 | for ent in query._mapper_entities: |
|---|
| 692 | if ent.path_entity is searchfor: |
|---|
| 693 | return ent |
|---|
| 694 | else: |
|---|
| 695 | if raiseerr: |
|---|
| 696 | raise sa_exc.ArgumentError("Can't find entity %s in Query. Current list: %r" |
|---|
| 697 | % (searchfor, [str(m.path_entity) for m in query._entities])) |
|---|
| 698 | else: |
|---|
| 699 | return None |
|---|
| 700 | |
|---|
| 701 | def __getstate__(self): |
|---|
| 702 | d = self.__dict__.copy() |
|---|
| 703 | d['key'] = ret = [] |
|---|
| 704 | for token in util.to_list(self.key): |
|---|
| 705 | if isinstance(token, PropComparator): |
|---|
| 706 | ret.append((token.mapper.class_, token.key)) |
|---|
| 707 | else: |
|---|
| 708 | ret.append(token) |
|---|
| 709 | return d |
|---|
| 710 | |
|---|
| 711 | def __setstate__(self, state): |
|---|
| 712 | ret = [] |
|---|
| 713 | for key in state['key']: |
|---|
| 714 | if isinstance(key, tuple): |
|---|
| 715 | cls, propkey = key |
|---|
| 716 | ret.append(getattr(cls, propkey)) |
|---|
| 717 | else: |
|---|
| 718 | ret.append(key) |
|---|
| 719 | state['key'] = tuple(ret) |
|---|
| 720 | self.__dict__ = state |
|---|
| 721 | |
|---|
| 722 | def __get_paths(self, query, raiseerr): |
|---|
| 723 | path = None |
|---|
| 724 | entity = None |
|---|
| 725 | l = [] |
|---|
| 726 | mappers = [] |
|---|
| 727 | |
|---|
| 728 | # _current_path implies we're in a secondary load |
|---|
| 729 | # with an existing path |
|---|
| 730 | current_path = list(query._current_path) |
|---|
| 731 | |
|---|
| 732 | if self.mapper: |
|---|
| 733 | entity = self.__find_entity(query, self.mapper, raiseerr) |
|---|
| 734 | mapper = entity.mapper |
|---|
| 735 | path_element = entity.path_entity |
|---|
| 736 | |
|---|
| 737 | for key in util.to_list(self.key): |
|---|
| 738 | if isinstance(key, basestring): |
|---|
| 739 | tokens = key.split('.') |
|---|
| 740 | else: |
|---|
| 741 | tokens = [key] |
|---|
| 742 | for token in tokens: |
|---|
| 743 | if isinstance(token, basestring): |
|---|
| 744 | if not entity: |
|---|
| 745 | entity = query._entity_zero() |
|---|
| 746 | path_element = entity.path_entity |
|---|
| 747 | mapper = entity.mapper |
|---|
| 748 | mappers.append(mapper) |
|---|
| 749 | prop = mapper.get_property(token, resolve_synonyms=True, raiseerr=raiseerr) |
|---|
| 750 | key = token |
|---|
| 751 | elif isinstance(token, PropComparator): |
|---|
| 752 | prop = token.property |
|---|
| 753 | if not entity: |
|---|
| 754 | entity = self.__find_entity(query, token.parententity, raiseerr) |
|---|
| 755 | if not entity: |
|---|
| 756 | return [], [] |
|---|
| 757 | path_element = entity.path_entity |
|---|
| 758 | mappers.append(prop.parent) |
|---|
| 759 | key = prop.key |
|---|
| 760 | else: |
|---|
| 761 | raise sa_exc.ArgumentError("mapper option expects string key or list of attributes") |
|---|
| 762 | |
|---|
| 763 | if current_path and key == current_path[1]: |
|---|
| 764 | current_path = current_path[2:] |
|---|
| 765 | continue |
|---|
| 766 | |
|---|
| 767 | if prop is None: |
|---|
| 768 | return [], [] |
|---|
| 769 | |
|---|
| 770 | path = build_path(path_element, prop.key, path) |
|---|
| 771 | l.append(path) |
|---|
| 772 | if getattr(token, '_of_type', None): |
|---|
| 773 | path_element = mapper = token._of_type |
|---|
| 774 | else: |
|---|
| 775 | path_element = mapper = getattr(prop, 'mapper', None) |
|---|
| 776 | |
|---|
| 777 | if path_element: |
|---|
| 778 | path_element = path_element.base_mapper |
|---|
| 779 | |
|---|
| 780 | |
|---|
| 781 | # if current_path tokens remain, then |
|---|
| 782 | # we didn't have an exact path match. |
|---|
| 783 | if current_path: |
|---|
| 784 | return [], [] |
|---|
| 785 | |
|---|
| 786 | return l, mappers |
|---|
| 787 | |
|---|
| 788 | class AttributeExtension(object): |
|---|
| 789 | """An event handler for individual attribute change events. |
|---|
| 790 | |
|---|
| 791 | AttributeExtension is assembled within the descriptors associated |
|---|
| 792 | with a mapped class. |
|---|
| 793 | |
|---|
| 794 | """ |
|---|
| 795 | |
|---|
| 796 | active_history = True |
|---|
| 797 | """indicates that the set() method would like to receive the 'old' value, |
|---|
| 798 | even if it means firing lazy callables. |
|---|
| 799 | """ |
|---|
| 800 | |
|---|
| 801 | def append(self, state, value, initiator): |
|---|
| 802 | """Receive a collection append event. |
|---|
| 803 | |
|---|
| 804 | The returned value will be used as the actual value to be |
|---|
| 805 | appended. |
|---|
| 806 | |
|---|
| 807 | """ |
|---|
| 808 | return value |
|---|
| 809 | |
|---|
| 810 | def remove(self, state, value, initiator): |
|---|
| 811 | """Receive a remove event. |
|---|
| 812 | |
|---|
| 813 | No return value is defined. |
|---|
| 814 | |
|---|
| 815 | """ |
|---|
| 816 | pass |
|---|
| 817 | |
|---|
| 818 | def set(self, state, value, oldvalue, initiator): |
|---|
| 819 | """Receive a set event. |
|---|
| 820 | |
|---|
| 821 | The returned value will be used as the actual value to be |
|---|
| 822 | set. |
|---|
| 823 | |
|---|
| 824 | """ |
|---|
| 825 | return value |
|---|
| 826 | |
|---|
| 827 | |
|---|
| 828 | class StrategizedOption(PropertyOption): |
|---|
| 829 | """A MapperOption that affects which LoaderStrategy will be used |
|---|
| 830 | for an operation by a StrategizedProperty. |
|---|
| 831 | """ |
|---|
| 832 | |
|---|
| 833 | def is_chained(self): |
|---|
| 834 | return False |
|---|
| 835 | |
|---|
| 836 | def process_query_property(self, query, paths, mappers): |
|---|
| 837 | if self.is_chained(): |
|---|
| 838 | for path in paths: |
|---|
| 839 | query._attributes[("loaderstrategy", path)] = self.get_strategy_class() |
|---|
| 840 | else: |
|---|
| 841 | query._attributes[("loaderstrategy", paths[-1])] = self.get_strategy_class() |
|---|
| 842 | |
|---|
| 843 | def get_strategy_class(self): |
|---|
| 844 | raise NotImplementedError() |
|---|
| 845 | |
|---|
| 846 | |
|---|
| 847 | class LoaderStrategy(object): |
|---|
| 848 | """Describe the loading behavior of a StrategizedProperty object. |
|---|
| 849 | |
|---|
| 850 | The ``LoaderStrategy`` interacts with the querying process in three |
|---|
| 851 | ways: |
|---|
| 852 | |
|---|
| 853 | * it controls the configuration of the ``InstrumentedAttribute`` |
|---|
| 854 | placed on a class to handle the behavior of the attribute. this |
|---|
| 855 | may involve setting up class-level callable functions to fire |
|---|
| 856 | off a select operation when the attribute is first accessed |
|---|
| 857 | (i.e. a lazy load) |
|---|
| 858 | |
|---|
| 859 | * it processes the ``QueryContext`` at statement construction time, |
|---|
| 860 | where it can modify the SQL statement that is being produced. |
|---|
| 861 | simple column attributes may add their represented column to the |
|---|
| 862 | list of selected columns, *eager loading* properties may add |
|---|
| 863 | ``LEFT OUTER JOIN`` clauses to the statement. |
|---|
| 864 | |
|---|
| 865 | * it processes the ``SelectionContext`` at row-processing time. This |
|---|
| 866 | includes straight population of attributes corresponding to rows, |
|---|
| 867 | setting instance-level lazyloader callables on newly |
|---|
| 868 | constructed instances, and appending child items to scalar/collection |
|---|
| 869 | attributes in response to eagerly-loaded relations. |
|---|
| 870 | """ |
|---|
| 871 | |
|---|
| 872 | def __init__(self, parent): |
|---|
| 873 | self.parent_property = parent |
|---|
| 874 | self.is_class_level = False |
|---|
| 875 | self.parent = self.parent_property.parent |
|---|
| 876 | self.key = self.parent_property.key |
|---|
| 877 | |
|---|
| 878 | def init(self): |
|---|
| 879 | raise NotImplementedError("LoaderStrategy") |
|---|
| 880 | |
|---|
| 881 | def init_class_attribute(self, mapper): |
|---|
| 882 | pass |
|---|
| 883 | |
|---|
| 884 | def setup_query(self, context, entity, path, adapter, **kwargs): |
|---|
| 885 | pass |
|---|
| 886 | |
|---|
| 887 | def create_row_processor(self, selectcontext, path, mapper, row, adapter): |
|---|
| 888 | """Return row processing functions which fulfill the contract specified |
|---|
| 889 | by MapperProperty.create_row_processor. |
|---|
| 890 | |
|---|
| 891 | StrategizedProperty delegates its create_row_processor method directly |
|---|
| 892 | to this method. |
|---|
| 893 | """ |
|---|
| 894 | |
|---|
| 895 | raise NotImplementedError() |
|---|
| 896 | |
|---|
| 897 | def __str__(self): |
|---|
| 898 | return str(self.parent_property) |
|---|
| 899 | |
|---|
| 900 | def debug_callable(self, fn, logger, announcement, logfn): |
|---|
| 901 | if announcement: |
|---|
| 902 | logger.debug(announcement) |
|---|
| 903 | if logfn: |
|---|
| 904 | def call(*args, **kwargs): |
|---|
| 905 | logger.debug(logfn(*args, **kwargs)) |
|---|
| 906 | return fn(*args, **kwargs) |
|---|
| 907 | return call |
|---|
| 908 | else: |
|---|
| 909 | return fn |
|---|
| 910 | |
|---|
| 911 | class InstrumentationManager(object): |
|---|
| 912 | """User-defined class instrumentation extension. |
|---|
| 913 | |
|---|
| 914 | The API for this class should be considered as semi-stable, |
|---|
| 915 | and may change slightly with new releases. |
|---|
| 916 | |
|---|
| 917 | """ |
|---|
| 918 | |
|---|
| 919 | # r4361 added a mandatory (cls) constructor to this interface. |
|---|
| 920 | # given that, perhaps class_ should be dropped from all of these |
|---|
| 921 | # signatures. |
|---|
| 922 | |
|---|
| 923 | def __init__(self, class_): |
|---|
| 924 | pass |
|---|
| 925 | |
|---|
| 926 | def manage(self, class_, manager): |
|---|
| 927 | setattr(class_, '_default_class_manager', manager) |
|---|
| 928 | |
|---|
| 929 | def dispose(self, class_, manager): |
|---|
| 930 | delattr(class_, '_default_class_manager') |
|---|
| 931 | |
|---|
| 932 | def manager_getter(self, class_): |
|---|
| 933 | def get(cls): |
|---|
| 934 | return cls._default_class_manager |
|---|
| 935 | return get |
|---|
| 936 | |
|---|
| 937 | def instrument_attribute(self, class_, key, inst): |
|---|
| 938 | pass |
|---|
| 939 | |
|---|
| 940 | def post_configure_attribute(self, class_, key, inst): |
|---|
| 941 | pass |
|---|
| 942 | |
|---|
| 943 | def install_descriptor(self, class_, key, inst): |
|---|
| 944 | setattr(class_, key, inst) |
|---|
| 945 | |
|---|
| 946 | def uninstall_descriptor(self, class_, key): |
|---|
| 947 | delattr(class_, key) |
|---|
| 948 | |
|---|
| 949 | def install_member(self, class_, key, implementation): |
|---|
| 950 | setattr(class_, key, implementation) |
|---|
| 951 | |
|---|
| 952 | def uninstall_member(self, class_, key): |
|---|
| 953 | delattr(class_, key) |
|---|
| 954 | |
|---|
| 955 | def instrument_collection_class(self, class_, key, collection_class): |
|---|
| 956 | global collections |
|---|
| 957 | if collections is None: |
|---|
| 958 | from sqlalchemy.orm import collections |
|---|
| 959 | return collections.prepare_instrumentation(collection_class) |
|---|
| 960 | |
|---|
| 961 | def get_instance_dict(self, class_, instance): |
|---|
| 962 | return instance.__dict__ |
|---|
| 963 | |
|---|
| 964 | def initialize_instance_dict(self, class_, instance): |
|---|
| 965 | pass |
|---|
| 966 | |
|---|
| 967 | def install_state(self, class_, instance, state): |
|---|
| 968 | setattr(instance, '_default_state', state) |
|---|
| 969 | |
|---|
| 970 | def remove_state(self, class_, instance): |
|---|
| 971 | delattr(instance, '_default_state', state) |
|---|
| 972 | |
|---|
| 973 | def state_getter(self, class_): |
|---|
| 974 | return lambda instance: getattr(instance, '_default_state') |
|---|
| 975 | |
|---|
| 976 | def dict_getter(self, class_): |
|---|
| 977 | return lambda inst: self.get_instance_dict(class_, inst) |
|---|
| 978 | |
|---|