Skip to content

Latest commit

 

History

History
executable file
·
1716 lines (1302 loc) · 36.9 KB

Internals.txt

File metadata and controls

executable file
·
1716 lines (1302 loc) · 36.9 KB

Cell Attributes

Cell attributes are descriptors used to implement cell-based attributes. They are created using a name:

>>> from peak.events import trellis, activity
>>> from peak.events.trellis import Cell, CellAttribute

>>> CellAttribute(__name__='C')
CellAttribute('C')

When used as descriptors in a class, they read or write the .value attribute of the corresponding cell in the object's __cells__ dictionary:

>>> class Converter(object):
...     C = CellAttribute(__name__='C')
...     F = CellAttribute(__name__='F')
...     def __init__(self):
...         self.__cells__ = dict(
...             F = Cell(lambda: self.C * 1.8 + 32, 32),
...             C = Cell(lambda: (self.F - 32)/1.8, 0),
...         )

>>> Converter.C
CellAttribute('C')

>>> tc = Converter()
>>> tc.C
0.0
>>> tc.F
32.0

>>> tc.C = 100
>>> tc.F
212.0

Setting a CellAttribute-mediated attribute to a Cell instance, replaces that instance in the __cells__ dictionary:

>>> tc.F = Cell(lambda: -tc.C)
>>> tc.F
-100

Getting or setting a cell attribute that has no cell in the object's __cells__ invokes its make_cell() method to create a cell:

>>> def demo_factory(typ, ob, name):
...     print "Creating", name, "cell for", typ, "instance"
...     return Cell(lambda: ob.C*2, 0)

>>> Converter.F.make_cell = demo_factory
>>> del tc.__cells__['F']
>>> tc.F
Creating F cell for <class 'Converter'> instance
200
>>> tc.F = 42

>>> del tc.__cells__['F']
>>> tc.F = 27
Creating F cell for <class 'Converter'> instance
>>> tc.F
27

>>> tc.F = Cell(lambda: -tc.C)
>>> tc.F
-100

And cell attributes are of course inherited by subclasses:

>>> class Converter2(Converter):
...     pass
>>> tc2 = Converter2()
>>> del tc2.__cells__['F']
>>> tc2.F = -40
Creating F cell for <class 'Converter2'> instance
>>> tc2.F
-40

TodoProperty

TodoProperty objects are like CellAttribute, but offer an extra future attribute:

>>> from peak.events.trellis import TodoProperty

>>> class X(object):
...     x = TodoProperty(__name__='x')
...     future_x = x.future
>>> help(X)
Help on class X in module __builtin__:
<BLANKLINE>
class X(object)
 |  ...
 |  future_x
 |      The future value of the 'x' attribute...
 |  x...
 |      Property representing a ``todo`` attribute
<BLANKLINE>

Class Metadata

>>> from peak.events.trellis import IsOptional, NO_VALUE, CellFactories
>>> from peak.events.trellis import *
>>> class Test(trellis.Component):
...     x = compute(lambda self: 27)
...     attrs(y = 42)
>>> Test.x.rule
<function ... at ...>
>>> print Test.y.rule
None
>>> Test.y.value
42
>>> Test.x.value
NO_VALUE
>>> CellFactories(Test)
{'y': <bound method CellAttribute.make_cell of CellAttribute('y')>,
 'x': <bound method CellAttribute.make_cell of CellAttribute('x')>}
>>> Test.x
CellAttribute('x')
>>> Test.y
CellAttribute('y')
>>> Test.y.make_cell(Test, Test(), 'y')
Value(42)
>>> t = Test()
>>> t.__cells__ = {}
>>> t.x
27
>>> t.__cells__['x']
Constant(27)

IsOptional defaults to false for any attribute that has an explicit setting defined in a given class in any other registry besides themselves:

>>> IsOptional(Test)
{'y': False, 'x': True}

>>> Test.x.discrete
False

>>> Test.y.discrete
False

The default factory handles sentinel values by not passing them to the Cell constructor:

>>> Test.x.rule = None
>>> Test.x.value = NO_VALUE
>>> Test.x.factory = Cell
>>> Test.x.make_cell(Test, Test(), 'x')
Value(None)

And it binds non-None rules to the instance:

>>> Test.x.rule = lambda self: 42
>>> Test.x.initial_value(Test())
>>> Test.x.make_cell(Test, Test(), 'x')
Cell(<bound method Test.<lambda> of <Test object at...>>, None [uninitialized])

And uses the .discrete attribute to determine discreteness:

>>> Test.x.rule = None
>>> Test.x.discrete = True
>>> Test.x.make_cell(Test, Test(), 'x')
Value(None, discrete[None])

TodoProperty only uses the rule to create the default value, and always creates a rule-free receiver, regardless of the value or receiver flag settings:

