[Subversion] / Trellis / peak / events / trellis.py  

View of /Trellis/peak/events/trellis.py

Parent Directory | Revision Log
Revision: 2610 - (download) (as text)
Wed May 5 18:07:24 2010 UTC (13 years, 11 months ago) by pje
File size: 44034 byte(s)
Support 2.6 -OO option by switching to newer DecoratorTools
from thread import get_ident
from weakref import ref
from peak.util import addons, decorators
import sys, UserDict, UserList, sets, stm, types, new, weakref, copy
from peak.util.extremes import Max
from peak.util.symbols import Symbol, NOT_GIVEN

__all__ = [
    'Cell', 'Constant', 'make', 'todo', 'todos', 'modifier',
    'Component', 'repeat', 'poll', 'InputConflict',
    'Dict', 'List', 'Set', 'mark_dirty', 'ctrl', 'ConstantMixin', 'Sensor',
    'AbstractConnector', 'Connector',  'Effector', 'init_attrs',
    'attr', 'attrs', 'compute', 'maintain', 'perform', 'Performer', 'Pipe',
]

NO_VALUE = Symbol('NO_VALUE', __name__)
_sentinel = NO_VALUE


class InputConflict(Exception):
    """Attempt to set a cell to two different values during the same pulse"""


def init_attrs(self, **kw):
    """Initialize attributes from keyword arguments"""
    if kw:
        cls = type(self)
        for k, v in kw.iteritems():
            if not hasattr(cls, k):
                raise TypeError("%s() has no keyword argument %r"
                    % (cls.__name__, k)
                )
            setattr(self, k, v)

def named_lambda(func, name):
    if getattr(func,'__name__',None) == '<lambda>':
        try: func.__name__ = name
        except TypeError: pass  # Python 2.3 doesn't let you set __name__
    return func


try:
    set = set
except NameError:
    set = sets.Set
    frozenset = sets.ImmutableSet
    set_like = sets.BaseSet
    dictlike = dict, sets.BaseSet
else:
    set_like = set, frozenset, sets.BaseSet
    dictlike = (dict,) + set_like


class AbstractCell(object):
    """Base class for cells"""
    __slots__ = ()

    rule = value = _value = _needs_init = None
    writer = connector = None
    was_set = False
    _uninit_repr = ' [uninitialized]'

    def get_value(self):
        """Get the value of this cell"""
        return self.value

    def __repr__(self):
        rule = reset = ni = ''
        if getattr(self, 'rule', None) is not None:
            rule = repr(self.rule)+', '
        if self._needs_init:
            ni = self._uninit_repr
        if getattr(self, '_reset', _sentinel) is not _sentinel:
            reset =', discrete['+repr(self._reset)+']'
        return '%s(%s%r%s%s)'% (
            self.__class__.__name__, rule, self._value, ni, reset
        )





class _ReadValue(stm.AbstractSubject, AbstractCell):
    """Base class for readable cells"""

    __slots__ = '_value', 'next_listener', '_set_by', '_reset', # XXX 'manager'

    def __init__(self, value=None, discrete=False):
        self._value = value
        self._set_by = _sentinel
        stm.AbstractSubject.__init__(self)
        self._reset = (_sentinel, value)[bool(discrete)]
        if ctrl.newcells is not None:
            ctrl.new_cell(self)

    def get_value(self):
        if ctrl.active:
            # if atomic, make sure we're locked and consistent
            used(self)
        return self._value

    value = property(get_value)

    def _finish(self):
        if self._set_by is not _sentinel:
            change_attr(self, '_set_by', _sentinel)
        if self._reset is not _sentinel and self._value != self._reset:
            change_attr(self, '_value', self._reset)
            changed(self)

    decorators.decorate(property)
    def was_set(self):
        """True if value was set during this recalc"""
        if ctrl.current_listener is None:
            raise RuntimeError("was_set can only be accessed by a rule")
        used(self)
        who = self._set_by
        return who is not _sentinel and who is not self





class Value(_ReadValue):
    """A read-write value with optional discrete mode"""

    __slots__ = ('__weakref__')

    def set_value(self, value):
        if not ctrl.active:
            return atomically(self.set_value, value)

        lock(self)
        if self._set_by is _sentinel:
            change_attr(self, '_set_by', ctrl.current_listener)
            on_commit(self._finish)

        if value is self._value:
            return  # no change, no foul...

        if value!=self._value:
            if self._set_by not in (ctrl.current_listener, self):
                # already set by someone else
                raise InputConflict(self._value, value) #self._set_by) #, value, ctrl.current_listener) # XXX
            changed(self)

        change_attr(self, '_value', value)

    value = property(_ReadValue.get_value.im_func, set_value)


def install_controller(controller):
    global ctrl
    stm.ctrl = ctrl = controller
    for name in [
        'on_commit', 'on_undo', 'atomically', 'manage', 'savepoint',
        'rollback_to', 'schedule', 'cancel', 'lock', 'used', 'changed',
        'initialize', 'change_attr',
    ]:
        globals()[name] = getattr(ctrl, name)
        if name not in __all__: __all__.append(name)

