]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
fixes, but still unsure of things
authorMike Bayer <mike_mp@zzzcomputing.com>
Sun, 8 Aug 2010 23:56:45 +0000 (19:56 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Sun, 8 Aug 2010 23:56:45 +0000 (19:56 -0400)
1  2 
doc/build/reference/sqlalchemy/event.rst
lib/sqlalchemy/event.py
lib/sqlalchemy/orm/attributes.py
lib/sqlalchemy/orm/interfaces.py
lib/sqlalchemy/util.py

index 0000000000000000000000000000000000000000,0000000000000000000000000000000000000000..05e7ab63b7e7dc09c19c75769c6de4b6969cbd82
new file mode 100644 (file)
--- /dev/null
--- /dev/null
@@@ -1,0 -1,0 +1,7 @@@
++Events
++------
++
++.. automodule:: sqlalchemy.event
++   :members:
++
++
index 73be57b55c26250f75f72dd6d2ec5536c0b3467e,0000000000000000000000000000000000000000..f9fbcc118fd9d9d1a098dc9904dbbf24459cee40
mode 100644,000000..100644
--- /dev/null
@@@ -1,159 -1,0 +1,162 @@@
 +"""
 +The event system handles all events throughout the sqlalchemy
 +and sqlalchemy.orm packages.   
 +
 +Event specifications:
 +
 +:attr:`sqlalchemy.pool.Pool.events`
 +
 +"""
 +
 +from sqlalchemy import util
 +
 +def listen(fn, identifier, target, *args, **kw):
 +    """Listen for events, passing to fn."""
 +
 +    # rationale - the events on ClassManager, Session, and Mapper
 +    # will need to accept mapped classes directly as targets and know 
 +    # what to do
 +    
 +    for evt_cls in _registrars[identifier]:
 +        for tgt in evt_cls.accept_with(target):
 +            tgt.events.listen(fn, identifier, tgt, *args, **kw)
 +            break
 +    
 +class _DispatchMeta(type):
 +    def __init__(cls, classname, bases, dict_):
 +        for k in dict_:
 +            if k.startswith('on_'):
 +                setattr(cls, k, EventDescriptor(dict_[k]))
 +                _registrars[k].append(cls)
 +        return type.__init__(cls, classname, bases, dict_)
 +
 +_registrars = util.defaultdict(list)
 +
 +class Events(object):
 +    __metaclass__ = _DispatchMeta
 +    
 +    def __init__(self, parent_cls):
 +        self.parent_cls = parent_cls
 +    
 +    @classmethod
 +    def accept_with(cls, target):
 +        # Mapper, ClassManager, Session override this to
 +        # also accept classes, scoped_sessions, sessionmakers, etc.
 +        if hasattr(target, 'events') and (
 +                    isinstance(target.events, cls) or \
 +                    isinstance(target.events, type) and \
 +                    issubclass(target.events, cls)
 +                ):
 +            return [target]
 +        else:
 +            return []
 +        
 +    @classmethod
 +    def listen(cls, fn, identifier, target):
 +        getattr(target.events, identifier).append(fn, target)
 +    
 +    @property
 +    def events(self):
 +        """Iterate the Listeners objects."""
 +        
 +        return (getattr(self, k) for k in dir(self) if k.startswith("on_"))
 +        
 +    def update(self, other):
 +        """Populate from the listeners in another :class:`Events` object."""
 +
 +        for ls in other.events:
 +            getattr(self, ls.name).listeners.extend(ls.listeners)
 +
 +class _ExecEvent(object):
 +    _exec_once = False
 +    
 +    def exec_once(self, *args, **kw):
 +        """Execute this event, but only if it has not been
 +        executed already for this collection."""
 +        
 +        if not self._exec_once:
 +            self(*args, **kw)
 +            self._exec_once = True
 +    
 +    def exec_until_return(self, *args, **kw):
 +        """Execute listeners for this event until
 +        one returns a non-None value.
 +        
 +        Returns the value, or None.
 +        """
 +        
 +        if self:
 +            for fn in self:
 +                r = fn(*args, **kw)
 +                if r is not None:
 +                    return r
 +        return None
 +        
 +    def __call__(self, *args, **kw):
 +        """Execute this event."""
 +        
 +        if self:
 +            for fn in self:
 +                fn(*args, **kw)
 +    
 +class EventDescriptor(object):
 +    """Represent an event type associated with a :class:`Events` class
 +    as well as class-level listeners.
 +    
 +    """
 +    def __init__(self, fn):
 +        self.__name__ = fn.__name__
 +        self.__doc__ = fn.__doc__
 +        self._clslevel = util.defaultdict(list)
 +    
 +    def append(self, obj, target):
 +        assert isinstance(target, type), "Class-level Event targets must be classes."
 +        for cls in [target] + target.__subclasses__():
 +            self._clslevel[cls].append(obj)
 +    
 +    def __get__(self, obj, cls):
 +        if obj is None:
 +            return self
 +        obj.__dict__[self.__name__] = result = Listeners(self, obj.parent_cls)
 +        return result
 +
 +class Listeners(_ExecEvent):
 +    """Represent a collection of listeners linked
 +    to an instance of :class:`Events`."""
 +    
 +    def __init__(self, parent, target_cls):
 +        self.parent_listeners = parent._clslevel[target_cls]
 +        self.name = parent.__name__
 +        self.listeners = []
 +    
 +    # I'm not entirely thrilled about the overhead here,
 +    # but this allows class-level listeners to be added
 +    # at any point.
 +    
 +    def __len__(self):
 +        return len(self.parent_listeners + self.listeners)
 +        
 +    def __iter__(self):
 +        return iter(self.parent_listeners + self.listeners)
 +    
 +    def __getitem__(self, index):
 +        return (self.parent_listeners + self.listeners)[index]
 +        
 +    def __nonzero__(self):
 +        return bool(self.listeners or self.parent_listeners)
 +        
 +    def append(self, obj, target):
++        # this will be needed, but not 
++        # sure why we don't seem to need it yet
++ #       if obj not in self.listeners:
 +        self.listeners.append(obj)
 +
 +class dispatcher(object):
 +    def __init__(self, events):
 +        self.dispatch_cls = events
 +        
 +    def __get__(self, obj, cls):
 +        if obj is None:
 +            return self.dispatch_cls
 +        obj.__dict__['events'] = disp = self.dispatch_cls(cls)
 +        return disp
index 76a89a0d03633f2cda6ab499b73a73e8a381fab9,800f3889f7a6a7402718e0953c0fde8dc921708d..9d454a7b283da11a21188bb38bf48d692b21631d
@@@ -104,56 -104,6 +104,59 @@@ class QueryableAttribute(interfaces.Pro
          self.comparator = comparator
          self.parententity = parententity
  
-     # TODO: this can potentially be moved to AttributeImpl,
-     # have Sphinx document the "events" class directly, implement an 
-     # accept_with() that checks for QueryableAttribute
 +    class events(event.Events):
 +        """Events for ORM attributes.
 +
 +        e.g.::
 +        
 +            from sqlalchemy import event
 +            event.listen(listener, 'on_append', MyClass.collection)
 +            event.listen(listener, 'on_set', MyClass.some_scalar, active_history=True)
 +            
 +        active_history = True indicates that the "on_set" event would like
 +        to receive the 'old' value, even if it means firing lazy callables.
 +        
 +        """
 +        
 +        active_history = False
 +        
++        # TODO: what to do about subclasses !!
++        # a shared approach will be needed.   listeners can be placed
++        # before subclasses are created.   new attrs on subclasses
++        # can pull them from the superclass attr.  listeners
++        # should be auto-propagated to existing subclasses.
++        
 +        @classmethod
 +        def listen(cls, fn, identifier, target, active_history=False):
 +            if active_history:
 +                target.events.active_history = True
 +            event.Events.listen(fn, identifier, target)
 +            
 +        def on_append(self, state, value, initiator):
 +            """Receive a collection append event.
 +
 +            The returned value will be used as the actual value to be
 +            appended.
 +
 +            """
 +
 +        def on_remove(self, state, value, initiator):
 +            """Receive a remove event.
 +
 +            No return value is defined.
 +
 +            """
 +
 +        def on_set(self, state, value, oldvalue, initiator):
 +            """Receive a set event.
 +
 +            The returned value will be used as the actual value to be
 +            set.
 +
 +            """
 +
 +    events = event.dispatcher(events)
 +
      def get_history(self, instance, **kwargs):
          return self.impl.get_history(instance_state(instance),
                                          instance_dict(instance), **kwargs)
@@@ -346,17 -291,15 +349,19 @@@ class AttributeImpl(object)
              self.is_equal = operator.eq
          else:
              self.is_equal = compare_function
 -        self.extensions = util.to_list(extension or [])
 -        for e in self.extensions:
 -            if e.active_history:
 -                active_history = True
 -                break
 -        self.active_history = active_history
 -        self.expire_missing = expire_missing
          
-         attr = getattr(class_, key)
++        # TODO: pass in the manager here
++        # instead of doing a lookup
++        attr = manager_of_class(class_)[key]
+         
 +        for ext in util.to_list(extension or []):
 +            ext._adapt_listener(attr, ext)
 +            
 +        if active_history:
 +            events.active_history = True
 +            
 +        self.expire_missing = expire_missing
 +
-     
      def hasparent(self, state, optimistic=False):
          """Return the boolean value of a `hasparent` flag attached to 
          the given state.
Simple merge
index d274e3635076abf9d0c26cc5bead40a4f3dc3476,d274e3635076abf9d0c26cc5bead40a4f3dc3476..c26f38b6bdfbc97b73969a49ffb7207b2a99c0cc
@@@ -907,7 -907,7 +907,10 @@@ class OrderedSet(set)
  
      def __iter__(self):
          return iter(self._list)
--
++    
++    def __add__(self, other):
++        return self.union(other)
++        
      def __repr__(self):
          return '%s(%r)' % (self.__class__.__name__, self._list)