>>> Test.x = TodoProperty(__name__='x', rule=lambda self: dict(), value=54)
>>> Test.to_x = Test.x.future

>>> Test.x.make_cell(Test, Test(), 'x')
TodoValue(<bound method Test.<lambda> of <Test ...>>, {}, discrete[{}])

>>> CellFactories(Test)['x'] = Test.x.make_cell

future, @modifier, etc.

>>> t = Test()
>>> t.__cells__ = {}
>>> def dump(): print "t.x =", t.x
>>> watcher = Cell(dump)
>>> watcher.value
t.x = {}
>>> t.to_x
Traceback (most recent call last):
  ...
RuntimeError: future can only be accessed from a @modifier
>>> def add(key, val):
...     t.to_x[key] = val
>>> add = modifier(add)
>>> add(1, 2)
t.x = {1: 2}
t.x = {}
>>> def update(**kw):
...     for k, v in kw.items():
...         add(k, v)
>>> update(x=1, y=2)
t.x = {'y': 2}
t.x = {}
t.x = {'x': 1}
t.x = {}
>>> update = modifier(update)
>>> update(x=1, y=2)    # it all happens in one go
t.x = {'y': 2, 'x': 1}
t.x = {}

task_factory

The task_factory creates a TaskCell:

>>> from peak.events.activity import task
>>> def f(self): yield 1; print "done"
>>> t = task(f).make_cell(Test, Test(), 'x')
>>> t
TaskCell(None)
>>> print t.value
None
>>> activity.EventLoop.flush()  # yield 1
>>> activity.EventLoop.flush()  # 'print done'
done

Decorators

>>> from peak.util.decorators import decorate
>>> class Test(trellis.Component):
...     def aRule(self):
...         return 42
...     r = maintain(aRule)   # trick to exercise auto-name-finding
...     anEvent = attr(resetting_to=-1)
...     optRule = compute(lambda:99)
...     todo = todo(lambda self:{})
>>> Test.r
CellAttribute('aRule')
>>> Test.todo
TodoProperty('todo')
>>> print Test.anEvent.rule
None
>>> Test.r.rule
<function aRule at...>
>>> Test.anEvent.value
-1
>>> Test.r.value
NO_VALUE
>>> Test.anEvent.discrete
True
>>> Test.r.discrete
False
>>> CellFactories(Test)
{'anEvent': <bound method CellAttribute.make_cell of CellAttribute('anEvent')>,
 'optRule': <bound method CellAttribute.make_cell of CellAttribute('optRule')>,
 'r': <bound method CellAttribute.make_cell of CellAttribute('aRule')>,
 'todo': <bound method TodoProperty.make_cell of TodoProperty('todo')>}
>>> IsOptional(Test)
{'anEvent': False, 'optRule': True, 'r': False, 'todo': False}
>>> class Test(trellis.Component):
...     todos(
...         added   = lambda self:{},
...         removed = lambda self:set()
...     )
...     to_add = added.future
...     to_remove = removed.future
...     activity.task()
...     def task(self): yield None
>>> CellFactories(Test)
{'removed': <bound method TodoProperty.make_cell of TodoProperty('removed')>,
 'task': <bound method CellAttribute.make_cell of CellAttribute('task')>,
 'added': <bound method TodoProperty.make_cell of TodoProperty('added')>}
>>> class Test(trellis.Component):
...     trellis.compute.attrs(
...         x = lambda self: self.y
...     )
...     y = trellis.make(dict, writable=True)
>>> x=Test()
>>> x.x
{}
>>> trellis.Cells(x)
{'y': Value({}),
 'x': LazyCell(<bound method Test.x of <Test object...>>, {} [inactive])}
>>> class Test(trellis.Component):
...     trellis.compute.attrs(
...         x = lambda self: self.y
...     )
...     trellis.make.attrs(y = dict)
>>> x=Test()
>>> x.x
{}
>>> trellis.Cells(x)
{'y': Constant({}), 'x': Constant({})}

Components

>>> def hello(msg):
...     print msg
>>> class Test(Component):
...     trellis.maintain.attrs(
...         X = lambda self: self.Y + 2
...     )
...     trellis.attrs.resetting_to(Y = 0)
...     Z = trellis.attr(0)
...     def always(self):
...         print "always!"
...     always = maintain(always)
...     def only_on_request(self):
...         print "hello!"
...     only_on_request = compute(only_on_request)
...     A=compute(lambda s:hello("A!"))
...     maintain.attrs(B=lambda s:hello("B!"))
>>> Test.B.discrete
False
>>> Test.always.discrete
False
>>> Test.X.discrete
False
>>> Test.Y.discrete
True
>>> Test.Z.discrete
False

