Repository: zoofIO/flexx
Pull request #197 (2016-09-03 23:52:17)

Improve initial event caching.
Related to #146

Also fixes Bokeh example.


Patch:
diff --git a/flexx/app/funcs.py b/flexx/app/funcs.py
index f3fa01a..f5da3ce 100644
--- a/flexx/app/funcs.py
+++ b/flexx/app/funcs.py
@@ -61,6 +61,11 @@ def create_server(host=None, port=None, new_loop=False, backend='tornado'):
         _current_server.close()
     # Start hosting
     _current_server = TornadoServer(host, port, new_loop)
+    # Schedule pending calls
+    _current_server.call_later(0, _loop.loop.iter)
+    while _pending_call_laters:
+        delay, callback, args, kwargs = _pending_call_laters.pop(0)
+        call_later(delay, callback, *args, **kwargs)
     return _current_server
 
 
@@ -125,6 +130,9 @@ def call_later(delay, callback, *args, **kwargs):
         args: the positional arguments to call the callback with.
         kwargs: the keyword arguments to call the callback with.
     """
+    if not _current_server:
+        _pending_call_laters.append((delay, callback, args, kwargs))
+        return
     server = current_server()
     server.call_later(delay, callback, *args, **kwargs)
 
@@ -132,8 +140,10 @@ def call_later(delay, callback, *args, **kwargs):
 # Work around circular dependency
 model.call_later = call_later
 
+_pending_call_laters = []
+
 # Integrate the "event-loop" of flexx.event
-_loop.loop.integrate(lambda f: current_server().call_later(0, f))
+_loop.loop.integrate(lambda f: call_later(0, f))
 
 
 @manager.connect('connections_changed')
diff --git a/flexx/app/model.py b/flexx/app/model.py
index e588e53..7531de5 100644
--- a/flexx/app/model.py
+++ b/flexx/app/model.py
@@ -532,8 +532,8 @@ class Model(with_metaclass(ModelMeta, event.HasEvents)):
         for type, ev in pending:
             self.emit(type, ev, True)
     
-    def emit(self, type, ev, fromjs=False):
-        ev = super().emit(type, ev)
+    def emit(self, type, info=None, fromjs=False):
+        ev = super().emit(type, info)
         isprop = type in self.__properties__ and type not in self.__local_properties__
         if not fromjs and not isprop and type in self.__event_types_js:
             cmd = 'flexx.instances.%s._emit_from_py(%s, %r);' % (
@@ -613,8 +613,8 @@ class Model(with_metaclass(ModelMeta, event.HasEvents)):
             ev = window.flexx.serializer.loads(text)
             self.emit(type, ev, True)
         
-        def emit(self, type, ev, frompy=False):
-            ev = super().emit(type, ev)
+        def emit(self, type, info=None, frompy=False):
+            ev = super().emit(type, info)
             isprop = (self.__properties__.indexOf(type) >= 0 and
                       self.__local_properties__.indexOf(type) < 0 and
                       self._sync_props)
diff --git a/flexx/event/_handler.py b/flexx/event/_handler.py
index 2d2d5b5..dc6d494 100644
--- a/flexx/event/_handler.py
+++ b/flexx/event/_handler.py
@@ -13,7 +13,7 @@ from . import logger
 def this_is_js():
     return False
 
-console = setTimeout = None
+console = window = None
 
 
 # Decorator to wrap a function in a Handler object
@@ -212,7 +212,8 @@ class Handler:
             # register only once
             self._scheduled_update = True
             if this_is_js():
-                setTimeout(self._handle_now_callback.bind(self), 0)
+                #setTimeout(self._handle_now_callback.bind(self), 0)
+                loop.call_later(self._handle_now_callback.bind(self))
             else:
                 loop.call_later(self._handle_now_callback)
         self._pending.append((label, ev))
@@ -288,7 +289,7 @@ class Handler:
         working, but wont receive events from that object anymore.
         """
         for connection in self._connections:
-            for i in reversed(range(len(connection.objects))):
+            for i in range(len(connection.objects)-1, -1, -1):
                 if connection.objects[i][0] is ob:
                     connection.objects.pop(i)
         