install_controller(stm.LocalController())

class ReadOnlyCell(_ReadValue, stm.AbstractListener):
    """A cell with a rule"""
    __slots__ = 'rule', '_needs_init', 'next_subject', '__weakref__', 'layer'

    def __init__(self, rule, value=None, discrete=False):
        super(ReadOnlyCell, self).__init__(value, discrete)
        stm.AbstractListener.__init__(self)
        self._needs_init = True
        self.rule = rule
        self.layer = 0

    def get_value(self):
        if self._needs_init:
            if not ctrl.active:
                # initialization must be atomic
                atomically(schedule, self)
                return self._value
            else:
                cancel(self); initialize(self)
        if ctrl.current_listener is not None:
            used(self)
        return self._value

    value = property(get_value)

    def run(self):
        if self._needs_init:
            change_attr(self, '_needs_init', False)
            change_attr(self, '_set_by', self)
            change_attr(self, '_value', self.rule())
            on_commit(self._finish)
        else:
            value = self.rule()
            if value!=self._value:
                if self._set_by is _sentinel:
                    change_attr(self, '_set_by', self)
                    on_commit(self._finish)
                change_attr(self, '_value', value)
                changed(self)
        if not ctrl.reads: on_commit(self._check_const)

    def _check_const(self):
        if self.next_subject is None and (
            self._reset is _sentinel or self._value==self._reset
        ):
            change_attr(self, '_set_by', _sentinel)
            change_attr(self, 'rule', None)
            change_attr(self, 'next_listener', None)
            change_attr(self, '__class__', self._const_class())

    def _const_class(self):
        return ConstantRule


class ConstantMixin(AbstractCell):
    """A read-only abstract cell"""

    __slots__ = ()

    def __setattr__(self, name, value):
        """Constants can't be changed"""
        if name == '__class__':
            object.__setattr__(self, name, value)
        else:
            raise AttributeError("Constants can't be changed")

    def __repr__(self):
        return "Constant(%r)" % (self.value,)

class Constant(ConstantMixin):
    """A pure read-only value"""

    __slots__ = 'value'

    def __init__(self, value):
        Constant.value.__set__(self, value)

    decorators.decorate(classmethod)
    def from_attr(cls, rule, value, discrete):
        return cls(value)


class ConstantRule(ConstantMixin, ReadOnlyCell):
    """A read-only cell that no longer depends on anything else"""

    __slots__ = ()

    value = ReadOnlyCell._value

    def dirty(self):
        """Constants don't need recalculation"""
        return False

    def run(self):
        """Constants don't run"""



class Performer(stm.AbstractListener, AbstractCell):
    """Rule that performs non-undoable actions"""

    __slots__ = 'run', 'next_subject', '__weakref__'

    layer = Max

    def __init__(self, rule):
        self.run = rule
        super(Performer, self).__init__()
        atomically(schedule, self)

    decorators.decorate(classmethod)
    def from_attr(cls, rule, value, discrete):
        return cls(rule)

Performer.rule = Performer.run    # alias the attribute for inspection








def modifier(func):
    """Mark a function as performing modifications to Trellis data

    The wrapped function will always run atomically, and if called from inside
    a rule, reads performed in the function will not become dependencies of the
    caller.
    """
    def wrap(__func, __module):
        return """
        if not __module.ctrl.active:
            return __module.atomically(__func, $args)
        elif __module.ctrl.current_listener is None:
            return __func($args)
        else:
            # Prevent any reads from counting against the current rule
            old_reads, __module.ctrl.reads = __module.ctrl.reads, {}
            try:
                return __func($args)
            finally:
                __module.ctrl.reads = old_reads
        """
    return decorators.template_function(wrap)(func, sys.modules[__name__])



















set_next_listener = ReadOnlyCell.next_listener.__set__
get_next_listener = ReadOnlyCell.next_listener.__get__

class SensorBase(ReadOnlyCell):
    """Base for cells that connect to non-Trellis code"""

    __slots__ = ()

    def __init__(self, rule, value=None, discrete=False):
        if isinstance(rule, AbstractConnector):
            self.connector = rule
            rule = rule.read
        else:
            self.connector = None
        self.listening = NOT_GIVEN
        set_next_listener(self, None)
        super(SensorBase, self).__init__(rule, value, discrete)

    def _set_listener(self, listener):
        was_seen = get_next_listener(self) is not None
        set_next_listener(self, listener)
        if was_seen != (listener is not None) and self.connector is not None:
            atomically(on_commit, schedule, self.update_connection)

    next_listener = property(get_next_listener, _set_listener)

    _set_value = Value.set_value.im_func

    decorators.decorate(modifier)
    def receive(self, value):
        if not ctrl.active:
            return atomically(self.receive, value)
        lock(self)
        self._set_value(value)
        change_attr(self, '_set_by', self)

    def _check_const(self): pass    # we can never become Constant




    def update_connection():
        old_listener, ctrl.current_listener = ctrl.current_listener, None
        try:
            self = old_listener.im_self
            descr = type(self).listening
            listening = descr.__get__(self)
            if self.next_listener is not None:
                if listening is NOT_GIVEN:
                    descr.__set__(self, self.connector.connect(self))
            elif listening is not NOT_GIVEN:
                self.connector.disconnect(self, listening)
                descr.__set__(self, NOT_GIVEN)
        finally:
            ctrl.current_listener = old_listener

    update_connection.run = update_connection
    update_connection.next_subject = update_connection.next_listener = None
    update_connection.layer = -1