Non-optional attributes are activated at creation time, as are the appropriate cells:

>>> t = Test()
B!
always!

>>> t.__cells__.keys()
['Y', 'always', 'Z', 'B', 'X']

>>> t.only_on_request
hello!

>>> t.A
A!
>>> t.A

>>> t.X
2
>>> t.Y = 23
>>> t.X
2
>>> t.Z = 1
>>> t.X
2

>>> def show_X():
...     print "X =", t.X
>>> show_X = Cell(show_X)
>>> show_X.value
X = 2

>>> t.Y = 23
X = 25
X = 2

>>> t.__cells__.keys()
['A', 'B', 'always', 'only_on_request', 'Y', 'X', 'Z']

>>> del show_X

Keyword arguments are accepted by the constructor:

>>> t = Test(always=Constant(False), B=Constant(0), Z=55)
>>> t.Z
55
>>> t.B
0
>>> t.always
False

But not for undefined attributes:

>>> t = Test(qqqq=42)
Traceback (most recent call last):
  ...
TypeError: Test() has no keyword argument 'qqqq'

Creating a component from within a rule should not create a dependency link to the rule:

>>> x = Cell(value=27)
>>> class Test(Component):
...     trellis.maintain.attrs(x = lambda self: x.value)
>>> def _rule():
...     print "creating"
...     Test()
>>> r = Cell(_rule)
>>> r.value
creating
>>> x.value = 99

And initializing a component cell that would ordinarily be read-only, should replace it with a constant:

>>> class Test(Component):
...     trellis.make.attrs(x = lambda self: {})
>>> t = Test(x=())
>>> t.__cells__['x']
Constant(())

>>> t.x = r
Traceback (most recent call last):
  ...
AttributeError: Can't change a constant

A component should not create cells for attributes that are not cell properties (e.g., due to override in a subclass):

>>> class Test(Component):
...     trellis.maintain()
...     def attr(self):
...         print "computing"

>>> Test()
computing
<Test object at ...>

>>> class Test2(Test):
...     attr = None

>>> Test2()     # attr should not be initialized
<Test2 object at ...>

Cell Objects

get_value/set_value methods:

>>> c = Cell()
>>> print c.get_value()
None
>>> c.set_value(42)
>>> c.value
42
>>> c1 = Constant(42)
>>> c1.get_value()
42
>>> c1.set_value(99)
Traceback (most recent call last):
  ...
AttributeError: 'Constant' object has no attribute 'set_value'

>>> c1 = Cell(lambda: c.value)
>>> c1.get_value()
42
>>> c1.set_value(99)
Traceback (most recent call last):
  ...
AttributeError: 'ReadOnlyCell' object has no attribute 'set_value'

>>> c.set_value(99)
>>> c1.get_value()
99

Repeating, Polling, Recalc

>>> poll()
Traceback (most recent call last):
  ...
RuntimeError: poll() must be called from a rule
>>> repeat()
Traceback (most recent call last):
  ...
RuntimeError: repeat() must be called from a rule

"Poll" re-invokes a rule on the next recalc of anything:

>>> c2 = Cell(value=99)
>>> def count():
...     if poll(): print "calculating"
...     return c.value+1

>>> c = Cell(count, 0)

>>> def hello(): print "c =", c.value
>>> c1 = Performer(hello)
c = calculating
1
>>> c.value
1
>>> c.value
1

>>> c2.value = 16
calculating
c = 2

>>> c2.value = 7
calculating
c = 3

>>> c2.value = 66
calculating
c = 4

At least, until/unless you get rid of the cell:

>>> c = Cell(value=99)
>>> c.value
99

Which of course requires that its listeners drop their references first:

>>> c2.value = 27
calculating
c = 99

And now the repeated polling of the now-vanished cell stops:

>>> Cell().value = 66
>>> c.value = 20
c = 20

Discrete Processing

>>> class LineReceiver(trellis.Component):
...     bytes = trellis.attr(resetting_to='')
...     delimiter = trellis.attr('\r\n')
...     _buffer = ''
...
...     trellis.maintain(resetting_to=None)
...     def line(self):
...         buffer = self._buffer = self._buffer + self.bytes
...         lines = buffer.split(self.delimiter, 1)
...         if len(lines)>1:
...             buffer = self._buffer = lines[1]
...             trellis.repeat()
...             return lines[0]
...
...     trellis.maintain()
...     def dump(self):
...         if self.line is not None:
...             print "Line:", self.line
>>> LineReceiver.line.discrete
True
>>> lp = LineReceiver()
>>> lp.bytes = 'xyz'
>>> lp.bytes = '\r'
>>> lp.bytes = '\n'
Line: xyz
>>> lp.bytes = "abcdef\r\nghijkl\r\nmnopq"
Line: abcdef
Line: ghijkl
>>> lp.bytes = "FOObarFOObazFOOspam\n"
>>> lp.delimiter = "FOO"
Line: mnopq
Line: bar
Line: baz
>>> lp.delimiter = "\n"
Line: spam
>>> lp.bytes = 'abc\nabc\n'
Line: abc
Line: abc

