From: Mike Bayer Date: Sun, 8 Aug 2010 23:56:45 +0000 (-0400) Subject: fixes, but still unsure of things X-Git-Tag: rel_0_7b1~253^2~37 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=d47ec4158dd75cfd254db60002f797cfc24502fa;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git fixes, but still unsure of things --- d47ec4158dd75cfd254db60002f797cfc24502fa diff --cc doc/build/reference/sqlalchemy/event.rst index 0000000000,0000000000..05e7ab63b7 new file mode 100644 --- /dev/null +++ b/doc/build/reference/sqlalchemy/event.rst @@@ -1,0 -1,0 +1,7 @@@ ++Events ++------ ++ ++.. automodule:: sqlalchemy.event ++ :members: ++ ++ diff --cc lib/sqlalchemy/event.py index 73be57b55c,0000000000..f9fbcc118f mode 100644,000000..100644 --- a/lib/sqlalchemy/event.py +++ b/lib/sqlalchemy/event.py @@@ -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 diff --cc lib/sqlalchemy/orm/attributes.py index 76a89a0d03,800f3889f7..9d454a7b28 --- a/lib/sqlalchemy/orm/attributes.py +++ b/lib/sqlalchemy/orm/attributes.py @@@ -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. diff --cc lib/sqlalchemy/util.py index d274e36350,d274e36350..c26f38b6bd --- a/lib/sqlalchemy/util.py +++ b/lib/sqlalchemy/util.py @@@ -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)