class Sensor(SensorBase):
    """A cell that can receive value callbacks from the outside world"""
    __slots__ = 'connector', 'listening'

class AbstractConnector(object):
    """Base class for rules that connect to the outside world"""
    __slots__ = ()

    def read(self):
        """Return a value from the outside source"""
        # Just use the current/last received value by default
        return ctrl.current_listener._value

    def connect(self, sensor):
        """Connect the sensor to the outside world, returning disconnect key

        This method must arrange callbacks to ``sensor.receive(value)``, and
        return an object suitable for use by ``disconnect()``."""

    def disconnect(self, sensor, key):
        """Disconnect the key returned by ``connect()``"""

class Connector(AbstractConnector):
    """Trivial connector, wrapping three functions"""

    __slots__ = "read", "connect", "disconnect"

    def __init__(self, connect, disconnect, read=None):
        if read is None:
            read = noop
        self.read = read
        self.connect = connect
        self.disconnect = disconnect


def noop():
    """A rule that leaves its current/initial value unchanged"""
    return ctrl.current_listener._value



class LazyConnector(AbstractConnector):
    """Dummy connector object used for lazy cells"""

    decorators.decorate(staticmethod)
    def connect(sensor):
        pass

    decorators.decorate(staticmethod)
    def disconnect(sensor, key):
        link = sensor.next_subject
        if link is not None: change_attr(sensor, '_needs_init', True)
        while link is not None:
            nxt = link.next_subject   # avoid unlinks breaking iteration
            on_undo(stm.Link, link.subject, sensor)
            link.unlink()
            link = nxt






class LazyCell(Sensor):
    """A ReadOnlyCell that is only recalculated when it has listeners"""

    __slots__ = ()
    _uninit_repr = ' [inactive]'

    def __init__(self, rule, value=None, discrete=False):
        super(LazyCell, self).__init__(rule, value, discrete)
        #if self.connector is not None: raise XXX
        self.connector = LazyConnector

    _check_const = ReadOnlyCell._check_const

    def run(self):
        run = super(LazyCell, self).run
        if not ctrl.readonly:
            ctrl.with_readonly(run)
        else: run()
        if self.listening is NOT_GIVEN:  # may need to disconnect...
            self.listening = None; on_commit(schedule, self.update_connection)

    def _const_class(self):
        return LazyConstant

    def get_value(self):
        if ctrl.current_listener is self:
            raise RuntimeError("@compute rules cannot use their own value")
        return super(LazyCell, self).get_value()

    value = property(get_value)


class LazyConstant(ConstantRule, LazyCell):
    """LazyCell version of a constant"""
    __slots__ = ()






class Cell(ReadOnlyCell, Value):
    """Spreadsheet-like cell with automatic updating"""

    __slots__ = ()

    def __new__(cls, rule=None, value=_sentinel, discrete=False):
        v = [value,None][value is _sentinel]
        if cls is Cell:
            if isinstance(rule, AbstractConnector) and cls is Cell:
                if value is _sentinel:
                    return Sensor(rule, v, discrete)
                return Effector(rule, value, discrete)
            elif value is _sentinel and rule is not None:
                return ReadOnlyCell(rule, None, discrete)
            elif rule is None:
                return Value(v, discrete)
        return ReadOnlyCell.__new__(cls, rule, value, discrete)

    def _check_const(self):
        pass    # we can never become Constant

    def get_value(self):
        if self._needs_init:
            if not ctrl.active:
                atomically(schedule, self)  # initialization must be atomic
                return self._value
            if self._set_by is _sentinel:
                # No value set yet, so we have to run() first
                cancel(self); initialize(self)
        if ctrl.current_listener is not None:
            used(self)
        return self._value

    def set_value(self, value):
        if not ctrl.active:
            return atomically(self.set_value, value)
        super(Cell, self).set_value(value)
        if self._needs_init:
            schedule(self)
        else: cancel(self)

    value = property(get_value, set_value)

    def dirty(self):
        # If we've been manually set, don't reschedule
        who = self._set_by
        return who is _sentinel or who is self

    def run(self):
        if self.dirty():
            # Nominal case: the value hasn't been set in this txn, or was only
            # set by the rule itself.
            super(Cell, self).run()
        elif self._needs_init:
            # Initialization case: value was set before reading, so we ignore
            # the return value of the rule and leave our current value as-is,
            # but of course now we will notice any future changes
            change_attr(self, '_needs_init', False)
            self.rule()
        else:
            # It should be impossible to get here unless you run the cell
            # manually.  Don't do that.  :)
            raise AssertionError("This should never happen!")