Multitasking

>>> def raiser():
...     raise Exception("foo")
...     yield None  # make it a generator
>>> raiser().next()
Traceback (most recent call last):
  ...
Exception: foo
>>> activity.resume()
Traceback (most recent call last):
  ...
RuntimeError: resume() must be called from an @activity.task
>>> def yielder(ob):
...     while not ob.bytes:
...         print "pausing"
...         yield activity.Pause
...     yield ob.bytes
>>> def t():
...     yield 1
...     yield 2
...     while 1:
...         yield yielder(lp);
...         print "Got:", activity.resume()
...         yield activity.Pause     # nothing changes unless we pause
>>> c = activity.TaskCell(t)

Dictionaries

>>> d = Dict({1:2}, a="b")
>>> d
{'a': 'b', 1: 2}
>>> hash(d)
Traceback (most recent call last):
  ...
TypeError
>>> d.pop(1)
Traceback (most recent call last):
  ...
InputConflict: Can't read and write in the same operation
>>> d.popitem()
Traceback (most recent call last):
  ...
InputConflict: Can't read and write in the same operation
>>> d.setdefault(2, 4)
Traceback (most recent call last):
  ...
InputConflict: Can't read and write in the same operation
>>> del d['a']
>>> d
{1: 2}
>>> def dump():
...     for name in 'added', 'changed', 'deleted':
...         if getattr(d,name):
...             print name, '=', getattr(d,name)
>>> dump = Performer(dump)
>>> dump.value
>>> d[2] = 3
added = {2: 3}
>>> del d[1]
deleted = {1: 2}
>>> del d[42]
Traceback (most recent call last):
  ...
KeyError: 42
>>> d[2] = "blue"
changed = {2: 'blue'}
>>> d.clear()
deleted = {2: 'blue'}
>>> d.update({1:2}, blue=2)
added = {'blue': 2, 1: 2}
>>> d.update({3:4})
added = {3: 4}
>>> d.update(blue='shoe')
changed = {'blue': 'shoe'}
>>> def go(): d[99] = 42; del d[99]
>>> modifier(go)()
>>> def go(): d[99] = 42; d[99] = 26
>>> modifier(go)()
added = {99: 26}
>>> def go(): del d[99]; d[99] = 42
>>> modifier(go)()
changed = {99: 42}
>>> def go(): d[99] = 71; del d[99]
>>> modifier(go)()
deleted = {99: 42}
>>> def go(): d[99] = 71; d[1] = 23; d.clear(); d.update({1:3}, a='b')
>>> modifier(go)()
added = {'a': 'b'}
changed = {1: 3}
deleted = {'blue': 'shoe', 3: 4}

Lists

>>> L = List("abc")
>>> L
['a', 'b', 'c']
>>> def dump():
...     if L.changed:
...         print "changed to", L
>>> dump = Performer(dump)
>>> dump.value
>>> hash(L)
Traceback (most recent call last):
  ...
TypeError
>>> L.pop()
Traceback (most recent call last):
  ...
InputConflict: Can't read and write in the same operation
>>> L.pop(0)
Traceback (most recent call last):
  ...
InputConflict: Can't read and write in the same operation
>>> L.append(23)
changed to ['a', 'b', 'c', 23]
>>> L[1:2] = [3]
changed to ['a', 3, 'c', 23]
>>> del L[:3]
changed to [23]
>>> L[0] = 42
changed to [42]
>>> del L[0]
changed to []
>>> L += [1, 2]
changed to [1, 2]
>>> L *= 3
changed to [1, 2, 1, 2, 1, 2]
>>> del L[2:]
changed to [1, 2]
>>> L.reverse()
changed to [2, 1]
>>> L.remove(2)
changed to [1]
>>> L.insert(0, 88)
changed to [88, 1]
>>> L.extend( (423, -99) )
changed to [88, 1, 423, -99]
>>> L.sort()
changed to [-99, 1, 88, 423]

Sets

>>> try:
...     s = set
... except NameError:
...     from sets import Set as set
>>> s = Set('abc')
>>> s
Set(['a', 'c', 'b'])
>>> hash(s)
Traceback (most recent call last):
  ...
TypeError: Can't hash a Set, only an ImmutableSet.
>>> s.pop()
Traceback (most recent call last):
  ...
