[3] | 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 | |
---|