class Effector(SensorBase, Cell):
    """Writable Sensor"""

    __slots__ = 'connector', 'listening'













class _Defaulting(addons.Registry):
    def __init__(self, subject):
        self.defaults = {}
        return super(_Defaulting, self).__init__(subject)

    def created_for(self, cls):
        for k,v in self.defaults.items():
            self.setdefault(k, v)
        return super(_Defaulting, self).created_for(cls)

class CellFactories(_Defaulting):
    """Registry for cell factories"""

class IsOptional(_Defaulting):
    """Registry for flagging that an attribute need not be activated"""

class Cells(addons.AddOn):
    __slots__ = ()
    addon_key = classmethod(lambda cls: '__cells__')
    def __new__(cls, subject): return {}





















class Component(decorators.classy):
    """Base class for objects with Cell attributes"""
    __slots__ = ()

    decorators.decorate(classmethod, modifier)
    def __class_call__(cls, *args, **kw):
        if ctrl.readonly and ctrl.newcells is None:
            return ctrl.with_new(Component.__class_call__.im_func,cls,*args,**kw)
        optional = IsOptional(cls)  # ensure initialization beforehand
        rv = super(Component, cls).__class_call__(*args, **kw)
        if isinstance(rv, cls):
            cells = Cells(rv)
            for k, v in optional.iteritems():
                if not v and k not in cells:
                    c = cells.setdefault(k, CellFactories(cls)[k](cls, rv, k))
                    c.value     # XXX
        return rv

    __init__ = init_attrs

    decorators.decorate(staticmethod)
    def __sa_instrumentation_manager__(cls):
        from peak.events.sa_support import SAInstrument
        return SAInstrument(cls)

    def __class_init__(cls, name, bases, cdict, supr):
        supr()(cls, name, bases, cdict, supr)
        try: Component
        except NameError: return
        optional = IsOptional(cls)
        factories = CellFactories(cls)
        for k,descr in cdict.items():
            if isinstance(descr, CellAttribute):
                if descr.__name__ is None: descr.__name__ = k
                optional[k] = descr.optional
                factories[k] = descr.make_cell
            elif k in optional:
                # Don't create a cell for overridden non-CellProperty attribute
                optional[k] = True


def repeat():
    """Schedule the current rule to be run again, repeatedly"""
    if ctrl.current_listener is not None:
        on_commit(schedule, ctrl.current_listener)
    else:
        raise RuntimeError("repeat() must be called from a rule")

def poll():
    """Recalculate this rule the next time *any* other cell is set"""
    listener = ctrl.current_listener
    if listener is None or not hasattr(listener, '_needs_init'):
        raise RuntimeError("poll() must be called from a rule")
    else:
        return ctrl.pulse.value

def mark_dirty():
    """Force the current rule's return value to be treated as if it changed"""
    assert ctrl.current_listener is not None, "mark_dirty() must be called from a rule"
    changed(ctrl.current_listener)


def bind(rule, ob, typ=None):
    if hasattr(rule, '__get__'):
        return rule.__get__(ob, typ)
    return rule
    