InputConflict: Can't read and write in the same operation
>>> def dump():
...     for name in 'added', 'removed':
...         if getattr(s, name):
...             print name, '=', list(getattr(s,name))
>>> dump = Performer(dump)
>>> dump.value
>>> s.clear()
removed = ['a', 'c', 'b']
>>> s.add(1)
added = [1]
>>> s.remove(1)
removed = [1]
>>> s.remove(2)
Traceback (most recent call last):
  ...
KeyError: 2
>>> s.symmetric_difference_update((1,2))
added = [1, 2]
>>> s.difference_update((2,3))
removed = [2]
>>> def go(): s.add(3); s.remove(3)
>>> modifier(go)()
>>> def go(): s.remove(1); s.add(1)
>>> modifier(go)()
>>> def go(): s.add(3); s.clear()
>>> modifier(go)()
removed = [1]
>>> def go(): ss=s; ss |= set([1,2]); ss &= set([2,3])
>>> modifier(go)()
added = [2]
>>> def go(): ss=s; s.remove(2); ss |= set([2, 3])
>>> modifier(go)()
added = [3]
>>> def go(): ss=s; ss |= set([4]); ss -= set([4, 2])
>>> modifier(go)()
removed = [2]
>>> s
Set([3])
>>> def go(): ss=s; s.remove(3); s.add(4); ss ^= set([3, 4])
>>> modifier(go)()
>>> s
Set([3])

Cell and Component Initialization

This bit tests whether a rule gets to fire when a cell's value is initialized. When you set a ruled cell's value before computing its value, the "old" value should be the passed-in value, and the "new" value should be the assigned value (overriding the rule):

>>> def aRule():
...     print "running", c.value
...     return 42

>>> c = Cell(aRule, 96)
>>> c.value = 27
running 27
>>> c.value
27

And if the setting occurs in a modifier or action rule, the rule's execution should be deferred until the current calculation/rule is complete, thereby allowing several cells to be simultaneously pre-set and initialized at once:

>>> c = trellis.Cell(aRule, 96)
>>> def go():
...     c.value = 27
...     print "finished setting"
>>> trellis.modifier(go)()
finished setting
running 27
>>> c.value
27

Observers:

>>> v = trellis.Value(26)
>>> def o():
...     print "value is", v.value
>>> o = trellis.Performer(o)
value is 26
>>> v.value = 27
value is 27

>>> o.layer
Max

>>> def fail():
...     v.value = 99

>>> trellis.Performer(fail)
Traceback (most recent call last):
  ...
RuntimeError: Can't change objects during @perform or @compute

Pentagram of Death

The infamous "Pentagram of Death" problem is described as follows:

"""If X is an input cell, and A and B and H depend on it, and C depends on B, and A depends on C, and H depends on A and C, then most algorithms will fail to handle a situation where H is recalculated before C knows it's out of date."""

Let's try it. Note that the order of values in the lambda expressions is intended to force the dependencies to be resolved in an order that ensures H gets told that X has changed before C does, and that C has to find out whether B has changed before it is allowed to be recalculated:

>>> def recalc(name):
...     print "calculating", name

>>> X = trellis.Cell(value=1)
>>> A = trellis.Cell(lambda: recalc("A") or (X.value, C.value))
>>> B = trellis.Cell(lambda: recalc("B") or  X.value)
>>> C = trellis.Cell(lambda: recalc("C") or (B.value, X.value))
>>> H = trellis.Cell(lambda: recalc("H") or (X.value, C.value))

We'll calculate H first, so it will be X's first listener:

>>> H.value
calculating H
calculating C
calculating B
(1, (1, 1))

And then A, so it'll be the last listener:

>>> A.value
calculating A
(1, (1, 1))

At this point, the layers of all the cells are known:

>>> X.layer
0
>>> B.layer
1
>>> C.layer
2
>>> A.layer
3
>>> H.layer
3

X is at layer zero because it doesn't depend on anything. B depends only on X, and C depends on both X and B (so it's a higher layer). A depends on X and C, so it's higher than C, and H is the same.

So now, if we change X, everyone should update in the correct order:

>>> X.value = 2
calculating B
calculating C
calculating A
calculating H

>>> H.value
(2, (2, 2))

If this had been a shoddy algorithm, then H.value would have been (2, (1, 1)) instead, and the update order might have been different. Note by the way that B is calculated before C, because C depends on B as its first dependency. So it has to look "up" the dependency graph to see if B or X have changed before C's rule can be run. Since B is first in C's dependency order, it gets recalculated first.

(Similarly, H gets recalculated before C, because its first dependency is X, so it immediately realizes it needs to recalculate. X also notifies H first that a recalculation might be necessary.)

Demos

Circular calculations:

