root/galaxy-central/eggs/SQLAlchemy-0.5.6_dev_r6498-py2.6.egg/sqlalchemy/orm/collections.py @ 3

リビジョン 3, 49.9 KB (コミッタ: kohda, 14 年 前)

Install Unix tools  http://hannonlab.cshl.edu/galaxy_unix_tools/galaxy.html

行番号 
1"""Support for collections of mapped entities.
2
3The collections package supplies the machinery used to inform the ORM of
4collection membership changes.  An instrumentation via decoration approach is
5used, allowing arbitrary types (including built-ins) to be used as entity
6collections without requiring inheritance from a base class.
7
8Instrumentation decoration relays membership change events to the
9``InstrumentedCollectionAttribute`` that is currently managing the collection.
10The decorators observe function call arguments and return values, tracking
11entities entering or leaving the collection.  Two decorator approaches are
12provided.  One is a bundle of generic decorators that map function arguments
13and return values to events::
14
15  from sqlalchemy.orm.collections import collection
16  class MyClass(object):
17      # ...
18
19      @collection.adds(1)
20      def store(self, item):
21          self.data.append(item)
22
23      @collection.removes_return()
24      def pop(self):
25          return self.data.pop()
26
27
28The second approach is a bundle of targeted decorators that wrap appropriate
29append and remove notifiers around the mutation methods present in the
30standard Python ``list``, ``set`` and ``dict`` interfaces.  These could be
31specified in terms of generic decorator recipes, but are instead hand-tooled
32for increased efficiency.  The targeted decorators occasionally implement
33adapter-like behavior, such as mapping bulk-set methods (``extend``,
34``update``, ``__setslice__``, etc.) into the series of atomic mutation events
35that the ORM requires.
36
37The targeted decorators are used internally for automatic instrumentation of
38entity collection classes.  Every collection class goes through a
39transformation process roughly like so:
40
411. If the class is a built-in, substitute a trivial sub-class
422. Is this class already instrumented?
433. Add in generic decorators
444. Sniff out the collection interface through duck-typing
455. Add targeted decoration to any undecorated interface method
46
47This process modifies the class at runtime, decorating methods and adding some
48bookkeeping properties.  This isn't possible (or desirable) for built-in
49classes like ``list``, so trivial sub-classes are substituted to hold
50decoration::
51
52  class InstrumentedList(list):
53      pass
54
55Collection classes can be specified in ``relation(collection_class=)`` as
56types or a function that returns an instance.  Collection classes are
57inspected and instrumented during the mapper compilation phase.  The
58collection_class callable will be executed once to produce a specimen
59instance, and the type of that specimen will be instrumented.  Functions that
60return built-in types like ``lists`` will be adapted to produce instrumented
61instances.
62
63When extending a known type like ``list``, additional decorations are not
64generally not needed.  Odds are, the extension method will delegate to a
65method that's already instrumented.  For example::
66
67  class QueueIsh(list):
68     def push(self, item):
69         self.append(item)
70     def shift(self):
71         return self.pop(0)
72
73There's no need to decorate these methods.  ``append`` and ``pop`` are already
74instrumented as part of the ``list`` interface.  Decorating them would fire
75duplicate events, which should be avoided.
76
77The targeted decoration tries not to rely on other methods in the underlying
78collection class, but some are unavoidable.  Many depend on 'read' methods
79being present to properly instrument a 'write', for example, ``__setitem__``
80needs ``__getitem__``.  "Bulk" methods like ``update`` and ``extend`` may also
81reimplemented in terms of atomic appends and removes, so the ``extend``
82decoration will actually perform many ``append`` operations and not call the
83underlying method at all.
84
85Tight control over bulk operation and the firing of events is also possible by
86implementing the instrumentation internally in your methods.  The basic
87instrumentation package works under the general assumption that collection
88mutation will not raise unusual exceptions.  If you want to closely
89orchestrate append and remove events with exception management, internal
90instrumentation may be the answer.  Within your method,
91``collection_adapter(self)`` will retrieve an object that you can use for
92explicit control over triggering append and remove events.
93
94The owning object and InstrumentedCollectionAttribute are also reachable
95through the adapter, allowing for some very sophisticated behavior.
96
97"""
98
99import copy
100import inspect
101import operator
102import sys
103import weakref
104
105import sqlalchemy.exceptions as sa_exc
106from sqlalchemy.sql import expression
107from sqlalchemy import schema, util
108
109
110__all__ = ['collection', 'collection_adapter',
111           'mapped_collection', 'column_mapped_collection',
112           'attribute_mapped_collection']
113
114__instrumentation_mutex = util.threading.Lock()
115
116
117def column_mapped_collection(mapping_spec):
118    """A dictionary-based collection type with column-based keying.
119
120    Returns a MappedCollection factory with a keying function generated
121    from mapping_spec, which may be a Column or a sequence of Columns.
122
123    The key value must be immutable for the lifetime of the object.  You
124    can not, for example, map on foreign key values if those key values will
125    change during the session, i.e. from None to a database-assigned integer
126    after a session flush.
127
128    """
129    from sqlalchemy.orm.util import _state_mapper
130    from sqlalchemy.orm.attributes import instance_state
131
132    cols = [expression._no_literals(q) for q in util.to_list(mapping_spec)]
133    if len(cols) == 1:
134        def keyfunc(value):
135            state = instance_state(value)
136            m = _state_mapper(state)
137            return m._get_state_attr_by_column(state, cols[0])
138    else:
139        mapping_spec = tuple(cols)
140        def keyfunc(value):
141            state = instance_state(value)
142            m = _state_mapper(state)
143            return tuple(m._get_state_attr_by_column(state, c)
144                         for c in mapping_spec)
145    return lambda: MappedCollection(keyfunc)
146
147def attribute_mapped_collection(attr_name):
148    """A dictionary-based collection type with attribute-based keying.
149
150    Returns a MappedCollection factory with a keying based on the
151    'attr_name' attribute of entities in the collection.
152
153    The key value must be immutable for the lifetime of the object.  You
154    can not, for example, map on foreign key values if those key values will
155    change during the session, i.e. from None to a database-assigned integer
156    after a session flush.
157
158    """
159    return lambda: MappedCollection(operator.attrgetter(attr_name))
160
161
162def mapped_collection(keyfunc):
163    """A dictionary-based collection type with arbitrary keying.
164
165    Returns a MappedCollection factory with a keying function generated
166    from keyfunc, a callable that takes an entity and returns a key value.
167
168    The key value must be immutable for the lifetime of the object.  You
169    can not, for example, map on foreign key values if those key values will
170    change during the session, i.e. from None to a database-assigned integer
171    after a session flush.
172
173    """
174    return lambda: MappedCollection(keyfunc)
175
176class collection(object):
177    """Decorators for entity collection classes.
178
179    The decorators fall into two groups: annotations and interception recipes.
180
181    The annotating decorators (appender, remover, iterator,
182    internally_instrumented, on_link) indicate the method's purpose and take no
183    arguments.  They are not written with parens::
184
185        @collection.appender
186        def append(self, append): ...
187
188    The recipe decorators all require parens, even those that take no
189    arguments::
190
191        @collection.adds('entity'):
192        def insert(self, position, entity): ...
193
194        @collection.removes_return()
195        def popitem(self): ...
196
197    Decorators can be specified in long-hand for Python 2.3, or with
198    the class-level dict attribute '__instrumentation__'- see the source
199    for details.
200
201    """
202    # Bundled as a class solely for ease of use: packaging, doc strings,
203    # importability.
204
205    @staticmethod
206    def appender(fn):
207        """Tag the method as the collection appender.
208
209        The appender method is called with one positional argument: the value
210        to append. The method will be automatically decorated with 'adds(1)'
211        if not already decorated::
212
213            @collection.appender
214            def add(self, append): ...
215
216            # or, equivalently
217            @collection.appender
218            @collection.adds(1)
219            def add(self, append): ...
220
221            # for mapping type, an 'append' may kick out a previous value
222            # that occupies that slot.  consider d['a'] = 'foo'- any previous
223            # value in d['a'] is discarded.
224            @collection.appender
225            @collection.replaces(1)
226            def add(self, entity):
227                key = some_key_func(entity)
228                previous = None
229                if key in self:
230                    previous = self[key]
231                self[key] = entity
232                return previous
233
234        If the value to append is not allowed in the collection, you may
235        raise an exception.  Something to remember is that the appender
236        will be called for each object mapped by a database query.  If the
237        database contains rows that violate your collection semantics, you
238        will need to get creative to fix the problem, as access via the
239        collection will not work.
240
241        If the appender method is internally instrumented, you must also
242        receive the keyword argument '_sa_initiator' and ensure its
243        promulgation to collection events.
244
245        """
246        setattr(fn, '_sa_instrument_role', 'appender')
247        return fn
248
249    @staticmethod
250    def remover(fn):
251        """Tag the method as the collection remover.
252
253        The remover method is called with one positional argument: the value
254        to remove. The method will be automatically decorated with
255        'removes_return()' if not already decorated::
256
257            @collection.remover
258            def zap(self, entity): ...
259
260            # or, equivalently
261            @collection.remover
262            @collection.removes_return()
263            def zap(self, ): ...
264
265        If the value to remove is not present in the collection, you may
266        raise an exception or return None to ignore the error.
267
268        If the remove method is internally instrumented, you must also
269        receive the keyword argument '_sa_initiator' and ensure its
270        promulgation to collection events.
271
272        """
273        setattr(fn, '_sa_instrument_role', 'remover')
274        return fn
275
276    @staticmethod
277    def iterator(fn):
278        """Tag the method as the collection remover.
279
280        The iterator method is called with no arguments.  It is expected to
281        return an iterator over all collection members::
282
283            @collection.iterator
284            def __iter__(self): ...
285
286        """
287        setattr(fn, '_sa_instrument_role', 'iterator')
288        return fn
289
290    @staticmethod
291    def internally_instrumented(fn):
292        """Tag the method as instrumented.
293
294        This tag will prevent any decoration from being applied to the method.
295        Use this if you are orchestrating your own calls to collection_adapter
296        in one of the basic SQLAlchemy interface methods, or to prevent
297        an automatic ABC method decoration from wrapping your implementation::
298
299            # normally an 'extend' method on a list-like class would be
300            # automatically intercepted and re-implemented in terms of
301            # SQLAlchemy events and append().  your implementation will
302            # never be called, unless:
303            @collection.internally_instrumented
304            def extend(self, items): ...
305
306        """
307        setattr(fn, '_sa_instrumented', True)
308        return fn
309
310    @staticmethod
311    def on_link(fn):
312        """Tag the method as a the "linked to attribute" event handler.
313
314        This optional event handler will be called when the collection class
315        is linked to or unlinked from the InstrumentedAttribute.  It is
316        invoked immediately after the '_sa_adapter' property is set on
317        the instance.  A single argument is passed: the collection adapter
318        that has been linked, or None if unlinking.
319
320        """
321        setattr(fn, '_sa_instrument_role', 'on_link')
322        return fn
323
324    @staticmethod
325    def converter(fn):
326        """Tag the method as the collection converter.
327
328        This optional method will be called when a collection is being
329        replaced entirely, as in::
330
331            myobj.acollection = [newvalue1, newvalue2]
332
333        The converter method will receive the object being assigned and should
334        return an iterable of values suitable for use by the ``appender``
335        method.  A converter must not assign values or mutate the collection,
336        it's sole job is to adapt the value the user provides into an iterable
337        of values for the ORM's use.
338
339        The default converter implementation will use duck-typing to do the
340        conversion.  A dict-like collection will be convert into an iterable
341        of dictionary values, and other types will simply be iterated.
342
343            @collection.converter
344            def convert(self, other): ...
345
346        If the duck-typing of the object does not match the type of this
347        collection, a TypeError is raised.
348
349        Supply an implementation of this method if you want to expand the
350        range of possible types that can be assigned in bulk or perform
351        validation on the values about to be assigned.
352
353        """
354        setattr(fn, '_sa_instrument_role', 'converter')
355        return fn
356
357    @staticmethod
358    def adds(arg):
359        """Mark the method as adding an entity to the collection.
360
361        Adds "add to collection" handling to the method.  The decorator
362        argument indicates which method argument holds the SQLAlchemy-relevant
363        value.  Arguments can be specified positionally (i.e. integer) or by
364        name::
365
366            @collection.adds(1)
367            def push(self, item): ...
368
369            @collection.adds('entity')
370            def do_stuff(self, thing, entity=None): ...
371
372        """
373        def decorator(fn):
374            setattr(fn, '_sa_instrument_before', ('fire_append_event', arg))
375            return fn
376        return decorator
377
378    @staticmethod
379    def replaces(arg):
380        """Mark the method as replacing an entity in the collection.
381
382        Adds "add to collection" and "remove from collection" handling to
383        the method.  The decorator argument indicates which method argument
384        holds the SQLAlchemy-relevant value to be added, and return value, if
385        any will be considered the value to remove.
386
387        Arguments can be specified positionally (i.e. integer) or by name::
388
389            @collection.replaces(2)
390            def __setitem__(self, index, item): ...
391
392        """
393        def decorator(fn):
394            setattr(fn, '_sa_instrument_before', ('fire_append_event', arg))
395            setattr(fn, '_sa_instrument_after', 'fire_remove_event')
396            return fn
397        return decorator
398
399    @staticmethod
400    def removes(arg):
401        """Mark the method as removing an entity in the collection.
402
403        Adds "remove from collection" handling to the method.  The decorator
404        argument indicates which method argument holds the SQLAlchemy-relevant
405        value to be removed. Arguments can be specified positionally (i.e.
406        integer) or by name::
407
408            @collection.removes(1)
409            def zap(self, item): ...
410
411        For methods where the value to remove is not known at call-time, use
412        collection.removes_return.
413
414        """
415        def decorator(fn):
416            setattr(fn, '_sa_instrument_before', ('fire_remove_event', arg))
417            return fn
418        return decorator
419
420    @staticmethod
421    def removes_return():
422        """Mark the method as removing an entity in the collection.
423
424        Adds "remove from collection" handling to the method.  The return value
425        of the method, if any, is considered the value to remove.  The method
426        arguments are not inspected::
427
428            @collection.removes_return()
429            def pop(self): ...
430
431        For methods where the value to remove is known at call-time, use
432        collection.remove.
433
434        """
435        def decorator(fn):
436            setattr(fn, '_sa_instrument_after', 'fire_remove_event')
437            return fn
438        return decorator
439
440
441# public instrumentation interface for 'internally instrumented'
442# implementations
443def collection_adapter(collection):
444    """Fetch the CollectionAdapter for a collection."""
445    return getattr(collection, '_sa_adapter', None)
446
447def collection_iter(collection):
448    """Iterate over an object supporting the @iterator or __iter__ protocols.
449
450    If the collection is an ORM collection, it need not be attached to an
451    object to be iterable.
452
453    """
454    try:
455        return getattr(collection, '_sa_iterator',
456                       getattr(collection, '__iter__'))()
457    except AttributeError:
458        raise TypeError("'%s' object is not iterable" %
459                        type(collection).__name__)
460
461
462class CollectionAdapter(object):
463    """Bridges between the ORM and arbitrary Python collections.
464
465    Proxies base-level collection operations (append, remove, iterate)
466    to the underlying Python collection, and emits add/remove events for
467    entities entering or leaving the collection.
468
469    The ORM uses an CollectionAdapter exclusively for interaction with
470    entity collections.
471
472    """
473    def __init__(self, attr, owner_state, data):
474        self.attr = attr
475        # TODO: figure out what this being a weakref buys us
476        self._data = weakref.ref(data)
477        self.owner_state = owner_state
478        self.link_to_self(data)
479
480    data = property(lambda s: s._data(),
481                    doc="The entity collection being adapted.")
482
483    def link_to_self(self, data):
484        """Link a collection to this adapter, and fire a link event."""
485        setattr(data, '_sa_adapter', self)
486        if hasattr(data, '_sa_on_link'):
487            getattr(data, '_sa_on_link')(self)
488
489    def unlink(self, data):
490        """Unlink a collection from any adapter, and fire a link event."""
491        setattr(data, '_sa_adapter', None)
492        if hasattr(data, '_sa_on_link'):
493            getattr(data, '_sa_on_link')(None)
494
495    def adapt_like_to_iterable(self, obj):
496        """Converts collection-compatible objects to an iterable of values.
497
498        Can be passed any type of object, and if the underlying collection
499        determines that it can be adapted into a stream of values it can
500        use, returns an iterable of values suitable for append()ing.
501
502        This method may raise TypeError or any other suitable exception
503        if adaptation fails.
504
505        If a converter implementation is not supplied on the collection,
506        a default duck-typing-based implementation is used.
507
508        """
509        converter = getattr(self._data(), '_sa_converter', None)
510        if converter is not None:
511            return converter(obj)
512
513        setting_type = util.duck_type_collection(obj)
514        receiving_type = util.duck_type_collection(self._data())
515
516        if obj is None or setting_type != receiving_type:
517            given = obj is None and 'None' or obj.__class__.__name__
518            if receiving_type is None:
519                wanted = self._data().__class__.__name__
520            else:
521                wanted = receiving_type.__name__
522
523            raise TypeError(
524                "Incompatible collection type: %s is not %s-like" % (
525                given, wanted))
526
527        # If the object is an adapted collection, return the (iterable)
528        # adapter.
529        if getattr(obj, '_sa_adapter', None) is not None:
530            return getattr(obj, '_sa_adapter')
531        elif setting_type == dict:
532            return getattr(obj, 'itervalues', getattr(obj, 'values'))()
533        else:
534            return iter(obj)
535
536    def append_with_event(self, item, initiator=None):
537        """Add an entity to the collection, firing mutation events."""
538        getattr(self._data(), '_sa_appender')(item, _sa_initiator=initiator)
539
540    def append_without_event(self, item):
541        """Add or restore an entity to the collection, firing no events."""
542        getattr(self._data(), '_sa_appender')(item, _sa_initiator=False)
543
544    def remove_with_event(self, item, initiator=None):
545        """Remove an entity from the collection, firing mutation events."""
546        getattr(self._data(), '_sa_remover')(item, _sa_initiator=initiator)
547
548    def remove_without_event(self, item):
549        """Remove an entity from the collection, firing no events."""
550        getattr(self._data(), '_sa_remover')(item, _sa_initiator=False)
551
552    def clear_with_event(self, initiator=None):
553        """Empty the collection, firing a mutation event for each entity."""
554        for item in list(self):
555            self.remove_with_event(item, initiator)
556
557    def clear_without_event(self):
558        """Empty the collection, firing no events."""
559        for item in list(self):
560            self.remove_without_event(item)
561
562    def __iter__(self):
563        """Iterate over entities in the collection."""
564        return getattr(self._data(), '_sa_iterator')()
565
566    def __len__(self):
567        """Count entities in the collection."""
568        return len(list(getattr(self._data(), '_sa_iterator')()))
569
570    def __nonzero__(self):
571        return True
572
573    def fire_append_event(self, item, initiator=None):
574        """Notify that a entity has entered the collection.
575
576        Initiator is the InstrumentedAttribute that initiated the membership
577        mutation, and should be left as None unless you are passing along
578        an initiator value from a chained operation.
579
580        """
581        if initiator is not False and item is not None:
582            return self.attr.fire_append_event(self.owner_state, self.owner_state.dict, item, initiator)
583        else:
584            return item
585
586    def fire_remove_event(self, item, initiator=None):
587        """Notify that a entity has been removed from the collection.
588
589        Initiator is the InstrumentedAttribute that initiated the membership
590        mutation, and should be left as None unless you are passing along
591        an initiator value from a chained operation.
592
593        """
594        if initiator is not False and item is not None:
595            self.attr.fire_remove_event(self.owner_state, self.owner_state.dict, item, initiator)
596
597    def fire_pre_remove_event(self, initiator=None):
598        """Notify that an entity is about to be removed from the collection.
599
600        Only called if the entity cannot be removed after calling
601        fire_remove_event().
602
603        """
604        self.attr.fire_pre_remove_event(self.owner_state, self.owner_state.dict, initiator=initiator)
605
606    def __getstate__(self):
607        return {'key': self.attr.key,
608                'owner_state': self.owner_state,
609                'data': self.data}
610
611    def __setstate__(self, d):
612        self.attr = getattr(d['owner_state'].obj().__class__, d['key']).impl
613        self.owner_state = d['owner_state']
614        self._data = weakref.ref(d['data'])
615
616
617def bulk_replace(values, existing_adapter, new_adapter):
618    """Load a new collection, firing events based on prior like membership.
619
620    Appends instances in ``values`` onto the ``new_adapter``. Events will be
621    fired for any instance not present in the ``existing_adapter``.  Any
622    instances in ``existing_adapter`` not present in ``values`` will have
623    remove events fired upon them.
624
625    values
626      An iterable of collection member instances
627
628    existing_adapter
629      A CollectionAdapter of instances to be replaced
630
631    new_adapter
632      An empty CollectionAdapter to load with ``values``
633
634
635    """
636    if not isinstance(values, list):
637        values = list(values)
638
639    idset = util.IdentitySet
640    constants = idset(existing_adapter or ()).intersection(values or ())
641    additions = idset(values or ()).difference(constants)
642    removals = idset(existing_adapter or ()).difference(constants)
643
644    for member in values or ():
645        if member in additions:
646            new_adapter.append_with_event(member)
647        elif member in constants:
648            new_adapter.append_without_event(member)
649
650    if existing_adapter:
651        for member in removals:
652            existing_adapter.remove_with_event(member)
653
654def prepare_instrumentation(factory):
655    """Prepare a callable for future use as a collection class factory.
656
657    Given a collection class factory (either a type or no-arg callable),
658    return another factory that will produce compatible instances when
659    called.
660
661    This function is responsible for converting collection_class=list
662    into the run-time behavior of collection_class=InstrumentedList.
663
664    """
665    # Convert a builtin to 'Instrumented*'
666    if factory in __canned_instrumentation:
667        factory = __canned_instrumentation[factory]
668
669    # Create a specimen
670    cls = type(factory())
671
672    # Did factory callable return a builtin?
673    if cls in __canned_instrumentation:
674        # Wrap it so that it returns our 'Instrumented*'
675        factory = __converting_factory(factory)
676        cls = factory()
677
678    # Instrument the class if needed.
679    if __instrumentation_mutex.acquire():
680        try:
681            if getattr(cls, '_sa_instrumented', None) != id(cls):
682                _instrument_class(cls)
683        finally:
684            __instrumentation_mutex.release()
685
686    return factory
687
688def __converting_factory(original_factory):
689    """Convert the type returned by collection factories on the fly.
690
691    Given a collection factory that returns a builtin type (e.g. a list),
692    return a wrapped function that converts that type to one of our
693    instrumented types.
694
695    """
696    def wrapper():
697        collection = original_factory()
698        type_ = type(collection)
699        if type_ in __canned_instrumentation:
700            # return an instrumented type initialized from the factory's
701            # collection
702            return __canned_instrumentation[type_](collection)
703        else:
704            raise sa_exc.InvalidRequestError(
705                "Collection class factories must produce instances of a "
706                "single class.")
707    try:
708        # often flawed but better than nothing
709        wrapper.__name__ = "%sWrapper" % original_factory.__name__
710        wrapper.__doc__ = original_factory.__doc__
711    except:
712        pass
713    return wrapper
714
715def _instrument_class(cls):
716    """Modify methods in a class and install instrumentation."""
717
718    # TODO: more formally document this as a decoratorless/Python 2.3
719    # option for specifying instrumentation.  (likely doc'd here in code only,
720    # not in online docs.)  Useful for C types too.
721    #
722    # __instrumentation__ = {
723    #   'rolename': 'methodname', # ...
724    #   'methods': {
725    #     'methodname': ('fire_{append,remove}_event', argspec,
726    #                    'fire_{append,remove}_event'),
727    #     'append': ('fire_append_event', 1, None),
728    #     '__setitem__': ('fire_append_event', 1, 'fire_remove_event'),
729    #     'pop': (None, None, 'fire_remove_event'),
730    #     }
731    #  }
732
733    # In the normal call flow, a request for any of the 3 basic collection
734    # types is transformed into one of our trivial subclasses
735    # (e.g. InstrumentedList).  Catch anything else that sneaks in here...
736    if cls.__module__ == '__builtin__':
737        raise sa_exc.ArgumentError(
738            "Can not instrument a built-in type. Use a "
739            "subclass, even a trivial one.")
740
741    collection_type = util.duck_type_collection(cls)
742    if collection_type in __interfaces:
743        roles = __interfaces[collection_type].copy()
744        decorators = roles.pop('_decorators', {})
745    else:
746        roles, decorators = {}, {}
747
748    if hasattr(cls, '__instrumentation__'):
749        roles.update(copy.deepcopy(getattr(cls, '__instrumentation__')))
750
751    methods = roles.pop('methods', {})
752
753    for name in dir(cls):
754        method = getattr(cls, name, None)
755        if not util.callable(method):
756            continue
757
758        # note role declarations
759        if hasattr(method, '_sa_instrument_role'):
760            role = method._sa_instrument_role
761            assert role in ('appender', 'remover', 'iterator',
762                            'on_link', 'converter')
763            roles[role] = name
764
765        # transfer instrumentation requests from decorated function
766        # to the combined queue
767        before, after = None, None
768        if hasattr(method, '_sa_instrument_before'):
769            op, argument = method._sa_instrument_before
770            assert op in ('fire_append_event', 'fire_remove_event')
771            before = op, argument
772        if hasattr(method, '_sa_instrument_after'):
773            op = method._sa_instrument_after
774            assert op in ('fire_append_event', 'fire_remove_event')
775            after = op
776        if before:
777            methods[name] = before[0], before[1], after
778        elif after:
779            methods[name] = None, None, after
780
781    # apply ABC auto-decoration to methods that need it
782    for method, decorator in decorators.items():
783        fn = getattr(cls, method, None)
784        if (fn and method not in methods and
785            not hasattr(fn, '_sa_instrumented')):
786            setattr(cls, method, decorator(fn))
787
788    # ensure all roles are present, and apply implicit instrumentation if
789    # needed
790    if 'appender' not in roles or not hasattr(cls, roles['appender']):
791        raise sa_exc.ArgumentError(
792            "Type %s must elect an appender method to be "
793            "a collection class" % cls.__name__)
794    elif (roles['appender'] not in methods and
795          not hasattr(getattr(cls, roles['appender']), '_sa_instrumented')):
796        methods[roles['appender']] = ('fire_append_event', 1, None)
797
798    if 'remover' not in roles or not hasattr(cls, roles['remover']):
799        raise sa_exc.ArgumentError(
800            "Type %s must elect a remover method to be "
801            "a collection class" % cls.__name__)
802    elif (roles['remover'] not in methods and
803          not hasattr(getattr(cls, roles['remover']), '_sa_instrumented')):
804        methods[roles['remover']] = ('fire_remove_event', 1, None)
805
806    if 'iterator' not in roles or not hasattr(cls, roles['iterator']):
807        raise sa_exc.ArgumentError(
808            "Type %s must elect an iterator method to be "
809            "a collection class" % cls.__name__)
810
811    # apply ad-hoc instrumentation from decorators, class-level defaults
812    # and implicit role declarations
813    for method, (before, argument, after) in methods.items():
814        setattr(cls, method,
815                _instrument_membership_mutator(getattr(cls, method),
816                                               before, argument, after))
817    # intern the role map
818    for role, method in roles.items():
819        setattr(cls, '_sa_%s' % role, getattr(cls, method))
820
821    setattr(cls, '_sa_instrumented', id(cls))
822
823def _instrument_membership_mutator(method, before, argument, after):
824    """Route method args and/or return value through the collection adapter."""
825    # This isn't smart enough to handle @adds(1) for 'def fn(self, (a, b))'
826    if before:
827        fn_args = list(util.flatten_iterator(inspect.getargspec(method)[0]))
828        if type(argument) is int:
829            pos_arg = argument
830            named_arg = len(fn_args) > argument and fn_args[argument] or None
831        else:
832            if argument in fn_args:
833                pos_arg = fn_args.index(argument)
834            else:
835                pos_arg = None
836            named_arg = argument
837        del fn_args
838
839    def wrapper(*args, **kw):
840        if before:
841            if pos_arg is None:
842                if named_arg not in kw:
843                    raise sa_exc.ArgumentError(
844                        "Missing argument %s" % argument)
845                value = kw[named_arg]
846            else:
847                if len(args) > pos_arg:
848                    value = args[pos_arg]
849                elif named_arg in kw:
850                    value = kw[named_arg]
851                else:
852                    raise sa_exc.ArgumentError(
853                        "Missing argument %s" % argument)
854
855        initiator = kw.pop('_sa_initiator', None)
856        if initiator is False:
857            executor = None
858        else:
859            executor = getattr(args[0], '_sa_adapter', None)
860
861        if before and executor:
862            getattr(executor, before)(value, initiator)
863
864        if not after or not executor:
865            return method(*args, **kw)
866        else:
867            res = method(*args, **kw)
868            if res is not None:
869                getattr(executor, after)(res, initiator)
870            return res
871    try:
872        wrapper._sa_instrumented = True
873        wrapper.__name__ = method.__name__
874        wrapper.__doc__ = method.__doc__
875    except:
876        pass
877    return wrapper
878
879def __set(collection, item, _sa_initiator=None):
880    """Run set events, may eventually be inlined into decorators."""
881
882    if _sa_initiator is not False and item is not None:
883        executor = getattr(collection, '_sa_adapter', None)
884        if executor:
885            item = getattr(executor, 'fire_append_event')(item, _sa_initiator)
886    return item
887   
888def __del(collection, item, _sa_initiator=None):
889    """Run del events, may eventually be inlined into decorators."""
890    if _sa_initiator is not False and item is not None:
891        executor = getattr(collection, '_sa_adapter', None)
892        if executor:
893            getattr(executor, 'fire_remove_event')(item, _sa_initiator)
894
895def __before_delete(collection, _sa_initiator=None):
896    """Special method to run 'commit existing value' methods"""
897    executor = getattr(collection, '_sa_adapter', None)
898    if executor:
899        getattr(executor, 'fire_pre_remove_event')(_sa_initiator)
900
901def _list_decorators():
902    """Tailored instrumentation wrappers for any list-like class."""
903
904    def _tidy(fn):
905        setattr(fn, '_sa_instrumented', True)
906        fn.__doc__ = getattr(getattr(list, fn.__name__), '__doc__')
907
908    def append(fn):
909        def append(self, item, _sa_initiator=None):
910            item = __set(self, item, _sa_initiator)
911            fn(self, item)
912        _tidy(append)
913        return append
914
915    def remove(fn):
916        def remove(self, value, _sa_initiator=None):
917            __before_delete(self, _sa_initiator)
918            # testlib.pragma exempt:__eq__
919            fn(self, value)
920            __del(self, value, _sa_initiator)
921        _tidy(remove)
922        return remove
923
924    def insert(fn):
925        def insert(self, index, value):
926            value = __set(self, value)
927            fn(self, index, value)
928        _tidy(insert)
929        return insert
930
931    def __setitem__(fn):
932        def __setitem__(self, index, value):
933            if not isinstance(index, slice):
934                existing = self[index]
935                if existing is not None:
936                    __del(self, existing)
937                value = __set(self, value)
938                fn(self, index, value)
939            else:
940                # slice assignment requires __delitem__, insert, __len__
941                if index.stop is None:
942                    stop = 0
943                elif index.stop < 0:
944                    stop = len(self) + index.stop
945                else:
946                    stop = index.stop
947                step = index.step or 1
948                rng = range(index.start or 0, stop, step)
949                if step == 1:
950                    for i in rng:
951                        del self[index.start]
952                    i = index.start
953                    for item in value:
954                        self.insert(i, item)
955                        i += 1
956                else:
957                    if len(value) != len(rng):
958                        raise ValueError(
959                            "attempt to assign sequence of size %s to "
960                            "extended slice of size %s" % (len(value),
961                                                           len(rng)))
962                    for i, item in zip(rng, value):
963                        self.__setitem__(i, item)
964        _tidy(__setitem__)
965        return __setitem__
966
967    def __delitem__(fn):
968        def __delitem__(self, index):
969            if not isinstance(index, slice):
970                item = self[index]
971                __del(self, item)
972                fn(self, index)
973            else:
974                # slice deletion requires __getslice__ and a slice-groking
975                # __getitem__ for stepped deletion
976                # note: not breaking this into atomic dels
977                for item in self[index]:
978                    __del(self, item)
979                fn(self, index)
980        _tidy(__delitem__)
981        return __delitem__
982
983    def __setslice__(fn):
984        def __setslice__(self, start, end, values):
985            for value in self[start:end]:
986                __del(self, value)
987            values = [__set(self, value) for value in values]
988            fn(self, start, end, values)
989        _tidy(__setslice__)
990        return __setslice__
991
992    def __delslice__(fn):
993        def __delslice__(self, start, end):
994            for value in self[start:end]:
995                __del(self, value)
996            fn(self, start, end)
997        _tidy(__delslice__)
998        return __delslice__
999
1000    def extend(fn):
1001        def extend(self, iterable):
1002            for value in iterable:
1003                self.append(value)
1004        _tidy(extend)
1005        return extend
1006
1007    def __iadd__(fn):
1008        def __iadd__(self, iterable):
1009            # list.__iadd__ takes any iterable and seems to let TypeError raise
1010            # as-is instead of returning NotImplemented
1011            for value in iterable:
1012                self.append(value)
1013            return self
1014        _tidy(__iadd__)
1015        return __iadd__
1016
1017    def pop(fn):
1018        def pop(self, index=-1):
1019            __before_delete(self)
1020            item = fn(self, index)
1021            __del(self, item)
1022            return item
1023        _tidy(pop)
1024        return pop
1025
1026    # __imul__ : not wrapping this.  all members of the collection are already
1027    # present, so no need to fire appends... wrapping it with an explicit
1028    # decorator is still possible, so events on *= can be had if they're
1029    # desired.  hard to imagine a use case for __imul__, though.
1030
1031    l = locals().copy()
1032    l.pop('_tidy')
1033    return l
1034
1035def _dict_decorators():
1036    """Tailored instrumentation wrappers for any dict-like mapping class."""
1037
1038    def _tidy(fn):
1039        setattr(fn, '_sa_instrumented', True)
1040        fn.__doc__ = getattr(getattr(dict, fn.__name__), '__doc__')
1041
1042    Unspecified = util.symbol('Unspecified')
1043
1044    def __setitem__(fn):
1045        def __setitem__(self, key, value, _sa_initiator=None):
1046            if key in self:
1047                __del(self, self[key], _sa_initiator)
1048            value = __set(self, value, _sa_initiator)
1049            fn(self, key, value)
1050        _tidy(__setitem__)
1051        return __setitem__
1052
1053    def __delitem__(fn):
1054        def __delitem__(self, key, _sa_initiator=None):
1055            if key in self:
1056                __del(self, self[key], _sa_initiator)
1057            fn(self, key)
1058        _tidy(__delitem__)
1059        return __delitem__
1060
1061    def clear(fn):
1062        def clear(self):
1063            for key in self:
1064                __del(self, self[key])
1065            fn(self)
1066        _tidy(clear)
1067        return clear
1068
1069    def pop(fn):
1070        def pop(self, key, default=Unspecified):
1071            if key in self:
1072                __del(self, self[key])
1073            if default is Unspecified:
1074                return fn(self, key)
1075            else:
1076                return fn(self, key, default)
1077        _tidy(pop)
1078        return pop
1079
1080    def popitem(fn):
1081        def popitem(self):
1082            __before_delete(self)
1083            item = fn(self)
1084            __del(self, item[1])
1085            return item
1086        _tidy(popitem)
1087        return popitem
1088
1089    def setdefault(fn):
1090        def setdefault(self, key, default=None):
1091            if key not in self:
1092                self.__setitem__(key, default)
1093                return default
1094            else:
1095                return self.__getitem__(key)
1096        _tidy(setdefault)
1097        return setdefault
1098
1099    if sys.version_info < (2, 4):
1100        def update(fn):
1101            def update(self, other):
1102                for key in other.keys():
1103                    if key not in self or self[key] is not other[key]:
1104                        self[key] = other[key]
1105            _tidy(update)
1106            return update
1107    else:
1108        def update(fn):
1109            def update(self, __other=Unspecified, **kw):
1110                if __other is not Unspecified:
1111                    if hasattr(__other, 'keys'):
1112                        for key in __other.keys():
1113                            if (key not in self or
1114                                self[key] is not __other[key]):
1115                                self[key] = __other[key]
1116                    else:
1117                        for key, value in __other:
1118                            if key not in self or self[key] is not value:
1119                                self[key] = value
1120                for key in kw:
1121                    if key not in self or self[key] is not kw[key]:
1122                        self[key] = kw[key]
1123            _tidy(update)
1124            return update
1125
1126    l = locals().copy()
1127    l.pop('_tidy')
1128    l.pop('Unspecified')
1129    return l
1130
1131if util.py3k:
1132    _set_binop_bases = (set, frozenset)
1133else:
1134    import sets
1135    _set_binop_bases = (set, frozenset, sets.BaseSet)
1136
1137def _set_binops_check_strict(self, obj):
1138    """Allow only set, frozenset and self.__class__-derived objects in binops."""
1139    return isinstance(obj, _set_binop_bases + (self.__class__,))
1140
1141def _set_binops_check_loose(self, obj):
1142    """Allow anything set-like to participate in set binops."""
1143    return (isinstance(obj, _set_binop_bases + (self.__class__,)) or
1144            util.duck_type_collection(obj) == set)
1145
1146
1147def _set_decorators():
1148    """Tailored instrumentation wrappers for any set-like class."""
1149
1150    def _tidy(fn):
1151        setattr(fn, '_sa_instrumented', True)
1152        fn.__doc__ = getattr(getattr(set, fn.__name__), '__doc__')
1153
1154    Unspecified = util.symbol('Unspecified')
1155
1156    def add(fn):
1157        def add(self, value, _sa_initiator=None):
1158            if value not in self:
1159                value = __set(self, value, _sa_initiator)
1160            # testlib.pragma exempt:__hash__
1161            fn(self, value)
1162        _tidy(add)
1163        return add
1164
1165    if sys.version_info < (2, 4):
1166        def discard(fn):
1167            def discard(self, value, _sa_initiator=None):
1168                if value in self:
1169                    self.remove(value, _sa_initiator)
1170            _tidy(discard)
1171            return discard
1172    else:
1173        def discard(fn):
1174            def discard(self, value, _sa_initiator=None):
1175                # testlib.pragma exempt:__hash__
1176                if value in self:
1177                    __del(self, value, _sa_initiator)
1178                    # testlib.pragma exempt:__hash__
1179                fn(self, value)
1180            _tidy(discard)
1181            return discard
1182
1183    def remove(fn):
1184        def remove(self, value, _sa_initiator=None):
1185            # testlib.pragma exempt:__hash__
1186            if value in self:
1187                __del(self, value, _sa_initiator)
1188            # testlib.pragma exempt:__hash__
1189            fn(self, value)
1190        _tidy(remove)
1191        return remove
1192
1193    def pop(fn):
1194        def pop(self):
1195            __before_delete(self)
1196            item = fn(self)
1197            __del(self, item)
1198            return item
1199        _tidy(pop)
1200        return pop
1201
1202    def clear(fn):
1203        def clear(self):
1204            for item in list(self):
1205                self.remove(item)
1206        _tidy(clear)
1207        return clear
1208
1209    def update(fn):
1210        def update(self, value):
1211            for item in value:
1212                self.add(item)
1213        _tidy(update)
1214        return update
1215
1216    def __ior__(fn):
1217        def __ior__(self, value):
1218            if not _set_binops_check_strict(self, value):
1219                return NotImplemented
1220            for item in value:
1221                self.add(item)
1222            return self
1223        _tidy(__ior__)
1224        return __ior__
1225
1226    def difference_update(fn):
1227        def difference_update(self, value):
1228            for item in value:
1229                self.discard(item)
1230        _tidy(difference_update)
1231        return difference_update
1232
1233    def __isub__(fn):
1234        def __isub__(self, value):
1235            if not _set_binops_check_strict(self, value):
1236                return NotImplemented
1237            for item in value:
1238                self.discard(item)
1239            return self
1240        _tidy(__isub__)
1241        return __isub__
1242
1243    def intersection_update(fn):
1244        def intersection_update(self, other):
1245            want, have = self.intersection(other), set(self)
1246            remove, add = have - want, want - have
1247
1248            for item in remove:
1249                self.remove(item)
1250            for item in add:
1251                self.add(item)
1252        _tidy(intersection_update)
1253        return intersection_update
1254
1255    def __iand__(fn):
1256        def __iand__(self, other):
1257            if not _set_binops_check_strict(self, other):
1258                return NotImplemented
1259            want, have = self.intersection(other), set(self)
1260            remove, add = have - want, want - have
1261
1262            for item in remove:
1263                self.remove(item)
1264            for item in add:
1265                self.add(item)
1266            return self
1267        _tidy(__iand__)
1268        return __iand__
1269
1270    def symmetric_difference_update(fn):
1271        def symmetric_difference_update(self, other):
1272            want, have = self.symmetric_difference(other), set(self)
1273            remove, add = have - want, want - have
1274
1275            for item in remove:
1276                self.remove(item)
1277            for item in add:
1278                self.add(item)
1279        _tidy(symmetric_difference_update)
1280        return symmetric_difference_update
1281
1282    def __ixor__(fn):
1283        def __ixor__(self, other):
1284            if not _set_binops_check_strict(self, other):
1285                return NotImplemented
1286            want, have = self.symmetric_difference(other), set(self)
1287            remove, add = have - want, want - have
1288
1289            for item in remove:
1290                self.remove(item)
1291            for item in add:
1292                self.add(item)
1293            return self
1294        _tidy(__ixor__)
1295        return __ixor__
1296
1297    l = locals().copy()
1298    l.pop('_tidy')
1299    l.pop('Unspecified')
1300    return l
1301
1302
1303class InstrumentedList(list):
1304    """An instrumented version of the built-in list."""
1305
1306    __instrumentation__ = {
1307       'appender': 'append',
1308       'remover': 'remove',
1309       'iterator': '__iter__', }
1310
1311class InstrumentedSet(set):
1312    """An instrumented version of the built-in set."""
1313
1314    __instrumentation__ = {
1315       'appender': 'add',
1316       'remover': 'remove',
1317       'iterator': '__iter__', }
1318
1319class InstrumentedDict(dict):
1320    """An instrumented version of the built-in dict."""
1321
1322    __instrumentation__ = {
1323        'iterator': 'itervalues', }
1324
1325__canned_instrumentation = {
1326    list: InstrumentedList,
1327    set: InstrumentedSet,
1328    dict: InstrumentedDict,
1329    }
1330
1331__interfaces = {
1332    list: {'appender': 'append',
1333           'remover': 'remove',
1334           'iterator': '__iter__',
1335           '_decorators': _list_decorators(), },
1336    set: {'appender': 'add',
1337          'remover': 'remove',
1338          'iterator': '__iter__',
1339          '_decorators': _set_decorators(), },
1340    # decorators are required for dicts and object collections.
1341    dict: {'iterator': 'itervalues',
1342           '_decorators': _dict_decorators(), },
1343    # < 0.4 compatible naming, deprecated- use decorators instead.
1344    None: {}
1345    }
1346
1347class MappedCollection(dict):
1348    """A basic dictionary-based collection class.
1349
1350    Extends dict with the minimal bag semantics that collection classes require.
1351    ``set`` and ``remove`` are implemented in terms of a keying function: any
1352    callable that takes an object and returns an object for use as a dictionary
1353    key.
1354
1355    """
1356
1357    def __init__(self, keyfunc):
1358        """Create a new collection with keying provided by keyfunc.
1359
1360        keyfunc may be any callable any callable that takes an object and
1361        returns an object for use as a dictionary key.
1362
1363        The keyfunc will be called every time the ORM needs to add a member by
1364        value-only (such as when loading instances from the database) or
1365        remove a member.  The usual cautions about dictionary keying apply-
1366        ``keyfunc(object)`` should return the same output for the life of the
1367        collection.  Keying based on mutable properties can result in
1368        unreachable instances "lost" in the collection.
1369
1370        """
1371        self.keyfunc = keyfunc
1372
1373    def set(self, value, _sa_initiator=None):
1374        """Add an item by value, consulting the keyfunc for the key."""
1375
1376        key = self.keyfunc(value)
1377        self.__setitem__(key, value, _sa_initiator)
1378    set = collection.internally_instrumented(set)
1379    set = collection.appender(set)
1380
1381    def remove(self, value, _sa_initiator=None):
1382        """Remove an item by value, consulting the keyfunc for the key."""
1383
1384        key = self.keyfunc(value)
1385        # Let self[key] raise if key is not in this collection
1386        # testlib.pragma exempt:__ne__
1387        if self[key] != value:
1388            raise sa_exc.InvalidRequestError(
1389                "Can not remove '%s': collection holds '%s' for key '%s'. "
1390                "Possible cause: is the MappedCollection key function "
1391                "based on mutable properties or properties that only obtain "
1392                "values after flush?" %
1393                (value, self[key], key))
1394        self.__delitem__(key, _sa_initiator)
1395    remove = collection.internally_instrumented(remove)
1396    remove = collection.remover(remove)
1397
1398    def _convert(self, dictlike):
1399        """Validate and convert a dict-like object into values for set()ing.
1400
1401        This is called behind the scenes when a MappedCollection is replaced
1402        entirely by another collection, as in::
1403
1404          myobj.mappedcollection = {'a':obj1, 'b': obj2} # ...
1405
1406        Raises a TypeError if the key in any (key, value) pair in the dictlike
1407        object does not match the key that this collection's keyfunc would
1408        have assigned for that value.
1409
1410        """
1411        for incoming_key, value in util.dictlike_iteritems(dictlike):
1412            new_key = self.keyfunc(value)
1413            if incoming_key != new_key:
1414                raise TypeError(
1415                    "Found incompatible key %r for value %r; this collection's "
1416                    "keying function requires a key of %r for this value." % (
1417                    incoming_key, value, new_key))
1418            yield value
1419    _convert = collection.converter(_convert)
Note: リポジトリブラウザについてのヘルプは TracBrowser を参照してください。