class CellAttribute(object):
    """Self-contained cell descriptor"""

    value = NO_VALUE
    rule = connect = disconnect = None
    make = None
    factory = Cell
    discrete = False
    optional = False
    __name__ = None
    __init__ = init_attrs

    def initial_value(self, ob):
        if self.value is NO_VALUE:
            if self.make is not None:
                return build_value(self.make, ob, self.__name__)
            elif self.rule is not None:
                return None
        return self.value

    def make_cell(self, typ, ob, name):
        rule = bind(self.rule, ob, typ)
        if self.connect is not None or self.disconnect is not None:
            connect = bind(self.connect, ob, typ)
            disconnect = bind(self.disconnect, ob, typ)
            missing = 'disconnect'
            if connect is None:
                missing='connect'
            if connect is None or disconnect is None:
                raise TypeError("%r is missing a .%sor" % (self,missing))
            rule = Connector(connect, disconnect, rule)
        return self.factory(rule, self.initial_value(ob), self.discrete)

    def connector(self, func=None):
        """Decorate a method as providing a connect function for this cell"""
        return self._hook_method('connect', func)

    def disconnector(self, func=None):
        """Decorate a method as providing a disconnect function for this cell"""
        return self._hook_method('disconnect', func)

    def __get__(self, ob, typ=None):
        if ob is None:
            return self
        try:
            cells = ob.__cells__
        except AttributeError:
            cells = Cells(ob)
        try:
            cell = cells[self.__name__]
        except KeyError:
            name = self.__name__
            cell = cells.setdefault(name, self.make_cell(typ, ob, name))
        return cell.value

    def __repr__(self):
        return "%s(%r)" % (self.__class__.__name__, self.__name__)

    def __set__(self, ob, value):
        try:
            cells = ob.__cells__
        except AttributeError:
            cells = Cells(ob)

        if isinstance(value, AbstractCell):
            name = self.__name__
            if name in cells and isinstance(cells[name], ConstantMixin):
                raise AttributeError("Can't change a constant")
            cells[name] = value
        else:
            try:
                cell = cells[self.__name__]
            except KeyError:
                name = self.__name__
                typ = type(ob)
                cell = self.make_cell(typ, ob, name)
                if not hasattr(cell, 'set_value'):
                    return cells.setdefault(name, Constant(value))
                cell = cells.setdefault(name, cell)
            cell.value = value


    decorators.decorate(classmethod)
    def mkattr(cls, initially=NO_VALUE, resetting_to=NO_VALUE, **kw):
        if initially is not NO_VALUE and resetting_to is not NO_VALUE:
            raise TypeError("Can't specify both 'initially' and 'resetting_to'")
        value = initially
        discrete = False
        if resetting_to is not NO_VALUE:
            value = resetting_to
            discrete = True
        if value is not NO_VALUE and kw.get('make') is not None:
            raise TypeError("Can't specify both a value and 'make'")

        return cls(value=value, discrete=discrete, **kw)


    def can_connect(self):
        """Can this attribute have ``.connect`` and ``.disconnect`` methods?

        By default, only @compute and @maintain rules can.  Subclasses should
        override this if they override ``factory()`` and still want to support
        connect/disconnect methods.
        """
        if self.factory is LazyCell:
            self.factory = Sensor   # XXX kludge!
        else:
            return self.factory is Cell or self.factory is Sensor
        return True














    def _hook_method(self, methodname, func=None, frame=None):
        if not self.can_connect():
            raise TypeError("%r cannot have a .%sor" % (self, methodname))

        if isinstance(self.rule, AbstractConnector):
            raise TypeError("The rule for %r is itself a Connector" % (self,))

        if getattr(self, methodname) is not None:
            raise TypeError("%r already has a .%sor" % (self, methodname))

        if func is not None:
            setattr(self, methodname, func)
            if getattr(func, '__name__', None)==self.__name__:
                frame = frame or sys._getframe(2)
                if frame.f_locals.get(self.__name__) is self:
                    return self                
            return func

        frame = frame or sys._getframe(2)
        def callback(frame, name, func, locals):
            setattr(self, methodname, func)
            if name==self.__name__ and locals.get(name) is self:
                return self
            return func

        return decorators.decorate_assignment(callback, frame=frame)
        














def attr(initially=NO_VALUE, resetting_to=NO_VALUE):
    return CellAttribute.mkattr(initially, resetting_to)

def compute(rule=None, resetting_to=NO_VALUE):
    return _build_descriptor(
        rule=rule, resetting_to=resetting_to, factory=LazyCell, optional=True
    )

def maintain(rule=None, make=None, initially=NO_VALUE, resetting_to=NO_VALUE, optional=False):
    return _build_descriptor(
        rule=rule, initially=initially, resetting_to=resetting_to, make=make,
        optional=optional
    )

def perform(rule=None, optional=False):
    return _build_descriptor(
        rule=rule, factory=Performer.from_attr, optional=optional
    )

def compute_attrs(**attrs):
    """Define multiple rule-cell attributes"""
    _make_multi(sys._getframe(1), attrs, factory=LazyCell, optional=True)
compute.attrs = compute_attrs

def maintain_attrs(**attrs):
    """Define multiple rule-cell attributes"""
    _make_multi(sys._getframe(1), attrs)
maintain.attrs = maintain_attrs

def attrs(**attrs):
    """Define multiple value-cell attributes"""
    _make_multi(sys._getframe(1), attrs, arg='initially')


def attrs_resetting_to(**attrs):
    """Define multiple receiver-cell attributes"""
    _make_multi(sys._getframe(1), attrs, arg='resetting_to')
attrs.resetting_to = attrs_resetting_to



def _make_multi(frame, kw, wrap=CellAttribute.mkattr, arg='rule', **opts):
    for k, v in kw.items():
        if k in frame.f_locals:
            raise TypeError("%s is already defined in this class" % (k,))
        opts[arg] = named_lambda(v, k)
        frame.f_locals[k]= wrap(__name__=k, **opts)

def _build_descriptor(
    rule=None, __frame=None, __name=None, __proptype = CellAttribute.mkattr,
    __ruleattr='rule', **kw
):
    frame = __frame or sys._getframe(2)
    name  = __name
    if isinstance(rule, types.FunctionType): # only pick up name if a function
        if frame.f_locals.get(rule.__name__) is rule:   # and locally-defined!
            name = name or rule.__name__

    def callback(frame, name, rule, locals):
        kw[__ruleattr] = named_lambda(rule, name)
        return __proptype(__name__=name, **kw)

    if name:
        # Have everything we need, just go for it
        return callback(frame, name, rule, None)
    elif rule is not None:
        # We know everything but the name, so return the rule as-is and trap
        # the assignment...
        decorators.decorate_assignment(callback, frame=frame)
        return rule
    else:
        # We're being used as a decorator, so just decorate.
        return decorators.decorate_assignment(callback, frame=frame)