>>> F = Cell(lambda: C.value*1.8 + 32, 32)
>>> C = Cell(lambda: (F.value-32)/1.8, 0)
>>> F.value
32.0
>>> C.value
0.0
>>> F.value = 212
>>> C.value
100.0
>>> C.value = 0
>>> F.value
32.0
>>> C.value = -40
>>> F.value
-40.0

>>> def temp():
...     if C.value<10:
...         print "Brrrrr!"
>>> temp = Cell(temp)
>>> temp.value
Brrrrr!

>>> F.value = 212
>>> C.value
100.0
>>> F.value = 0
Brrrrr!
>>> C.value = 9
Brrrrr!
>>> F.value = 30
Brrrrr!

>>> del temp

Spreadsheet simulation:

>>> from UserDict import DictMixin, UserDict
>>> import sys
>>> class Spreadsheet(DictMixin, UserDict):
...     def __init__(self, *args, **kw):
...         self.data = {}
...         for arg in args+(kw,): self.update(arg)
...
...     def __getitem__(self, key):
...         return self.data[key][1].value
...
...     def __setitem__(self, key, value):
...         def rule():
...             value = self.data[key][0].value
...             print "computing", value
...             if sys.version>="2.4":
...                 return eval(value, globals(), self)
...             code = compile(value, '<string>', 'eval')
...             d = dict([(k,self[k]) for k in code.co_names if k in self.data])
...             return eval(code, globals(), d)
...         if key in self.data:
...             self.data[key][0].value = value
...         else:
...             self.data[key] = Cell(value=value), Cell(rule, None)

>>> ss = Spreadsheet()
>>> ss['a1'] ='5'
>>> ss['a2']='2*a1'
>>> ss['a3']='2*a2'

>>> ss['a1']
computing 5
5
>>> ss['a2']
computing 2*a1
10

>>> ss['a1'] = '7'
computing 7
computing 2*a1

>>> ss['a1']
7
>>> ss['a2']
14
>>> ss['a3']
computing 2*a2
28

>>> ss['a1'] = '3'
computing 3
computing 2*a1
computing 2*a2

Events:

>>> def last_ping():
...     if ping.value is not None:
...         print "ping", ping.value
...         return ping.value
...     return last_ping.value

>>> last_ping = Cell(last_ping)
>>> ping = Cell(discrete=True)

>>> print last_ping.value
None

>>> ping.value = 1
ping 1
>>> last_ping.value
1

>>> F.value = 27
>>> print ping.value    # value goes away as soon as something changes
None
>>> last_ping.value
1

>>> ping.value = 2
ping 2
>>> last_ping.value
2
>>> ping.value = 2      # deps are recalculated even if value is same
ping 2

>>> F.value = 99
>>> print ping.value
None
>>> last_ping.value
2

Dynamic dependencies:

>>> def C_rule():
...     print "computing",
...     if A.value<5:
...         print A.value, B.value
...     else:
...         print "...done"

>>> A = Cell(value=1)
>>> B = Cell(value=2)
>>> C = Cell(C_rule)

>>> C.value
computing 1 2
>>> C.value

>>> A.value = 3
computing 3 2

>>> B.value = 4
computing 3 4

>>> A.value = 5
computing ...done

>>> B.value = 6     # nothing happens, since C no longer depends on B
>>> A.value = 3
computing 3 6

>>> B.value = 7     # but now it's back depending on B again.
computing 3 7

>>> B.value = 7
>>> A.value = 1
computing 1 7

>>> A.value = 1

Forcing a rule to repeat itself:

>>> from peak.events.trellis import repeat, ReadOnlyCell
>>> def counter():
...     if counter.value == 10:
...         return counter.value
...     repeat()
...     return counter.value + 1
>>> counter = ReadOnlyCell(counter, 1)
>>> counter.value
10

Sensors and Connectors

>>> class MyConnector(trellis.AbstractConnector):
...     def connect(self, sensor):
...         print "connecting", sensor
...         return sensor
...     def disconnect(self, sensor, key):
...         print "disconnecting", key
>>> c = trellis.Cell(MyConnector())
>>> c
Sensor(<bound method MyConnector.read of...>, None [uninitialized])
>>> print c.value
None
>>> c2 = trellis.Cell(lambda: c.value)
>>> print c2.value
connecting Sensor(<bound method MyConnector.read of...>, None)
None
>>> c.receive(42)
>>> c.value
42
>>> c2.value
42
>>> del c2
disconnecting Sensor(<bound method MyConnector.read of...>, 42)
>>> c.listening
NOT_GIVEN
>>> class Demo(trellis.Component):
...     a = trellis.compute(lambda self:trellis.noop())
...     a.connector()
...     def _connect_a(self, sensor):
...         print "connecting", sensor
...         v = trellis.Value(42)
...         # this read should not produce a dependency, because
...         # connect/disconnect methods are in an implicit @modifier:
...         v.value
...         return sensor
...     a.disconnector()
...     def _disconnect_a(self, sensor, key):
...         print "disconnecting", key
...     trellis.perform(optional=False)
...     def p(self):
...         poll()
...         print "recalc"
>>> d = Demo()
recalc
>>> print d.a
recalc
None
>>> c = trellis.Cells(d)['a']
>>> c
Sensor(<bound method Demo.a of...>, None)
>>> c2 = trellis.Cell(lambda: c.value)
>>> print c2.value
recalc
connecting Sensor(<bound method Demo.a of...>, None)
recalc
None
>>> c.receive(42)
recalc
>>> c.value
42
>>> c2.value
42
>>> del c2
recalc
disconnecting Sensor(<bound method Demo.a of...>, 42)
recalc
>>> c.listening
NOT_GIVEN
>>> del d.__cells__['p']    # get rid of 'recalc' printing