@@ -350,7 +351,7 @@ class Handler:
         # Look inside?
         if selector in '***' and isinstance(ob, (tuple, list)):
             if len(selector) > 1:
-                path.insert(0, obname + '***')  # recurse
+                path = [obname + '***'] + path  # recurse (avoid insert for space)
             for sub_ob in ob:
                 self._seek_event_object(index, path, sub_ob)
             return
diff --git a/flexx/event/_hasevents.py b/flexx/event/_hasevents.py
index b62e7e6..f4a373d 100644
--- a/flexx/event/_hasevents.py
+++ b/flexx/event/_hasevents.py
@@ -8,11 +8,14 @@ import sys
 from ._dict import Dict
 from ._handler import HandlerDescriptor, Handler
 from ._emitters import BaseEmitter, Property
+from ._loop import loop
 from . import logger
 
 def this_is_js():
     return False
 
+setTimeout = None
+
 
 # From six.py
 def with_metaclass(meta, *bases):
@@ -136,7 +139,8 @@ class HasEvents(with_metaclass(HasEventsMeta, object)):
         self.__handlers = {}
         self.__props_being_set = {}
         self.__props_ever_set = {}
-        self.__initialized_handlers = False
+        self.__pending_events = {}
+        
         init_handlers = property_values.pop('_init_handlers', True)
         
         # Instantiate emitters
@@ -155,7 +159,7 @@ class HasEvents(with_metaclass(HasEventsMeta, object)):
                 self._set_prop(name, dd[0], True)
         for name, value in property_values.items():
             if name in self.__properties__:
-                setattr(self, name, value)  # should raises error whith readonly
+                setattr(self, name, value)  # should raise error whith readonly
             else:
                 cname = self.__class__.__name__
                 raise AttributeError('%s does not have a property %r' % (cname, name))
@@ -168,10 +172,14 @@ class HasEvents(with_metaclass(HasEventsMeta, object)):
         """ Initialize handlers and properties. You should only do this once,
         and only when using the object is initialized with init_handlers=False.
         """
-        if self.__initialized_handlers:
-            raise RuntimeError('Cannot initialize handlers twice')
-        self.__initialized_handlers = True
-        self.__init_handlers()  # calls Python or JS version
+        if self.__pending_events is None:
+            return
+        # Schedule a call to disable event capturing
+        def stop_capturing():
+            self.__pending_events = None
+        loop.call_later(stop_capturing)
+        # Call Python or JS version to initialize and connect the handlers
+        self.__init_handlers()
     
     def __init_handlers(self):
         # Instantiate handlers (i.e. resolve connections) its enough to reference them
@@ -217,16 +225,21 @@ class HasEvents(with_metaclass(HasEventsMeta, object)):
             handlers.append(entry)
         handlers.sort(key=lambda x: x[0]+'-'+x[1]._id)
         self._handlers_changed_hook()
-        # Emit initializing event if connection is a property. Old value == cur value.
-        if type in self.__properties__:
-            if self.__props_ever_set.get(type, False):
-                if not label.startswith('reconnect_'):  # Avoid recursion
-                    val = getattr(self, type)
-                    ev = Dict()  # PyScript compatible
-                    ev.type = type
-                    ev.source = self
-                    ev.new_value = ev.old_value = val
-                    handler._add_pending_event(label, ev)  # friend class
+        # Emit any pending events
+        if self.__pending_events is not None:
+            if not label.startswith('reconnect_'):
+                for ev in self.__pending_events.get(type, []):
+                    handler._add_pending_event(type + ":" + label, ev)  # friend class
+        # Send an event to communicate the value of a property
+        # if type in self.__properties__:
+        #     if self.__props_ever_set.get(type, False):
+        #         if not label.startswith('reconnect_'):  # Avoid recursion
+        #             val = getattr(self, type)
+        #             ev = Dict()  # PyScript compatible
+        #             ev.type = type
+        #             ev.source = self
+        #             ev.new_value = ev.old_value = val
+        #             handler._add_pending_event(label, ev)  # friend class
     
     def disconnect(self, type, handler=None):
         """ Disconnect handlers. 
@@ -242,7 +255,7 @@ class HasEvents(with_metaclass(HasEventsMeta, object)):
         # they reconnect (dynamism).
         type, _, label = type.partition(':')
         handlers = self.__handlers.get(type, ())
-        for i in reversed(range(len(handlers))):
+        for i in range(len(handlers)-1, -1, -1):
             entry = handlers[i]
             if not ((label and label != entry[0]) or
                     (handler and handler is not entry[1])):
@@ -270,6 +283,8 @@ class HasEvents(with_metaclass(HasEventsMeta, object)):
         ev.type = type
         ev.source = self
         # Push the event to the handlers (handlers use labels for dynamism)
+        if self.__pending_events is not None:
+            self.__pending_events.setdefault(ev.type, []).append(ev)
         self._emit(ev)
         return ev
     
@@ -333,7 +348,9 @@ class HasEvents(with_metaclass(HasEventsMeta, object)):
         property/emitter or for which any handlers are registered.
         Sorted alphabetically.
         """