def todos(**attrs):
    """Define multiple todo-cell attributes"""
    _make_multi(sys._getframe(1), attrs, TodoProperty.mkattr)

def todo(rule=None):
    """Define an attribute that can send "messages to the future" """
    return _build_descriptor(rule=rule, __proptype = TodoProperty.mkattr)

class TodoProperty(CellAttribute):
    """Property representing a ``todo`` attribute"""

    decorators.decorate(property)
    def future(self):
        """Get a read-only property for the "future" of this attribute"""
        name = self.__name__
        def get(ob):
            try: cells = ob.__cells__
            except AttributeError: cells = Cells(ob)
            try:
                cell = cells[name]
            except KeyError:
                typ = type(ob)
                cell = cells.setdefault(name, self.make_cell(typ, ob, name))
            return cell.get_future()
        return property(get, doc="The future value of the %r attribute" % name)

    def factory(self, rule, value, discrete):
        return TodoValue(rule)

def build_value(make, ob, name):
    if hasattr(make, '__get__'):
        make = make.__get__(ob, type(ob))
    return make()

def make(rule=None, writable=False, optional=True):
    """Create a Constant or Value, initialized from `rule()`"""
    return _build_descriptor(
        rule, optional=optional, __ruleattr='make',
        factory=[Constant.from_attr, Cell][bool(writable)]
    )

def make_attrs(**attrs):
    """Define multiple make-constant attributes"""
    _make_multi(
        sys._getframe(1), attrs,
        factory=Constant.from_attr, arg='make', optional=True
    )
make.attrs = make_attrs

class TodoValue(Value):
    """Value that logs changes for mutable data structures"""

    __slots__ = 'rule', '_copy', '_last_reader'

    def __new__(cls, rule, copy=copy.copy):
        return Value.__new__(cls)

    def __init__(self, rule, copy=copy.copy):
        self.rule = rule
        self._copy = copy
        self._last_reader = NOT_GIVEN
        Value.__init__(self, rule(), True)

    def get_future(self):
        """Get the 'future' value"""
        if not ctrl.active:
            raise RuntimeError("future can only be accessed from a @modifier")
        lock(self)
        if ctrl.current_listener is not self._last_reader:
            if self._last_reader is NOT_GIVEN:
                self.value = value = self.rule()
                changed(self)
            else:
                value = self._copy(self._value)
                change_attr(self, '_value', value)
            change_attr(self, '_last_reader', ctrl.current_listener)
        return self._value

    def _finish(self):
        change_attr(self, '_last_reader', NOT_GIVEN)
        super(TodoValue, self)._finish()









class Pipe(Component):
    """Allow one or more writers to send data to zero or more readers"""

    output = todo(list)
    input  = output.future

    decorators.decorate(modifier)
    def append(self, value):
        self.input.append(value)

    decorators.decorate(modifier)
    def extend(self, sequence):
        self.input.extend(sequence)

    def __iter__(self):
        return iter(self.output)

    def __contains__(self, value):
        return value in self.output

    def __len__(self):
        return len(self.output)

    def __repr__(self):
        return repr(self.output)
















class WeakDefaultDict(weakref.WeakValueDictionary):

    def __init__(self, missing):
        weakref.WeakValueDictionary.__init__(self)
        self.__missing__ = missing

    def __getitem__(self, key):
        try:
            return weakref.WeakValueDictionary.__getitem__(self, key)
        except KeyError:
            value = self.__missing__(key)
            self[key] = value
            return value

class CacheAttr(CellAttribute):
    optional = True
    def can_connect(self): return True
    def factory(self, rule, value, discrete):
        #if (isinstance(rule, Connector)
        #    and not isinstance(self.rule, AbstractConnector)
        #):
        conn, disc, rule = rule.connect, rule.disconnect, rule.read
        #else:
        #    conn = disc = None
        def make_cell(key):
            #if not isinstance(rule, AbstractConnector):
            r = new.instancemethod(rule, key, type(key))
            #if conn is None:
            #    return LazyCell(r, value, discrete)
            return Sensor(
                Connector(lambda s: conn(key), lambda s,m: disc(key), r),
                value, discrete
            )
        return Constant(WeakDefaultDict(make_cell))

def cellcache(rule=None, make=None, initially=NO_VALUE, resetting_to=NO_VALUE):
    return _build_descriptor(
        rule=rule, initially=initially, resetting_to=resetting_to, make=make,
        __proptype = CacheAttr.mkattr
    )
    
