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 | |
---|