-        return list(sorted(self.__handlers.keys()))
+        types = list(self.__handlers)  # avoid using sorted (one less stdlib func)
+        types.sort()
+        return types
     
     def get_event_handlers(self, type):
         """ Get a list of handlers for the given event type. The order
diff --git a/flexx/event/_js.py b/flexx/event/_js.py
index d6fe895..4ed5a8d 100644
--- a/flexx/event/_js.py
+++ b/flexx/event/_js.py
@@ -12,7 +12,7 @@ from flexx.event._handler import HandlerDescriptor, Handler
 from flexx.event._hasevents import HasEvents
 
 
-Object = Date = undefined = None  # fool pyflake
+Object = Date = console = setTimeout = undefined = None  # fool pyflake
 
 reprs = json.dumps
 
@@ -43,6 +43,7 @@ class HasEventsJS:
         self.__handlers = {}
         self.__props_being_set = {}
         self.__props_ever_set = {}
+        self.__pending_events = {}
         
         # Create properties
         for name in self.__properties__:
@@ -156,11 +157,39 @@ class HasEventsJS:
         return handler
 
 
+class Loop:
+    
+    def __init__(self):
+        self._pending_calls = []
+        self._scheduled = False
+    
+    def call_later(self, func):
+        """ Call the given function in the next iteration of the "event loop".
+        """
+        self._pending_calls.append(func)
+        if not self._scheduled:
+            self._scheduled = True
+            setTimeout(self.iter, 0)
+    
+    def iter(self):
+        """ Do one event loop iteration; process all pending function calls.
+        """
+        self._scheduled = False
+        while len(self._pending_calls):
+            func = self._pending_calls.pop(0)
+            try:
+                func()
+            except Exception as err:
+                console.log(err)
+
+
 def get_HasEvents_js():
     """ Get the final code for the JavaScript version of the HasEvents class.
     """
+    # Add the loop
+    jscode = py2js(Loop, 'Loop') + '\nvar loop = new Loop();\n'
     # Start with our special JS version
-    jscode = py2js(HasEventsJS, 'HasEvents')
+    jscode += py2js(HasEventsJS, 'HasEvents')
     # Add the Handler methods
     code = '\n'
     for name, val in sorted(Handler.__dict__.items()):
diff --git a/flexx/event/_loop.py b/flexx/event/_loop.py
index fe3c500..4ab0bdb 100644
--- a/flexx/event/_loop.py
+++ b/flexx/event/_loop.py
@@ -66,6 +66,7 @@ class Loop:
         if call_later_func is not None:
             if callable(call_later_func):
                 self._calllaterfunc = call_later_func
+                self._calllaterfunc(self.iter)
             else:
                 raise ValueError('call_later_func must be a function')
         elif 'tornado' in sys.modules:
@@ -83,6 +84,7 @@ class Loop:
         import tornado.ioloop
         loop = tornado.ioloop.IOLoop.current()
         self._calllaterfunc = loop.add_callback
+        self._calllaterfunc(self.iter)
         logger.debug('Flexx event loop integrated with Tornado')
     
     def integrate_pyqt4(self):  # pragma: no cover
@@ -125,6 +127,7 @@ class Loop:
         
         _callbackEventHandler = _CallbackEventHandler()
         self._calllaterfunc = _callbackEventHandler.postEventWithCallback
+        self._calllaterfunc(self.iter)
 
 
 loop = Loop()
diff --git a/flexx/pyscript/functions.py b/flexx/pyscript/functions.py
index 1f03942..551c7be 100644
--- a/flexx/pyscript/functions.py
+++ b/flexx/pyscript/functions.py
@@ -148,6 +148,8 @@ def evaljs(jscode, whitespace=True):
         cmd = [c.encode('raw_unicode_escape') for c in cmd]
     try:
         res = subprocess.check_output(cmd)
+    except FileNotFoundError as err:
+        raise Exception('%s - The code is probably too big' % str(err))
     except Exception as err:
         err = err.output.decode()
         err = err[:200] + '...' if len(err) > 200 else err


Test patch:
diff --git a/flexx/app/tests/test_funcs.py b/flexx/app/tests/test_funcs.py
index 0dc128b..ff77e01 100644
--- a/flexx/app/tests/test_funcs.py
+++ b/flexx/app/tests/test_funcs.py
@@ -187,7 +187,7 @@ def test_flexx_in_thread2():
         model = MyModel1()
         model.foo = 3
         model.foo = 4
-        # Run mainloop for two iterartions
+        # Run mainloop for one iterartion
         app.call_later(0, app.stop)
         app.start()
 
diff --git a/flexx/app/tests/test_live.py b/flexx/app/tests/test_live.py
index 490afcb..c5040ef 100644
--- a/flexx/app/tests/test_live.py
+++ b/flexx/app/tests/test_live.py
@@ -240,7 +240,6 @@ class ModelE(ModelA):
         
         self.emit('foo', {})
         self.emit('foo', {})
-        self.call_js('test_init()')
     
     @event.connect('foo')
     def foo_handler(self, *events):
@@ -269,7 +268,6 @@ class ModelE(ModelA):
             self.res3 = []
             self.res4 = []
         
-        def test_init(self):
             self.emit('bar', {})
             self.emit('bar', {})
         
diff --git a/flexx/app/tests/test_model.py b/flexx/app/tests/test_model.py
index 54eebfe..6188e36 100644
--- a/flexx/app/tests/test_model.py
+++ b/flexx/app/tests/test_model.py
@@ -233,4 +233,32 @@ def test_active_models():
     assert 'risk on race conditions' in str(handler.last_value)
 
 
+class EmitInInit(app.Model):
+    
+    def init(self):
+        self.res1 = []
+        self.res2 = []
+        self.emit('foo')
+        self.emit('foo')
+    
+    @event.prop
+    def bar(self, v=1):
+        return v
+    
+    @event.connect('foo')
+    def on_foo(self, *events):
+        self.res1.append(len(events))
+    
+    @event.connect('bar')
+    def on_bar(self, *events):
+        self.res2.append(len(events))
+
+def test_can_emit_in_init():
+    m = EmitInInit()
+    m.on_foo.handle_now()
+    m.on_bar.handle_now()
+    
+    assert m.res1 == [2]
+    assert m.res2 == [1]
+
 run_tests_if_main()
diff --git a/flexx/event/tests/test_both.py b/flexx/event/tests/test_both.py
index 0771b05..af346f7 100644
--- a/flexx/event/tests/test_both.py
+++ b/flexx/event/tests/test_both.py
@@ -17,6 +17,7 @@ single quotes.
 from flexx.util.testing import run_tests_if_main, raises
 
 from flexx import event
+from flexx.event import loop
 from flexx.event._js import create_js_hasevents_class, HasEventsJS, reprs
 from flexx.pyscript.functions import py2js, evaljs, evalpy, js_rename
 from flexx.pyscript.stdlib import get_std_info, get_partial_std_lib
@@ -1137,35 +1138,38 @@ class Node(event.HasEvents):
             else:
                 self._r2.append('null')
 
-@run_in_both(Node, "[0, 17, 18, 28, 29]")
+
+@run_in_both(Node, "[17, 18, 29]")
 def test_dynamism1(Node):
     n = Node()
     n1 = Node()
     n2 = Node()
     
+    loop.iter()
+    
     n.parent = n1
     n.val = 42
-    n.handle_parent_val.handle_now()
+    loop.iter()
     n1.val = 17
     n2.val = 27
-    n.handle_parent_val.handle_now()
+    loop.iter()
     n1.val = 18
     n2.val = 28
-    n.handle_parent_val.handle_now()
+    loop.iter()
     n.parent = n2
-    n.handle_parent_val.handle_now()
+    loop.iter()
     n1.val = 19
     n2.val = 29
-    n.handle_parent_val.handle_now()
+    loop.iter()
     n.parent = None
-    n.handle_parent_val.handle_now()
+    loop.iter()
     n1.val = 11
     n2.val = 21
-    n.handle_parent_val.handle_now()
+    loop.iter()
     return n._r1
 
 
-@run_in_both(Node, "[0, 17, 18, 28, 29]")
+@run_in_both(Node, "[17, 18, 29]")
 def test_dynamism2a(Node):
     n = Node()
     n1 = Node()
@@ -1181,6 +1185,8 @@ def test_dynamism2a(Node):
                 res.append(None)
     handler = n.connect(func, 'parent.val')
     
+    loop.iter()
+    
     n.parent = n1
     n.val = 42
     handler.handle_now()
@@ -1203,7 +1209,7 @@ def test_dynamism2a(Node):
     return res
 
 
-@run_in_both(Node, "[null, null, 0, 17, 18, null, 28, 29, null]")
+@run_in_both(Node, "[null, null, 17, 18, null, 29, null]")
 def test_dynamism2b(Node):
     n = Node()
     n1 = Node()
@@ -1219,6 +1225,8 @@ def test_dynamism2b(Node):
                 res.append(None)
     handler = n.connect(func, 'parent', 'parent.val')  # also connect to parent
     
+    loop.iter()
+    
     n.parent = n1
     n.val = 42
     handler.handle_now()
@@ -1241,12 +1249,14 @@ def test_dynamism2b(Node):
     return res
 
 
-@run_in_both(Node, "[0, 0, 17, 27, 18, 28, 28, 29]")
+@run_in_both(Node, "[17, 27, 18, 28, 29]")
 def test_dynamism3(Node):
     n = Node()
     n1 = Node()
     n2 = Node()
     
+    loop.iter()
+    
     n.children = n1, n2
     n.val = 42
     n.handle_children_val.handle_now()
@@ -1269,7 +1279,7 @@ def test_dynamism3(Node):
     return n._r2
 
 
-@run_in_both(Node, "[0, 0, 17, 27, 18, 28, 28, 29]")
+@run_in_both(Node, "[17, 27, 18, 28, 29]")
 def test_dynamism4a(Node):
     n = Node()
     n1 = Node()
@@ -1285,6 +1295,8 @@ def test_dynamism4a(Node):
                 res.append('null')
     handler = n.connect(func, 'children.*.val')
     
+    loop.iter()
+    
     n.children = n1, n2
     n.val = 42
     handler.handle_now()
@@ -1307,7 +1319,7 @@ def test_dynamism4a(Node):
     return res
 
 
-@run_in_both(Node, "['null', 'null', 0, 0, 17, 27, 18, 28, 'null', 28, 29, 'null']")
+@run_in_both(Node, "['null', 'null', 17, 27, 18, 28, 'null', 29, 'null']")
 def test_dynamism4b(Node):
     n = Node()
     n1 = Node()
@@ -1323,6 +1335,8 @@ def test_dynamism4b(Node):
                 res.append('null')
     handler = n.connect(func, 'children', 'children.*.val')  # also connect children
     
+    loop.iter()
+    
     n.children = n1, n2
     n.val = 42
     handler.handle_now()
@@ -1361,6 +1375,8 @@ def test_dynamism5(Node):
                 res.append('null')
     handler = n.connect(func, 'foo.val')
     
+    loop.iter()
+    
     n.val = 42
     handler.handle_now()
     n1.val = 17
diff --git a/flexx/event/tests/test_emitters.py b/flexx/event/tests/test_emitters.py
index 1835fd5..8ec7161 100644
--- a/flexx/event/tests/test_emitters.py
+++ b/flexx/event/tests/test_emitters.py
@@ -148,12 +148,12 @@ def test_prop_init():
     
     # Specifying the value in the init will result in just one event
     m = MyObject(foo=9)
-    assert len(m.foo_handler._pending) == 1
+    assert len(m.foo_handler._pending) == 2
     m.foo = 2
     m.foo = 3
-    assert len(m.foo_handler._pending) == 3
+    assert len(m.foo_handler._pending) == 4
     m.foo = 3
-    assert len(m.foo_handler._pending) == 3
+    assert len(m.foo_handler._pending) == 4
     
 
 def test_readonly():
diff --git a/flexx/event/tests/test_handlers.py b/flexx/event/tests/test_handlers.py
index 14329f2..be17057 100644
--- a/flexx/event/tests/test_handlers.py
+++ b/flexx/event/tests/test_handlers.py
@@ -301,6 +301,20 @@ def test_connecting():
         def foo(*events):
             pass
 
+def test_connecting_and_getting_cached_event():
+    
+    h = event.HasEvents()
+    h.emit('foo')
+    
+    res = []
+    @h.connect('foo')
+    def handle(ev):
+        res.append(ev)
+    
+    event.loop.iter()
+    event.loop.iter()
+    assert len(res) == 1
+
 
 def test_exceptions():
     h = event.HasEvents()
diff --git a/flexx/event/tests/test_hasevents.py b/flexx/event/tests/test_hasevents.py
index 6d0622d..bff8bcc 100644
--- a/flexx/event/tests/test_hasevents.py
+++ b/flexx/event/tests/test_hasevents.py
@@ -263,6 +263,8 @@ def test_disposing_method_handler1():
     foo_ref = weakref.ref(foo)
     foo.dispose()  # <----
     
+    event.loop.iter()
+    
     del foo
     gc.collect()
     assert foo_ref() is None
@@ -284,6 +286,8 @@ def test_disposing_handler2():
     assert foo.get_event_handlers('xx')
     foo_ref = weakref.ref(foo)
     
+    event.loop.iter()
+    
     del foo
     gc.collect()
     assert foo_ref() is None
@@ -302,6 +306,8 @@ def test_disposing_handler3():
     foo_ref = weakref.ref(foo)
     assert foo.get_event_handlers('xx')
     
+    event.loop.iter()
+    
     del foo
     gc.collect()
     assert foo_ref() is not None  # the handler still has refs
@@ -324,6 +330,8 @@ def test_disposing_handler4():
     foo_ref = weakref.ref(foo)
     assert foo.get_event_handlers('xx')
     
+    event.loop.iter()
+    
     foo.emit('xx', {})  # <---------
     
     del foo
@@ -358,10 +366,11 @@ def test_disposing_emitter():
     foo = Foo()
     foo_ref = weakref.ref(foo)
     
+    event.loop.iter()
+    
     del foo
     gc.collect()
     assert foo_ref() is None
 
 
-
 run_tests_if_main()
diff --git a/flexx/event/tests/test_loop.py b/flexx/event/tests/test_loop.py
index fdf9787..f05405e 100644
--- a/flexx/event/tests/test_loop.py
+++ b/flexx/event/tests/test_loop.py
@@ -67,6 +67,8 @@ def test_integrate():
     def calllater(f):
         res.append(f)
     
+    ori = event.loop._calllaterfunc
+    
     foo = Foo()
     event.loop.integrate(calllater)
     foo.emit('foo', {})
@@ -76,8 +78,7 @@ def test_integrate():
     with raises(ValueError):
         event.loop.integrate('not a callable')
     
-    import tornado
-    event.loop.integrate()
+    event.loop._calllaterfunc = ori
 
 
 run_tests_if_main()