class Dict(UserDict.IterableUserDict, Component):
    """Dictionary-like object that recalculates observers when it's changed

    The ``added``, ``changed``, and ``deleted`` attributes are dictionaries
    showing the current added/changed/deleted contents.  Note that ``changed``
    may include items that were set as of this recalc, but in fact have the
    same value as they had in the previous recalc, as no value comparisons are
    done!

    You may observe these attributes directly, but any rule that reads the
    dictionary in any way (e.g. gets items, iterates, checks length, etc.)
    will be recalculated if the dictionary is changed in any way.

    Note that this operations like pop(), popitem(), and setdefault() that both
    read and write in the same operation are NOT supported, since reading must
    always happen in the present, whereas writing is done to the future version
    of the dictionary.
    """
    added = todo(dict)
    deleted = todo(dict)
    changed = todo(dict)

    to_add = added.future
    to_change = changed.future
    to_delete = deleted.future

    def __init__(self, other=(), **kw):
        Component.__init__(self)
        if other: self.data.update(other)
        if kw:    self.data.update(kw)

    def copy(self):
        return self.__class__(self.data)

    def get(self, key, failobj=None):
        return self.data.get(key, failobj)

    def __hash__(self):
        raise TypeError


    maintain(make=dict)
    def data(self):
        data = self.data
        if self.deleted or self.changed:
            old = [(k,data[k]) for k in self.deleted if k in data]
            old += [(k,data[k]) for k in self.changed if k in data]
            on_undo(data.update, dict(old))
        pop = data.pop
        if self.deleted:
            mark_dirty()
            for key in self.deleted:
                pop(key, None)
        if self.added:
            for key in self.added: on_undo(pop, key, None)
            mark_dirty(); data.update(self.added)
        if self.changed:
            mark_dirty(); data.update(self.changed)
        return data

    decorators.decorate(modifier)
    def __setitem__(self, key, item):
        if key in self.to_delete:
            del self.to_delete[key]
        if key in self.data:
            self.to_change[key] = item
        else:
            self.to_add[key] = item

    decorators.decorate(modifier)
    def __delitem__(self, key):
        if key in self.to_add:
            del self.to_add[key]
        elif key in self.data and key not in self.to_delete:
            self.to_delete[key] = self.data[key]
            if key in self.to_change:
                del self.to_change[key]
        else:
            raise KeyError, key



    decorators.decorate(modifier)
    def clear(self):
        self.to_add.clear()
        self.to_change.clear()
        self.to_delete.update(self.data)

    decorators.decorate(modifier)
    def update(self, d=(), **kw):
        if d:
            if kw:
                d = dict(d);  d.update(kw)
            elif not hasattr(d, 'iteritems'):
                d = dict(d)
        else:
            d = kw
        to_change = self.to_change
        to_add = self.to_add
        to_delete = self.to_delete
        data = self.data
        for k, v in d.iteritems():
            if k in to_delete:
                del to_delete[k]
            if k in data:
                to_change[k] = d[k]
            else:
                to_add[k] = d[k]

    def setdefault(self, key, failobj=None):
        """setdefault() is disallowed because it 'reads the future'"""
        raise InputConflict("Can't read and write in the same operation")

    def pop(self, key, *args):
        """The pop() method is disallowed because it 'reads the future'"""
        raise InputConflict("Can't read and write in the same operation")

    def popitem(self):
        """The popitem() method is disallowed because it 'reads the future'"""
        raise InputConflict("Can't read and write in the same operation")



class List(UserList.UserList, Component):
    """List-like object that recalculates observers when it's changed

    The ``changed`` attribute is True whenever the list has changed as of the
    current recalculation, and any rule that reads the list in any way (e.g.
    gets items, iterates, checks length, etc.) will be recalculated if the
    list is changed in any way.

    Note that this type is not efficient for large lists, as a copy-on-write
    strategy is used in each recalcultion that changes the list.  If what you
    really want is e.g. a sorted read-only view on a set, don't use this.
    """

    updated = todo(lambda self: self.data[:])
    future  = updated.future
    changed = todo(bool)

    def __init__(self, other=(), **kw):
        Component.__init__(self, **kw)
        self.data[:] = other

    maintain(make=list)
    def data(self):
        if self.changed:
            mark_dirty()
            return self.updated
        return self.data

    decorators.decorate(modifier)
    def __setitem__(self, i, item):
        self.changed = True
        self.future[i] = item

    decorators.decorate(modifier)
    def __delitem__(self, i):
        self.changed = True
        del self.future[i]




    decorators.decorate(modifier)
    def __setslice__(self, i, j, other):
        self.changed = True
        self.future[i:j] = other

    decorators.decorate(modifier)
    def __delslice__(self, i, j):
        self.changed = True
        del self.future[i:j]

    decorators.decorate(modifier)
    def __iadd__(self, other):
        self.changed = True
        self.future.extend(other)
        return self

    decorators.decorate(modifier)
    def append(self, item):
        self.changed = True
        self.future.append(item)

    decorators.decorate(modifier)
    def insert(self, i, item):
        self.changed = True
        self.future.insert(i, item)

    decorators.decorate(modifier)
    def extend(self, other):
        self.changed = True
        self.future.extend(other)

    decorators.decorate(modifier)
    def __imul__(self, n):
        self.changed = True
        self.future[:] = self.future * n
        return self





    decorators.decorate(modifier)
    def remove(self, item):
        self.changed = True
        self.future.remove(item)

    decorators.decorate(modifier)
    def reverse(self):
        self.changed = True
        self.future.reverse()

    decorators.decorate(modifier)
    def sort(self, *args, **kw):
        self.changed = True
        self.future.sort(*args, **kw)

    def pop(self, i=-1):
        """The pop() method isn't supported, because it 'reads the future'"""
        raise InputConflict(
            "Can't read and write in the same operation"
        )

    def __hash__(self):
        raise TypeError


