Effectors

>>> some_global = 42
>>> class GlobalConnector(MyConnector):
...     def read(self):
...         print "reading"
...         return some_global
>>> c = trellis.Cell(GlobalConnector(), None)
>>> c.value
reading
42
>>> def writer():
...     if c.was_set:
...         print "writing", c.value
...         global some_global
...         some_global = c.value
>>> writer = trellis.Performer(writer)
connecting Effector(<bound method GlobalConnector.read of...>, 42)
>>> c2 = trellis.Cell(lambda: c.value)
>>> print c2.value
42
>>> c.value = 99
writing 99
>>> some_global
99
>>> c.value = 99
>>> some_global = 77
>>> c.receive(77)
>>> c2.value
77
>>> del c2, writer
disconnecting Effector(<bound method GlobalConnector.read of...>, 77)
>>> class X(trellis.Component):
...     g = trellis.maintain(GlobalConnector())
>>> x = X()
reading
>>> x.g
77
>>> c2 = trellis.Cell(lambda: x.g)
>>> print c2.value
connecting Effector(<bound method GlobalConnector.read of...>, 77)
77
>>> del c2
disconnecting Effector(<bound method GlobalConnector.read of...>, 77)

Lazy cells

>>> c1=trellis.Value(66)
>>> def cantwrite():
...     c1.value = 23
>>> c2=trellis.LazyCell(cantwrite)
>>> c2.value
Traceback (most recent call last):
  ...
RuntimeError: Can't change objects during @perform or @compute
>>> def calc():
...     return c1.value
>>> c2=trellis.LazyCell(calc)
>>> c2.value
66
>>> list(c2.iter_listeners())
[]
>>> list(c2.iter_subjects())
[]
>>> c3 = trellis.Cell(c2.get_value)
>>> c3.value
66
>>> list(c2.iter_listeners())
[ReadOnlyCell(<...get_value of LazyCell(<function calc...>, 66)>, 66)]
>>> list(c2.iter_subjects())
[Value(66)]
>>> del c3
>>> list(c2.iter_listeners())
[]
>>> list(c2.iter_subjects())
[]
>>> c2=trellis.LazyCell(lambda: {})
>>> c2.value
{}
>>> c2
Constant({})

CellAttribute

>>> x = trellis.CellAttribute(z=22)
Traceback (most recent call last):
  ...
TypeError: CellAttribute() has no keyword argument 'z'
>>> x = trellis.CellAttribute.mkattr(initially=None, resetting_to=None)
Traceback (most recent call last):
  ...
TypeError: Can't specify both 'initially' and 'resetting_to'
>>> x = trellis.CellAttribute.mkattr(initially=None, make=list)
Traceback (most recent call last):
  ...
TypeError: Can't specify both a value and 'make'
>>> x = trellis.CellAttribute.mkattr(resetting_to=None, make=list)
Traceback (most recent call last):
  ...
TypeError: Can't specify both a value and 'make'
>>> trellis.CellAttribute.mkattr(resetting_to=42).initial_value(None)
42
>>> trellis.CellAttribute.mkattr(initially=99).initial_value(None)
99
>>> trellis.CellAttribute.mkattr(make=list).initial_value(None)
[]
>>> trellis.CellAttribute.mkattr(make=list).can_connect()
True
>>> trellis.CellAttribute.mkattr(resetting_to=42).can_connect()
True
>>> trellis.TodoProperty.mkattr(rule=list).can_connect()
False
>>> class Test(trellis.Component):
...     t = trellis.todo(list)
...     t.connector(lambda self: None)
Traceback (most recent call last):
  ...
TypeError: TodoProperty('t') cannot have a .connector
>>> class Test(trellis.Component):
...     t = trellis.todo(list)
...     t.disconnector(lambda self: None)
Traceback (most recent call last):
  ...