class Set(sets.Set, Component):
    """Mutable set that recalculates observers when it's changed

    The ``added`` and ``removed`` attributes can be watched for changes, but
    any rule that simply uses the set (e.g. iterates over it, checks for
    membership or size, etc.) will be recalculated if the set is changed.
    """
    _added = todo(set)
    _removed = todo(set)
    added, removed = _added, _removed
    to_add = _added.future
    to_remove = _removed.future

    def __init__(self, iterable=None, **kw):
        """Construct a set from an optional iterable."""
        Component.__init__(self, **kw)
        if iterable is not None:
            # we can update self._data in place, since no-one has seen it yet
            sets.Set._update(self, iterable)

    maintain(make=dict)
    def _data(self):
        """The dictionary containing the set data."""
        data = self._data
        pop = data.pop
        if self.removed:
            mark_dirty()
            for item in self.removed: pop(item, None)
            on_undo(data.update, dict.fromkeys(self.removed, True))
        if self.added:
            mark_dirty()
            data.update(dict.fromkeys(self.added, True))
            for item in self.added: on_undo(pop, item, None)
        return data

    def __setstate__(self, data):
        self.__init__(data[0])




    def _binary_sanity_check(self, other):
        # Check that the other argument to a binary operation is also
        # a set, raising a TypeError otherwise.
        if not isinstance(other, set_like):
            raise TypeError, "Binary operation only permitted between sets"

    def pop(self):
        """The pop() method isn't supported, because it 'reads the future'"""
        raise InputConflict(
            "Can't read and write in the same operation"
        )

    decorators.decorate(modifier)
    def _update(self, iterable):
        to_remove = self.to_remove
        add = self.to_add.add
        for item in iterable:
            if item in to_remove:
                to_remove.remove(item)
            else:
                add(item)

    decorators.decorate(modifier)
    def add(self, item):
        """Add an element to a set (no-op if already present)"""
        if item in self.to_remove:
            self.to_remove.remove(item)
        elif item not in self._data:
            self.to_add.add(item)

    decorators.decorate(modifier)
    def remove(self, item):
        """Remove an element from a set (KeyError if not present)"""
        if item in self.to_add:
            self.to_add.remove(item)
        elif item in self._data and item not in self.to_remove:
            self.to_remove.add(item)
        else:
            raise KeyError(item)


    decorators.decorate(modifier)
    def clear(self):
        """Remove all elements from this set."""
        self.to_remove.update(self)
        self.to_add.clear()

    def __ior__(self, other):
        """Update a set with the union of itself and another."""
        self._binary_sanity_check(other)
        self._update(other)
        return self

    def __iand__(self, other):
        """Update a set with the intersection of itself and another."""
        self._binary_sanity_check(other)
        self.intersection_update(other)
        return self

    decorators.decorate(modifier)
    def difference_update(self, other):
        """Remove all elements of another set from this set."""
        data = self._data
        to_add, to_remove = self.to_add, self.to_remove
        for item in other:
            if item in to_add: to_add.remove(item)
            elif item in data: to_remove.add(item)

    decorators.decorate(modifier)
    def intersection_update(self, other):
        """Update a set with the intersection of itself and another."""
        to_remove = self.to_remove
        to_add = self.to_add
        self.to_add.intersection_update(other)
        other = to_dict_or_set(other)
        for item in self._data:
            if item not in other:
                to_remove.add(item)
        return self



    decorators.decorate(modifier)
    def symmetric_difference_update(self, other):
        """Update a set with the symmetric difference of itself and another."""
        data = self._data
        to_add = self.to_add
        to_remove = self.to_remove
        for elt in to_dict_or_set(other):
            if elt in to_add:
                to_add.remove(elt)      # Got it; get rid of it
            elif elt in to_remove:
                to_remove.remove(elt)   # Don't got it; add it
            elif elt in data:
                to_remove.add(elt)      # Got it; get rid of it
            else:
                to_add.add(elt)         # Don't got it; add it

def to_dict_or_set(ob):
    """Return the most basic set or dict-like object for ob
    If ob is a sets.BaseSet, return its ._data; if it's something we can tell
    is dictlike, return it as-is.  Otherwise, make a dict using .fromkeys()
    """
    if isinstance(ob, sets.BaseSet):
        return ob._data
    elif not isinstance(ob, dictlike):
        return dict.fromkeys(ob)
    return ob
















cvs-admin@eby-sarna.com

Powered by ViewCVS 1.0-dev

ViewCVS and CVS Help