TypeError: TodoProperty('t') cannot have a .disconnector
>>> t = trellis.compute(lambda self: 42)
>>> t.connect, t.disconnect
(None, None)
>>> conn = lambda self:23
>>> t.connector(conn) is conn
True
>>> t.connector(conn)
Traceback (most recent call last):
  ...
TypeError: CellAttribute('t') already has a .connector
>>> t.connect is conn
True
>>> t.make_cell(int, 42, 't')
Traceback (most recent call last):
  ...
TypeError: CellAttribute('t') is missing a .disconnector
>>> disc = lambda self:99
>>> t.disconnector(disc) is disc
True
>>> t.disconnector(disc)
Traceback (most recent call last):
  ...
TypeError: CellAttribute('t') already has a .disconnector
>>> t.disconnect is disc
True
>>> t.make_cell(int, 42, 't')
Sensor(<bound method int.t of 42>, None [uninitialized])
>>> t.connect = None
>>> t.make_cell(int, 42, 't')
Traceback (most recent call last):
  ...
TypeError: CellAttribute('t') is missing a .connector
>>> t.rule = AbstractConnector()
>>> t.connect = None
>>> t.connector(conn)
Traceback (most recent call last):
  ...
TypeError: The rule for CellAttribute('t')  is itself a Connector
>>> t.disconnect = None
>>> t.disconnector(disc)
Traceback (most recent call last):
  ...
TypeError: The rule for CellAttribute('t')  is itself a Connector

Connect/disconnect of func w/same name should return the cellattr so as not to overwrite it:

>>> def attr(self): return 42
>>> attr = trellis.compute(attr)
>>> attr.connector(attr.rule) is attr
True
>>> attr.disconnector(attr.rule) is attr
True

>>> class Dummy:
...     trellis.compute()
...     def attr(self):
...         return 42
...
...     _attr = attr
...
...     attr.connector()
...     def attr(self,*args):
...         pass
...
...     print attr is _attr
...
...     attr.disconnector()
...     def attr(self,*args):
...         pass
...
...     print attr is _attr
True
True

But only if has the same name:

>>> foo = trellis.compute(attr.rule)
>>> foo.connector(foo.rule) is foo.rule
True
>>> foo.disconnector(foo.rule) is foo.rule
True

>>> class Dummy:
...     trellis.compute()
...     def foo(self):
...         return 99
...
...     foo.connector()
...     def conn(self,*args):
...         pass
...
...     print conn is foo.connect
...
...     foo.disconnector()
...     def disconn(self,*args):
...         pass
...
...     print disconn is foo.disconnect
True
True

And only if the local variable of the same name is the attribute object:

>>> def attr(self): return 42
>>> _attr = trellis.compute(attr)
>>> _attr.connector(attr) is _attr.connect
True
>>> _attr.disconnector(attr) is _attr.disconnect
True

>>> class Dummy:
...     trellis.compute()
...     def attr(self):
...         return 42
...
...     _attr = attr; del attr
...
...     _attr.connector()
...     def attr(self,*args):
...         pass
...
...     print attr is _attr.connect
...
...     _attr.disconnector()
...     def attr(self,*args):
...         pass
...
...     print attr is _attr.disconnect
True
True

Pipe

>>> p = trellis.Pipe()
>>> def show(): print p
>>> show = trellis.Performer(show)
[]
>>> p.append(1)
[1]
[]
>>> p.extend([2,3])
[2, 3]
[]
>>> def add_many(*args):
...     for arg in args:
...         p.append(arg)
>>> add_many = trellis.modifier(add_many)
>>> add_many(7,8,9)
[7, 8, 9]
[]
>>> def show(): print 42 in p
>>> show = trellis.Performer(show)
False
>>> p.append(27)
False
False
>>> p.append(42)
True
False
>>> def show(): print list(p)
>>> show = trellis.Performer(show)
[]
>>> p.append(99)
[99]
[]
>>> def show(): print len(p)
>>> show = trellis.Performer(show)
0
>>> add_many(3,4,5)
3
0

Cell Caches

>>> class Demo(trellis.Component):
...     cc = trellis.cellcache(lambda self, key: key)
...     cc.connector()
...     def cc_conn(self, key):
...         print "connecting", key
...     cc.disconnector()
...     def cc_disc(self, key):
...         print "disconnecting", key
>>> d = Demo()
>>> c1 = d.cc[1]
>>> c2 = d.cc[2]
>>> c1.value
1
>>> c2.value
2
>>> c = Cell(lambda: c1.value)
>>> c.value
connecting 1
1
>>> c = Cell(lambda: c2.value)
disconnecting 1
>>> c.value
connecting 2
2
>>> del c
disconnecting 2