Repository: zoofIO/flexx
Pull request #106 (2016-03-22 09:40:25)

Implementation of new event system
This brings a new event system to Flexx, as a replacement for `flexx.react`. This PR only implements the new system. Adopting the rest of Flexx to use it will be done in a new PR.

See issue #60


Patch:
diff --git a/docs/event/api.rst b/docs/event/api.rst
new file mode 100644
index 0000000..45735b8
--- /dev/null
+++ b/docs/event/api.rst
@@ -0,0 +1,41 @@
+flexx.event API
+===============
+
+
+Decorators
+----------
+
+.. autofunction:: flexx.event.connect
+
+.. autofunction:: flexx.event.prop
+
+.. autofunction:: flexx.event.readonly
+
+.. autofunction:: flexx.event.emitter
+
+
+HasEvents
+---------
+
+.. autoclass:: flexx.event.HasEvents
+    :members:
+    :private-members:
+
+Handler
+-------
+
+.. autoclass:: flexx.event.Handler
+    :members:
+
+Dict
+----
+
+
+.. autoclass:: flexx.event.Dict
+    :members:
+
+loop
+----
+
+.. autoclass:: flexx.event._loop.Loop
+    :members:
diff --git a/docs/event/index.rst b/docs/event/index.rst
new file mode 100644
index 0000000..be25d27
--- /dev/null
+++ b/docs/event/index.rst
@@ -0,0 +1,12 @@
+-------------------------
+Reference for flexx.event
+-------------------------
+
+The event module provides functionality for properties and events.
+
+.. toctree::
+   :maxdepth: 2
+   
+   intro
+   api
+   examples
diff --git a/docs/event/intro.rst b/docs/event/intro.rst
new file mode 100644
index 0000000..2449878
--- /dev/null
+++ b/docs/event/intro.rst
@@ -0,0 +1,4 @@
+Intro to the Flexx event system
+===============================
+
+.. automodule:: flexx.event
diff --git a/docs/index.rst b/docs/index.rst
index bce8fa9..63489df 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -20,7 +20,7 @@ also be used by themselves:
 
 * :doc:`ui <ui/index>` - the widgets
 * :doc:`app <app/index>` - the event loop and server
-* :doc:`react <react/index>` - reactive programming (how information flows through your program)
+* :doc:`event <event/index>` - properties and events
 * :doc:`pyscript <pyscript/index>` - Python to JavaScript transpiler
 * :doc:`webruntime <webruntime/index>` - to launch a runtime
 
@@ -51,7 +51,7 @@ Contents
    
    ui/index
    app/index
-   react/index
+   event/index
    pyscript/index
    webruntime/index
    util
diff --git a/docs/releasenotes.rst b/docs/releasenotes.rst
index 4bdf199..4445f25 100644
--- a/docs/releasenotes.rst
+++ b/docs/releasenotes.rst
@@ -13,7 +13,7 @@ Roadmap
 Release notes
 -------------
 
-**v0.3.1** (19-02-2016)
+**v0.3.1** (19-02-2015)
 
 A few small fixes, and improvements to distribution. The universal wheel
 on Pypi for v0.3 did not work on Python 2.7. Flexx now includes
@@ -23,7 +23,7 @@ Also see the
 `overview of pull request <https://github.com/zoofIO/flexx/issues?q=is%3Apr+milestone%3Av0.3.1>`_.
 
 
-**v0.3** (15-02-2016)
+**v0.3** (15-02-2015)
 
 The most important changes with respect to the previous release are:
     
diff --git a/examples/app/benchmark.py b/examples/app/benchmark.py
index 7488a44..9c0de3b 100644
--- a/examples/app/benchmark.py
+++ b/examples/app/benchmark.py
@@ -7,22 +7,16 @@ than CPython and for some tests on par with Pypy.
 
 See also http://brythonista.wordpress.com/2015/03/28
 
+Some quick pystone results on my machine (Win 10, Intel i7-4710 HQ 2.5GHz):
+
+* CPython 3.4:          126396 pystones / second
+* Pypy3:                980334 pystones / second
+* PyScript on Firefox:  443478 pystones / second
+* PyScript on Chrome:   268225 pystones / second
+* PyScript on MS Edge:  347451 pystones / second
+* Brython (debug off):    4751 pystones / second
 """
 
-# Measured results, in pystones/second, measured on 05-03-2016,
-# on Win 10, Intel i7-4710 HQ 2.5GHz
-RESULTS = [(124863, 'CPython 3.4', 'blue'),
-           (137250, 'CPython 3.5', 'blue'),
-           (3927770, 'Pypy4', 'blue'),
-           (3739170, 'Pypy3', 'blue'),
-           (127957, 'PyScript on Firefox', 'orange'),
-           (79517, 'PyScript on Chrome', 'orange'),
-           (128325, 'PyScript on MS Edge', 'orange'),
-           (2982, 'Brython', 'magenta'),
-           (2780, 'Skulpt', 'magenta'),
-           (268817, 'PypyJS', 'magenta'),
-           ]
-
 import sys
 import platform
 from test import pystone
@@ -38,27 +32,10 @@ if sys.argv[1:]:
 pycode = open(pystone.__file__, 'rb').read().decode()
 jscode = pyscript.py2js(pycode, module='pystone')
 
-
-def plot_results():
-    import matplotlib.pyplot as plt
-    plt.ion()
-    
-    data = list(reversed(RESULTS))
-    plt.figure(1)
-    plt.clf()
-    ax = plt.subplot(111)
-    ax.barh([i for i in range(len(data))], [x[0] for x in data],
-            color=[x[2] for x in data])
-    ax.set_yticks([i+0.3 for i in range(len(data))])
-    ax.set_yticklabels([x[1] for x in data])
-    ax.set_xscale('log')
-
-
 def Float32Array(n):
     """ Factory function. """
     return [0.0] * n
 
-
 def convolve():
     import time
     N = 400000
diff --git a/examples/event/basic_emit.py b/examples/event/basic_emit.py
new file mode 100644
index 0000000..08c4897
--- /dev/null
+++ b/examples/event/basic_emit.py
@@ -0,0 +1,26 @@
+"""
+Example demonstrating the emitting of events using the emit() method.
+This also shows how a method prefixed with "on_" is automatically
+connected to the corresponding event.
+"""
+
+from flexx import event
+
+class Basic(event.HasEvents):
+    
+    def on_foo(self, *events):
+        print('foo handler called with %i events' % len(events))
+    
+    def on_bar(self, *events):
+        print('bar handler called with %i events' % len(events))
+
+b = Basic()
+
+# Emit dummy events
+b.emit('foo', {})
+b.emit('foo', {})
+b.emit('bar', {})
+b.emit('spam', {})  # we can emit this, but nobody's listening
+
+# Handle events
+event.loop.iter()
diff --git a/examples/event/caching.py b/examples/event/caching.py
new file mode 100644
index 0000000..5f65541
--- /dev/null
+++ b/examples/event/caching.py
@@ -0,0 +1,46 @@
+"""
+Caching example. The object has a readonly property "data", that gets
+automatically set when the "source" property gets set. The
+download_data handler takes care of updating data when necesary.
+
+This example aims for an analog of "streams" as they are used in
+reactive programming. In RP, the data property would be a signal
+connected to source and would contain the logic that is now in
+download_data,
+"""
+
+import time
+from flexx import event
+
+
+class CachingExample(event.HasEvents):
+    
+    @event.prop
+    def source(self, v=''):
+        """ The input for the calcualations. """
+        return str(v)
+    
+    @event.connect('source')
+    def download_data(self, *events):
+        """ Simulate a download of data from the web. takes a while. """
+        if self.source:
+            time.sleep(2)
+            self._set_prop('data', hash(self.source))
+    
+    @event.readonly
+    def data(self, v=None):
+        """ readonly prop to cache the result. """
+        return v
+    
+    @event.connect('data')
+    def show_data(self, *events):
+        """ handler to show the data. Can be called at any time. """
+        if self.data is not None:
+            print('The data is', self.data)
+
+
+c = CachingExample()
+
+c.source = 'foo'
+
+event.loop.iter()
diff --git a/examples/event/greeter.py b/examples/event/greeter.py
new file mode 100644
index 0000000..451ed83
--- /dev/null
+++ b/examples/event/greeter.py
@@ -0,0 +1,39 @@
+"""
+This example implements a simple class to hold a persons name, and three
+ways to connect a function that will be print a greet when the name is
+changed.
+"""
+
+from flexx import event
+
+class Name(event.HasEvents):
+    
+    @event.prop
+    def first_name(self, n='John'):
+        return str(n)
+    
+    @event.prop
+    def last_name(self, n='Doe'):
+        return str(n)
+    
+    @event.connect('first_name', 'last_name')
+    def greet1(self, *events):
+        print('Hello %s %s' % (self.first_name, self.last_name))
+
+
+name = Name()
+
+# Connect a function using a decorator
+@name.connect('first_name', 'last_name')
+def greet2(*events):
+    print('Hi %s %s' % (name.first_name, name.last_name))
+
+# Connect a function using the classic approach
+def greet3(*events):
+    print('Heya %s %s' % (name.first_name, name.last_name))
+name.connect(greet3, 'first_name', 'last_name')
+
+
+name.first_name = 'Jane'
+
+event.loop.iter()
diff --git a/examples/event/temperature.py b/examples/event/temperature.py
new file mode 100644
index 0000000..4b5c3a4
--- /dev/null
+++ b/examples/event/temperature.py
@@ -0,0 +1,39 @@
+"""
+This example demonstrates how props can set each-other without
+getting into an infinite loop.
+"""
+
+from flexx import event
+
+class Temperature(event.HasEvents):
+    """ Temperature object with a settable prop for both Celcius and
+    Fahrenheit.
+    """
+    
+    @event.prop
+    def C(self, t=0):
+        t = float(t)
+        self.F = t * 1.8 + 32
+        return t
+    
+    @event.prop
+    def F(self, t=0):
+        t = float(t)
+        self.C = (t - 32) / 1.8
+        return t
+    
+    @event.connect('C', 'F')
+    def on_temp_change(self, *events):
+        # This gets called once with two events when either C or F is changed.
+        print('  temp in Celcius: %1.1f C' % self.C)
+        print('  temp in Fahrenheit: %1.1f F' % self.F)
+
+t = Temperature()
+
+print('Water if freezing:')
+t.C = 0
+
+print('Average annual temp in California')
+t.F = 59.4
+
+event.loop.iter()
diff --git a/examples/react/temperature.py b/examples/react/temperature1.py
similarity index 100%
rename from examples/react/temperature.py
rename to examples/react/temperature1.py
diff --git a/flexx/event/__init__.py b/flexx/event/__init__.py
new file mode 100644
index 0000000..87f2ecb
--- /dev/null
+++ b/flexx/event/__init__.py
@@ -0,0 +1,344 @@
+"""
+The event module provides a simple system for properties and events,
+to let different components of an application react to each-other and
+to user input.
+
+In short:
+
+* The :class:`HasEvents <flexx.event.HasEvents>` class provides objects
+  that have properties and can emit events.
+* There are three decorators to create :func:`properties <flexx.event.prop>`,
+  :func:`readonlies <flexx.event.readonly>` and 
+  :func:`emitters <flexx.event.emitter>`.
+* There is a decorator to :func:`connect <flexx.event.connect>` a method
+  to an event.
+
+
+Event
+-----
+
+An event is something that has occurred at a certain moment in time,
+such as the mouse being pressed down or a property changing its value.
+In this framework events are represented with dictionary objects that
+provide information about the event (such as what button was pressed,
+or the new value of a property). A custom :class:`Dict <flexx.event.Dict>`
+class is used that inherits from ``dict`` but allows attribute access,
+e.g. ``ev.button`` as an alternative to ``ev['button']``.
+
+
+The HasEvents class
+-------------------
+
+The :class:`HasEvents <flexx.event.HasEvents>` class provides a base
+class for objects that have properties and/or emit events. E.g. a
+``flexx.ui.Widget`` inherits from ``flexx.app.Model``, which inherits
+from ``flexx.event.HasEvents``.
+
+Events are emitted using the :func:`emit() <flexx.event.HasEvents.emit>`
+method, which accepts a name for the type of the event, and a dict:
+e.g. ``emitter.emit('mouse_down', dict(button=1, x=103, y=211))``.
+
+The HasEvents object will add two attributes to the event: ``source``,
+a reference to the HasEvents object itself, and ``type``, a string
+indicating the type of the event.
+
+As a user, you generally do not need to emit events explicitly; events are
+automatically emitted e.g. when setting a property.
+
+
+Handler
+-------
+
+A handler is an object that can handle events. Handlers can be created
+using the :func:`connect <flexx.event.connect>` decorator:
+
+.. code-block:: python
+    
+    from flexx import event
+    
+    class MyObject(event.HasEvents):
+       
+        @event.connect('foo')
+        def handle_foo(self, *events):
+            print(events)
+    
+    ob = MyObject()
+    ob.emit('foo', dict(value=42))  # will invoke handle_foo()
+
+This example demonstrates a few concepts. Firstly, the handler is
+connected via a *connection-string* that specifies the type of the
+event; in this case the handler is connected to the event-type "foo"
+of the object. This connection-string can also be a path, e.g.
+"sub.subsub.event_type". This allows for some powerful mechanics, as
+discussed in the section on dynamism.
+
+One can also see that the handler function accepts ``*events`` argument.
+This is because handlers can be passed zero or more events. If a handler
+is called manually (e.g. ``ob.handle_foo()``) it will have zero events.
+When called by the event system, it will have at least 1 event. When
+multiple events are emitted in a row, the handler function is called
+just once, with multiple events, in the next event loop iteration. It
+is up to the programmer to determine whether only one action is
+required, or whether all events need processing. In the latter case,
+just use ``for ev in events: ``.
+
+Another feature of this system is that a handler can connect to multiple
+events:
+
+.. code-block:: python
+
+    class MyObject(event.HasEvents):
+       
+        @event.connect('foo', 'bar')
+        def handle_foo_and_bar(self, *events):
+            print(events)
+
+To create a handler from a normal function, use the
+:func:`HasEvents.connect() <flexx.event.HasEvents.connect>` method:
+
+.. code-block:: python
+
+    h = event.HasEvents()
+    
+    # Using a decorator
+    @h.connect('foo', 'bar')
+    def handle_func1(self, *events):
+        print(events)
+    
+    # Explicit notation
+    def handle_func2(self, *events):
+        print(events)
+    h.connect(handle_func2, 'foo', 'bar')
+
+
+Handlers can be implicitly created by prefixing a method with ``on_``:
+
+.. code-block:: python
+
+    class MyObject(event.HasEvents):
+       
+        def on_foo(self, *events):
+            # This gets connected to the "foo" event
+            print(events)
+
+
+Event emitters
+--------------
+
+Apart from using :func:`emit() <flexx.event.HasEvents.emit>` there are
+certain attributes of ``HasEvents`` instances that generate events.
+
+Properties
+==========
+
+Settable properties can be created easiliy using the
+:func:`prop <flexx.event.prop>` decorator:
+
+.. code-block:: python
+
+    class MyObject(event.HasEvents):
+       
+        @event.prop
+        def foo(self, v=0):
+            ''' This is a float indicating bla bla ...
+            '''
+            return float(v)
+
+The function that is decorated should have one argument (the new value
+for the property), and it should have a default value (representing the
+initial value). The function body is used to validate
+and normalize the provided input. In this case the input is simply cast
+to a float. The docstring of the function will be the docstring of the
+property (e.g. for Sphynx docs).
+
+An alternative initial value for a property can be provided upon instantiation:
+
+.. code-block:: python
+
+    m = MyObject(foo=3)
+
+Readonly
+========
+
+Readonly properties are created with the 
+:func:`readonly <flexx.event.readonly>` decorator. The value of a
+readonly property can be set internally using the
+:func:`_set_prop() <flexx.event.HasEvents._set_prop>` method:.
+
+.. code-block:: python
+
+    class MyObject(event.HasEvents):
+       
+        @event.readonly
+        def foo(self, v=0):
+            ''' This is a float indicating bla.
+            '''
+            return float(v)
+        
+        def _somewhere(self):
+            self._set_prop('foo', 42)
+
+Emitter
+=======
+
+Emitter attributes make it easy to generate events, and function as a
+placeholder to document events on a class. They are created with the
+:func:`emitter <flexx.event.emitter>` decorator.
+
+.. code-block:: python
+
+    class MyObject(event.HasEvents):
+    
+        @event.emitter
+        def mouse_down(self, js_event):
+            ''' Event emitted when the mouse is pressed down.
+            '''
+            return dict(button=js_event.button)
+
+Emitters can have any number of arguments and should return a dictionary,
+which will get emitted as an event, with the event type matching the name
+of the emitter.
+
+
+Labels
+------
+
+Labels are a feature that makes it possible to infuence the order by
+which event handlers are called, and provide a means to disconnect
+specific (groups of) handlers. The label is part of the connection
+string: 'foo.bar:label'.
+
+.. code-block:: python
+    
+    class MyObject(event.HasEvents):
+    
+        @event.connect('foo')
+        def given_foo_handler(*events):
+                ...
+        
+        @event.connect('foo:aa')
+        def my_foo_handler(*events):
+            # This one is called first: 'aa' < 'given_f...'
+            ...
+
+When an event is emitted, the event is added to the pending events of
+the handlers in the order of a key, which is the label if present, and
+otherwise the name of the handler. Note that this does not guarantee
+the order in case a handler has multiple connections: a handler can be
+scheduled to handle its events due to another event, and a handler
+always handles all its pending events at once.
+
+The label can also be used in the
+:func:`disconnect() <flexx.event.HasEvents.disconnect>` method:
+
+.. code-block:: python
+
+    @h.connect('foo:mylabel')
+    def handle_foo(*events):
+        ...
+    
+    ...
+    
+    h.disconnect('foo:mylabel')  # don't need reference to handle_foo
+
+
+Dynamism
+--------
+
+Dynamism is a concept that allows one to connect to events for which
+the source can change. For the following example, assume that ``Node``
+is a ``HasEvents`` subclass that has properties ``parent`` and
+``children``.
+
+.. code-block:: python
+    
+    main = Node()
+    main.parent = Node()
+    main.children = Node(), Node()
+    
+    @main.connect('parent.foo')
+    def parent_foo_handler(*events):
+        ...
+    
+    @main.connect('children.*.foo')
+    def children_foo_handler(*events):
+        ...
+
+The ``parent_foo_handler`` gets invoked when the "foo" event gets
+emitted on the parent of main, *and* when the parent of main changes.
+Similarly, the ``children_foo_handler`` gets invoked when any of the
+children emits its "foo" signal, or when the children property changes.
+
+The event system automatically reconnects handlers when necessary. This
+concept makes it very easy to connect to the right events without the
+need for a lot of boilerplate code.
+
+Note that the above example would also work if ``parent`` would be a
+regular attribute instead of a property, but the handler would not be
+automatically reconnected when it changed.
+
+
+Patterns
+--------
+
+This event system is quite flexible and designed to cover the needs
+of a variety of event/messaging mechanisms. This section discusses
+how this system relates to some common patterns, and how these can be
+implemented.
+
+Observer pattern
+================
+
+The idea of the observer pattern is that observers keep track (the state
+of) of an object, and that object is agnostic about what it's tracked by.
+For example, in a music player, instead of writing code to update the
+window-title inside the function that starts a song, there would be a
+concept of a "current song", and the window would listen for changes to
+the current song to update the title when it changes.
+
+In ``flexx.event``, a ``HasEvents`` object keeps track of its observers
+(handlers) and notifies them when there are changes. In our music player
+example, there would be a property "current_song", and a handler to
+take action when it changes.
+
+As is common in the observer pattern, the handlers keep track of the
+handlers that they observe. Therefore both handlers and ``HasEvents``
+objects have a ``dispose()`` method for cleaning up.
+
+Signals and slots
+=================
+
+The Qt GUI toolkit makes use of a mechanism called "signals and slots" as
+an easy way to connect different components of an application. In
+``flexx.event`` signals translate to readonly properties, and slots to
+the handlers that connect to them.
+
+Overloadable event handlers
+===========================
+
+In Qt, the "event system" consists of methods that handles an event, which
+can be overloaded in subclasses to handle an event differently. In
+``flexx.event``, a handler can be implemented simply by naming a method
+``on_xx``. Doing so allows subclasses to re-implement the handler, and
+optionally call the original handler using ``super()``.
+
+Publish-subscribe pattern
+==========================
+
+In pub-sub, publishers generate messages identified by a 'topic', and
+subscribers can subscribe to such topics. There can be zero or more publishers
+and zero or more subscribers to any topic. 
+
+In ``flexx.event`` a `HasEvents` object can play the role of a broker.
+Publishers can simply emit events. The event type represents the message
+topic. Subscribers are represented by handlers.
+
+"""
+
+from ._dict import Dict  # noqa
+from ._loop import loop  # noqa
+from ._handler import Handler, connect  # noqa
+from ._emitters import prop, readonly, emitter  # noqa
+from ._hasevents import HasEvents  # noqa
+
+# from ._hasevents import new_type, with_metaclass
diff --git a/flexx/event/_dict.py b/flexx/event/_dict.py
new file mode 100644
index 0000000..cf9547c
--- /dev/null
+++ b/flexx/event/_dict.py
@@ -0,0 +1,87 @@
+"""
+Implementation of a dict class with attribute access.
+"""
+
+import re
+
+try:  # pragma: no cover
+    from collections import OrderedDict as _dict
+except ImportError:
+    _dict = dict
+
+
+def isidentifier(s):
+    # http://stackoverflow.com/questions/2544972/
+    if not isinstance(s, str):
+        return False
+    return re.match(r'^\w+$', s, re.UNICODE) and re.match(r'^[0-9]', s) is None 
+
+
+class Dict(_dict):
+    """ A dict in which the items can be get/set as attributes.
+    
+    This provides a lean way to represent structured data, and works
+    well in combination with autocompletion. Keys can be anything that
+    are otherwise valid keys, but keys that are not valid identifiers
+    or that are methods of the dict class (e.g. 'items' or 'copy')
+    can only be get/set in the classic way.
+    
+    Example:
+    
+    .. code-block:: python
+    
+        >> d = Dict(foo=3)
+        >> d.foo
+        3
+        >> d['foo'] = 4
+        >> d.foo
+        4
+        >> d.bar = 5
+        >> d.bar
+        5
+        
+    """
+    
+    __reserved_names__ = dir(_dict())  # Also from OrderedDict
+    __pure_names__ = dir(dict())
+    
+    __slots__ = []
+    
+    def __repr__(self):
+        identifier_items = []
+        nonidentifier_items = []
+        for key, val in self.items():
+            if isidentifier(key):
+                identifier_items.append('%s=%r' % (key, val))
+            else:
+                nonidentifier_items.append('(%r, %r)' % (key, val))
+        if nonidentifier_items:
+            return 'Dict([%s], %s)' % (', '.join(nonidentifier_items),
+                                       ', '.join(identifier_items))
+        else:
+            return 'Dict(%s)' % (', '.join(identifier_items))
+    
+    def __getattribute__(self, key):
+        try:
+            return object.__getattribute__(self, key)
+        except AttributeError:
+            if key in self:
+                return self[key]
+            else:
+                raise
+    
+    def __setattr__(self, key, val):
+        if key in Dict.__reserved_names__:
+            # Either let OrderedDict do its work, or disallow
+            if key not in Dict.__pure_names__:
+                return _dict.__setattr__(self, key, val)
+            else:
+                raise AttributeError('Reserved name, this key can only ' +
+                                     'be set via ``d[%r] = X``' % key)
+        else:
+            # if isinstance(val, dict): val = Dict(val) -> no, makes a copy!
+            self[key] = val
+    
+    def __dir__(self):
+        names = [k for k in self.keys() if isidentifier(k)]
+        return Dict.__reserved_names__ + names
diff --git a/flexx/event/_emitters.py b/flexx/event/_emitters.py
new file mode 100644
index 0000000..f12bf7e
--- /dev/null
+++ b/flexx/event/_emitters.py
@@ -0,0 +1,146 @@
+"""
+Implementation of descriptors for generating events:
+prop, readonly and emitter.
+"""
+
+# Decorators to apply at a HasEvents class
+
+def prop(func):
+    """ Decorator to define a settable propery. An event is emitted
+    when the property is set, which has values for "old_value" and
+    "new_value".
+    
+    .. code-block:: python
+    
+        class MyObject(event.HasEvents):
+           
+           @prop
+           def foo(self, v=1):
+                ''' docstring goes here. '''
+                return float(v)
+        
+        m = MyObject(foo=2)
+        m.foo = 3
+    
+    The method should have one argument, which should have a default
+    value, which represents the initial value of the property. The body
+    of the method is used to do verification and normalization of the
+    value being set. The method's docstring is used as the property's
+    docstring.
+    """
+    if not callable(func):
+        raise TypeError('prop decorator needs a callable')
+    return Property(func)
+
+
+def readonly(func):
+    """ Decorator to define a readonly property. An event is emitted
+    when the property is set, which has values for "old_value" and
+    "new_value". To set a readonly property internally, use the
+    :func:`HasEvents._set_prop() <flexx.event.HasEvents._set_prop>` method.
+    
+    .. code-block:: python
+    
+        class MyObject(event.HasEvents):
+           
+           @readonly
+           def bar(self, v=1):
+                return float(v)
+        
+        m = MyObject()
+        m._set_prop('bar', 2)  # only for internal use
+    
+    """
+    if not callable(func):
+        raise TypeError('readonly decorator needs a callable')
+    return Readonly(func)
+
+
+def emitter(func):
+    """ Decorator to define an emitter. An emitter is an attribute that
+    makes it easy to emit specific events and functions as a placeholder
+    for documenting an event.
+    
+    .. code-block:: python
+    
+        class MyObject(event.HasEvents):
+           
+           @emitter
+           def spam(self, v):
+                return dict(value=v)
+        
+        m = MyObject()
+        m.spam(42)
+    
+    The method can have any number of arguments, and should return a
+    dictionary that represents the event to generate. The method's
+    docstring is used as the emitter's docstring.
+    """
+    if not callable(func):
+        raise TypeError('emitter decorator needs a callable')
+    return Emitter(func)
+
+
+class BaseEmitter:
+    """ Base class for descriptors used for generating events.
+    """
+    
+    def __init__(self, func):
+        assert callable(func)
+        self._func = func
+        self._name = func.__name__  # updated by HasEvents meta class
+        self._is_being_set = False
+        self.__doc__ = '*%s*: %s' % (self.__class__.__name__.lower(),
+                                     func.__doc__ or self._name)
+    
+    def __repr__(self):
+        cls_name = self.__class__.__name__
+        return '<%s for %s at 0x%x>' % (cls_name, self._name, id(self))
+
+
+class Property(BaseEmitter):
+    """ A value that is gettable and settable.
+    """
+    
+    _SUFFIX = '_value'
+    
+    
+    def __set__(self, instance, value):
+        if isinstance is not None:  # pragma: no cover
+            return instance._set_prop(self._name, value)
+    
+    def __delete__(self, instance):
+        raise AttributeError('Cannot delete property %r.' % self._name)
+    
+    def __get__(self, instance, owner):
+        if instance is None:
+            return self
+        
+        private_name = '_' + self._name + self._SUFFIX
+        return getattr(instance, private_name)
+
+
+class Readonly(Property):
+    """ A value that is gettable and only settable internally.
+    """
+    
+    def __set__(self, instance, value):
+        raise AttributeError("Can't set readonly property %r" % self._name)
+
+
+class Emitter(BaseEmitter):
+    """ Placeholder for documentation and easy emitting of the event.
+    """
+    
+    def __set__(self, instance, value):
+        raise AttributeError("Can't set emitter attribute %r" % self._name)
+    
+    def __delete__(self, instance):
+        raise AttributeError('Cannot delete emitter attribute %r.' % self._name)
+    
+    def __get__(self, instance, owner):
+        if instance is None:
+            return self
+        func = instance._get_emitter(self._name)
+        func.__doc__ = self.__doc__
+        return func
diff --git a/flexx/event/_handler.py b/flexx/event/_handler.py
new file mode 100644
index 0000000..eb545c0
--- /dev/null
+++ b/flexx/event/_handler.py
@@ -0,0 +1,320 @@
+"""
+Implementation of handler class and corresponding descriptor.
+"""
+
+import sys
+import logging
+import inspect
+import weakref
+
+from ._dict import Dict
+from ._loop import loop
+
+
+def this_is_js():
+    return False
+
+console = setTimeout = None
+
+
+# Decorator to wrap a function in a Handler object
+def connect(*connection_strings):
+    """ Decorator to turn a method of HasEvents into an event
+    :class:`Handler <flexx.event.Handler>`.
+    
+    A method can be connected to multiple event types. Each connection
+    string represents an event type to connect to. Read more about
+    dynamism and labels for further information on the possibilities
+    of connection strings.
+    
+    Note that naming a method ``on_foo()`` will be automatically
+    connected to event "foo". To connect a regular function to an event,
+    use the :func:`HasEvents.connect() <flexx.event.HasEvents.connect>` method.
+    
+    .. code-block:: py
+        
+        class MyObject(event.HasEvents):
+            @event.connect('first_name', 'last_name')
+            def greet(self, *events):
+                print('hello %s %s' % (self.first_name, self.last_name))
+    """
+    if (not connection_strings) or (len(connection_strings) == 1 and
+                                    callable(connection_strings[0])):
+        raise RuntimeError('Connect decorator needs one or more event strings.')
+    
+    func = None
+    if callable(connection_strings[0]):
+        func = connection_strings[0]
+        connection_strings = connection_strings[1:]
+    
+    for s in connection_strings:
+        if not (isinstance(s, str) and len(s) > 0):
+            raise ValueError('Connection string must be nonempty strings.')
+    
+    def _connect(func):
+        if not callable(func):
+            raise TypeError('connect() decorator requires a callable.')
+        return HandlerDescriptor(func, connection_strings)
+    
+    if func is not None:
+        return _connect(func)
+    else:
+        return _connect
+
+
+class HandlerDescriptor:
+    """ Class descriptor for handlers.
+    """
+    
+    def __init__(self, func, connection_strings):
+        assert callable(func)  # HandlerDescriptor is not instantiated directly
+        self._func = func
+        self._name = func.__name__  # updated by HasEvents meta class
+        self._connection_strings = connection_strings
+        self.__doc__ = '*%s*: %s' % ('event handler', func.__doc__ or self._name)
+    
+    def __repr__(self):
+        cls_name = self.__class__.__name__
+        return '<%s (this should be a class attribute) at 0x%x>' % (cls_name, id(self))
+        
+    def __set__(self, obj, value):
+        raise AttributeError('Cannot overwrite handler %r.' % self._name)
+    
+    def __delete__(self, obj):
+        raise AttributeError('Cannot delete handler %r.' % self._name)
+    
+    def __get__(self, instance, owner):
+        if instance is None:
+            return self
+        
+        private_name = '_' + self._name + '_handler'
+        try:
+            return getattr(instance, private_name)
+        except AttributeError:
+            new = Handler(self._func, self._connection_strings, instance)
+            setattr(instance, private_name, new)
+            return new
+
+
+class Handler:
+    """ Wrapper around a function object to connect it to one or more events.
+    This class should not be instantiated directly; use ``event.connect`` or
+    ``HasEvents.connect`` instead.
+    
+    Arguments:
+        func (callable): function that handles the events.
+        connection_strings (list): the strings that represent the connections.
+        ob (HasEvents): the HasEvents object to use a a basis for the
+            connection. A weak reference to this object is stored. It
+            is passed a a first argument to the function in case its
+            first arg is self.
+    """
+    
+    _count = 0
+    
+    def __init__(self, func, connection_strings, ob):
+        # Check and set func
+        assert callable(func)
+        self._func = func
+        self._name = func.__name__
+        Handler._count += 1
+        self._id = 'h%i' % Handler._count  # to ensure a consistent event order
+        
+        # Set docstring; this appears correct in sphinx docs
+        self.__doc__ = '*%s*: %s' % ('event handler', func.__doc__ or self._name)
+        
+        # Store object using a weakref
+        self._ob = weakref.ref(ob)
+        
+        # Get whether function is a method
+        try:
+            self._func_is_method = inspect.getargspec(func)[0][0] in ('self', 'this')
+        except (TypeError, IndexError):
+            self._func_is_method = False
+        
+        self._init(connection_strings)
+    
+    
+    def _init(self, connection_strings):
+        """ Init of this handler that is compatible with PyScript.
+        """
+        # Init connections
+        self._connections = []
+        for s in connection_strings:
+            d = Dict()  # don't do Dict(foo=x) bc PyScript only supports that for dict
+            self._connections.append(d)
+            d.fullname = s
+            d.type = s.split('.')[-1]
+            d.objects = []
+        
+        # Pending events for this handler
+        self._scheduled_update = False
+        self._pending = []  # pending events
+        
+        # Connect
+        for index in range(len(self._connections)):
+            self._connect_to_event(index)
+    
+    def __repr__(self):
+        c = '+'.join([str(len(c.objects)) for c in self._connections])
+        cname = self.__class__.__name__
+        return '<%s %r with %s connections at 0x%x>' % (cname, self._name, c, id(self))
+    
+    def get_name(self):
+        """ Get the name of this handler, usually corresponding to the name
+        of the function that this signal wraps.
+        """
+        return self._name
+    
+    def get_connection_info(self):
+        """ Get a list of tuples (name, connection_names), where
+        connection_names is a list of type names (including label) for
+        the made connections.
+        """
+        return [(c.fullname, [u[1] for u in c.objects])
+                for c in self._connections]
+    
+    ## Calling / handling
+    
+    def __call__(self, *events):
+        """ Call the handler function.
+        """
+        if self._func_is_method and self._ob is not None:
+            return self._func(self._ob(), *events)
+        else:
+            return self._func(*events)
+    
+    def _add_pending_event(self, label, ev):
+        """ Add an event object to be handled at the next event loop
+        iteration. Called from HasEvents.emit().
+        """
+        if not self._scheduled_update:
+            # register only once
+            self._scheduled_update = True
+            if this_is_js():
+                setTimeout(self._handle_now_callback.bind(self), 0)
+            else:
+                loop.call_later(self._handle_now_callback)
+        self._pending.append((label, ev))
+    
+    def _handle_now_callback(self):
+        self._scheduled_update = False
+        self.handle_now()
+    
+    def handle_now(self):
+        """ Invoke a call to the handler function with all pending
+        events. This is normally called in a next event loop iteration
+        when an event is scheduled for this handler, but it can also
+        be called manually to force the handler to process pending
+        events *now*.
+        """
+        # Collect pending events and check what connections need to reconnect
+        events = []
+        reconnect = []
+        for label, ev in self._pending:
+            events.append(ev)
+            if label.startswith('reconnect_'):
+                index = int(label.split('_')[-1])
+                reconnect.append(index)
+        self._pending = []
+        # Reconnect (dynamism)
+        for index in reconnect:
+            self._connect_to_event(index)
+        # Handle events
+        if events:
+            try:
+                self(*events)
+            except Exception as err:
+                if this_is_js():
+                    console.log(err)
+                else:
+                    # Allow post-mortem debugging
+                    type_, value, tb = sys.exc_info()
+                    tb = tb.tb_next  # Skip *this* frame
+                    sys.last_type = type_
+                    sys.last_value = value
+                    sys.last_traceback = tb
+                    tb = None  # Get rid of it in this namespace
+                    # Show the exception
+                    logging.exception(value)
+    
+    
+    ## Connecting
+    
+    def dispose(self):
+        """ Cleanup any references.
+        
+        Disconnects all connections, and cancel all pending events.
+        """
+        for connection in self._connections:
+            while len(connection.objects):
+                ob, name = connection.objects.pop(0)
+                ob.disconnect(name, self)
+        while len(self._pending):
+            self._pending.pop()  # no list.clear on legacy py
+    
+    def _clear_hasevents_refs(self, ob):
+        """ Clear all references to the given HasEvents instance. This is
+        called from a HasEvents' dispose() method. This handler remains
+        working, but wont receive events from that object anymore.
+        """
+        for connection in self._connections:
+            for i in reversed(range(len(connection.objects))):
+                if connection.objects[i][0] is ob:
+                    connection.objects.pop(i)
+        
+        # Do not clear pending events. This handler is assumed to continue
+        # working, and should thus handle its pending events at some point,
+        # at which point it cannot hold any references to ob anymore.
+    
+    def _connect_to_event(self, index):
+        """ Connect one connection.
+        """
+        connection = self._connections[index]
+        
+        # Disconnect
+        while len(connection.objects):
+            ob, name = connection.objects.pop(0)
+            ob.disconnect(name, self)
+        
+        path = connection.fullname.split('.')[:-1]
+        
+        # Obtain root object and setup connections
+        ob = self._ob()
+        if ob is not None:
+            self._seek_event_object(index, path, ob)
+        
+        # Verify
+        if not connection.objects:
+            raise RuntimeError('Could not connect to %r' % connection.fullname)
+        
+        # Connect
+        for ob, type in connection.objects:
+            ob._register_handler(type, self)
+    
+    def _seek_event_object(self, index, path, ob):
+        """ Seek an event object based on the name.
+        This bit is PyScript compatible (_resolve_signals is not).
+        """
+        connection = self._connections[index]
+        
+        # Done traversing name: add to list or fail
+        if ob is None or len(path) == 0:
+            if ob is None or not hasattr(ob, '_IS_HASEVENTS'):
+                return  # we cannot seek further
+            connection.objects.append((ob, connection.type))
+            return  # found it
+        
+        # Resolve name
+        obname, path = path[0], path[1:]
+        if hasattr(ob, '_IS_HASEVENTS') and obname in ob.__properties__:
+            name_label = obname + ':reconnect_' + str(index)
+            connection.objects.append((ob, name_label))
+            ob = getattr(ob, obname, None)
+        elif obname == '*' and isinstance(ob, (tuple, list)):
+            for sub_ob in ob:
+                self._seek_event_object(index, path, sub_ob)
+            return
+        else:
+            ob = getattr(ob, obname, None)
+        return self._seek_event_object(index, path, ob)
diff --git a/flexx/event/_hasevents.py b/flexx/event/_hasevents.py
new file mode 100644
index 0000000..c566ef1
--- /dev/null
+++ b/flexx/event/_hasevents.py
@@ -0,0 +1,374 @@
+"""
+Implements the HasEvents class; the core class via which events are
+generated and handled. It is the object that keeps track of handlers.
+"""
+
+from ._dict import Dict
+from ._handler import HandlerDescriptor, Handler
+from ._emitters import BaseEmitter, Property
+
+def this_is_js():
+    return False
+
+# Reasons to want/need a metaclass:
+# * Keep track of subclasses (but can handle that in flexx.app.Model)
+# * Wee bit more efficient? (I wonder if it would be measurable)
+# * You can turn on_foo() into a handler instead of wrapping it in an
+#   internal handler. Though maybe the latter is nicer.
+# * If we want to support this, there is no other way:
+#   foo = Int(42, 'this is a property')
+
+
+# todo: delete this? mmm, I think we need this for the JS part ...
+# # From six.py
+# def with_metaclass(meta, *bases):
+#     """Create a base class with a metaclass."""
+#     # This requires a bit of explanation: the basic idea is to make a dummy
+#     # metaclass for one level of class instantiation that replaces itself with
+#     # the actual metaclass.
+#     # On Python 2.7, the name cannot be unicode :/
+#     tmp_name = b'tmp_class' if sys.version_info[0] == 2 else 'tmp_class'
+#     class metaclass(meta):
+#         def __new__(cls, name, this_bases, d):
+#             return meta(name, bases, d)
+#     return type.__new__(metaclass, tmp_name, (), {})
+# 
+# 
+# def new_type(name, *args, **kwargs):
+#     """ Alternative for type(...) to be legacy-py compatible.
+#     """
+#     name = name.encode() if sys.version_info[0] == 2 else name
+#     return type(name, *args, **kwargs)
+# 
+#
+# class HasEventsMeta(type):
+#     """ Meta class for HasEvents
+#     * Set the name of each handler
+#     * Sets __handlers__ attribute on the class
+#     """
+#     
+#     CLASSES = []
+#     
+#     def __init__(cls, name, bases, dct):
+#         
+#         HasEventsMeta.CLASSES.append(cls)
+#         
+#         # Collect handlers defined on this class
+#         handlers = {}
+#         emitters = {}
+#         for name in dir(cls):
+#             if name.startswith('__'):
+#                 continue
+#             val = getattr(cls, name)
+#             if isinstance(val, BaseEmitter):
+#                 emitters[name] = val
+#             elif isinstance(val, HandlerDescriptor):
+#                 handlers[name] = val
+#             elif name.startswith('on_'):
+#                 val = HandlerDescriptor(val, [name[3:]],  sys._getframe(1))
+#                 setattr(cls, name, val)
+#                 handlers[name] = val
+#         # Finalize all found emitters
+#         for name, emitter in emitters.items():
+#             emitter._name = name
+#         for name, handler in handlers.items():
+#             handler._name = name
+#         # Cache prop names
+#         cls.__handlers__ = [name for name in sorted(handlers.keys())]
+#         cls.__emitters__ = [name for name in sorted(emitters.keys())]
+#         # Proceed as normal
+#         type.__init__(cls, name, bases, dct)
+
+
+#class HasEvents(with_metaclass(HasEventsMeta, object)):
+class HasEvents:
+    """ Base class for objects that have properties and can emit events.
+    Initial values of settable properties can be provided by passing them
+    as keyword arguments.
+    
+    Objects of this class can emit events through their ``emit()``
+    method. Subclasses can use the
+    :func:`prop <flexx.event.prop>` and :func:`readonly <flexx.event.readonly>`
+    decorator to create properties, and the
+    :func:`connect <flexx.event.connect>` decorator to create handlers.
+    Methods named ``on_foo`` are connected to the event "foo".
+    
+    .. code-block:: python
+    
+        class MyObject(event.HasEvents):
+            
+            # Emitters
+            
+            @event.prop
+            def foo(self, v=0):
+                return float(v)
+            
+            @event.emitter
+            def bar(self, v):
+                return dict(value=v)  # the event to emit
+            
+            # Handlers
+            
+            @event.connect
+            def handle_foo(self, *events):
+                print('foo was set to', events[-1].new_value)
+            
+            def on_bar(self, *events):
+                print('bar event was generated')
+        
+        ob = MyObject(foo=42)
+        
+        @ob.connect('foo')
+        def another_foo handler(*events):
+            print('foo was set %i times' % len(events))
+    
+    """
+    
+    _IS_HASEVENTS = True
+    
+    def __init__(self, **property_values):
+        
+        # Init some internal variables
+        self._he_handlers = {}
+        self._he_props_being_set = {}
+        
+        # Detect our emitters and handlers
+        handlers, emitters, properties = {}, {}, {}
+        for name in dir(self.__class__):
+            if name.startswith('__'):
+                continue
+            val = getattr(self.__class__, name)
+            if isinstance(val, BaseEmitter):
+                setattr(self, '_' + name + '_func', val._func)
+                if isinstance(val, Property):
+                    properties[name] = val
+                else:
+                    emitters[name] = val
+            elif isinstance(val, HandlerDescriptor):
+                handlers[name] = val
+            elif name.startswith('on_') and callable(val):
+                self._he_handlers.setdefault(name[3:], [])
+                Handler(val, [name[3:]], self)  # registers itself
+        
+        # Store handlers, properties and emitters that we found
+        self.__handlers__ = [name for name in sorted(handlers.keys())]
+        self.__emitters__ = [name for name in sorted(emitters.keys())]
+        self.__properties__ = [name for name in sorted(properties.keys())]
+        
+        # Instantiate handlers, its enough to reference them
+        for name in self.__handlers__:
+            getattr(self, name)
+        # Instantiate emitters
+        for name in self.__emitters__:
+            self._he_handlers.setdefault(name, [])
+        for name in self.__properties__:
+            self._he_handlers.setdefault(name, [])
+            self._init_prop(name)
+        
+        # Initialize given properties
+        for name, value in property_values.items():
+            if name in self.__properties__:
+                setattr(self, name, value)
+            else:
+                cname = self.__class__.__name__
+                raise AttributeError('%s does not have a property %r' % (cname, name))
+    
+    def dispose(self):
+        """ Use this to dispose of the object to prevent memory leaks.
+        
+        Make all subscribed handlers to forget about this object, clear
+        all references to subscribed handlers, disconnect all handlers
+        defined on this object.
+        """
+        for name, handlers in self._he_handlers.items():
+            for label, handler in handlers:
+                handler._clear_hasevents_refs(self)
+            while len(handlers):
+                handlers.pop()  # no list.clear on legacy py
+        for name in self.__handlers__:
+            getattr(self, name).dispose()
+    
+    def _register_handler(self, type, handler):
+        # Register a handler for the given event type. The type
+        # can include a label, e.g. 'mouse_down:foo'.
+        # This is called from Handler objects at initialization and when
+        # they reconnect (dynamism).
+        type, _, label = type.partition(':')
+        label = label or handler._name
+        handlers = self._he_handlers.setdefault(type, [])
+        entry = label, handler
+        if entry not in handlers:
+            handlers.append(entry)
+        handlers.sort(key=lambda x: x[0]+'-'+x[1]._id)
+    
+    def disconnect(self, type, handler=None):
+        """ Disconnect handlers. 
+        
+        Parameters:
+            type (str): the type for which to disconnect any handlers.
+                Can include the label to only disconnect handlers that
+                were registered with that label.
+            handler (optional): the handler object to disconnect. If given,
+               only this handler is removed.
+        """
+        # This is called from Handler objects when they dispose and when
+        # they reconnect (dynamism).
+        type, _, label = type.partition(':')
+        handlers = self._he_handlers.get(type, ())
+        for i in reversed(range(len(handlers))):
+            entry = handlers[i]
+            if not ((label and label != entry[0]) or
+                    (handler and handler is not entry[1])):
+                handlers.pop(i)
+    
+    def emit(self, type, ev):
+        """ Generate a new event and dispatch to all event handlers.
+        
+        Arguments:
+            type (str): the type of the event. Should not include a label.
+            ev (dict): the event object. This dict is turned into a Dict,
+                so that its elements can be accesses as attributes.
+        """
+        type, _, label = type.partition(':')
+        if label:
+            raise ValueError('The type given to emit() should not include a label.')
+        # Prepare event
+        if not isinstance(ev, dict):
+            raise TypeError('Event object (for %r) must be a dict' % type)
+        ev = Dict(ev)  # make copy and turn into nicer Dict on py
+        ev.type = type
+        ev.source = self
+        # Push the event to the handlers (handlers use labels for dynamism)
+        for label, handler in self._he_handlers.get(type, ()):
+            handler._add_pending_event(label, ev)  # friend class
+    
+    def _init_prop(self, prop_name):
+        # Initialize a property
+        private_name = '_' + prop_name + '_value'
+        func_name = '_' + prop_name + '_func'
+        # Trigger default value
+        func = getattr(self, func_name)
+        try:
+            value2 = func(self)
+        except Exception as err:
+            raise RuntimeError('Could not get default value for property'
+                               '%r:\n%s' % (prop_name, err))
+        # Update value and emit event
+        setattr(self, private_name, value2)
+        self.emit(prop_name, dict(new_value=value2, old_value=value2))
+    
+    def _set_prop(self, prop_name, value):
+        """ Set the value of a (readonly) property.
+        
+        Parameters:
+            prop_name (str): the name of the property to set.
+            value: the value to set.
+        """
+        # Checks
+        if not isinstance(prop_name, str):
+            raise TypeError("_set_prop's first arg must be str, not %s" %
+                             prop_name.__class__)
+        if prop_name not in self.__properties__:
+            cname = self.__class__.__name__
+            raise AttributeError('%s object has no property %r' % (cname, prop_name))
+        if self._he_props_being_set.get(prop_name, False):
+            return
+        # Prepare
+        private_name = '_' + prop_name + '_value'
+        func_name = '_' + prop_name + '_func'
+        # Validate value
+        self._he_props_being_set[prop_name] = True
+        try:
+            func = getattr(self, func_name)
+            if this_is_js():
+                value2 = func.apply(self, [value])
+            else:
+                value2 = func(self, value)
+        finally:
+            self._he_props_being_set[prop_name] = False
+        # Update value and emit event
+        old = getattr(self, private_name, None)
+        if value2 != old:
+            setattr(self, private_name, value2)
+            self.emit(prop_name, dict(new_value=value2, old_value=old))
+    
+    def _get_emitter(self, emitter_name):
+        # Get an emitter function.
+        func_name = '_' + emitter_name + '_func'
+        func = getattr(self, func_name)
+        def emitter_func(*args):
+            if this_is_js():
+                ev = func.apply(self, args)
+            else:
+                ev = func(self, *args)
+            self.emit(emitter_name, ev)
+        return emitter_func
+    
+    def get_event_types(self):
+        """ Get the known event types for this HasEvent object. Returns
+        a list of event type names, for which there is a
+        property/emitter or for which any handlers are registered.
+        Sorted alphabetically.
+        """
+        return list(sorted(self._he_handlers.keys()))
+    
+    def get_event_handlers(self, type):
+        """ Get a list of handlers for the given event type. The order
+        is the order in which events are handled: alphabetically by
+        label.
+        
+        Parameters:
+            type (str): the type of event to get handlers for. Should not
+                include a label.
+        
+        """
+        if not type:
+            raise TypeError('get_event_handlers() missing "type" argument.')
+        type, _, label = type.partition(':')
+        if label:
+            raise ValueError('The type given to get_event_handlers() '
+                             'should not include a label.')
+        handlers = self._he_handlers.get(type, ())
+        return [h[1] for h in handlers]
+
+    # This method does *not* get transpiled
+    def connect(self, *connection_strings):
+        """ Connect a function to one or more events of this instance. Can
+        also be used as a decorator. See the
+        :func:`connect <flexx.event.connect>` decorator for more information.
+        
+        .. code-block:: py
+            
+            h = HasEvents()
+            
+            # Usage as a decorator
+            @h.connect('first_name', 'last_name')
+            def greet(*events):
+                print('hello %s %s' % (h.first_name, h.last_name))
+            
+            # Direct usage
+            h.connect(greet, 'first_name', 'last_name')
+        
+        """
+        if (not connection_strings) or (len(connection_strings) == 1 and
+                                        callable(connection_strings[0])):
+            raise RuntimeError('connect() needs one or more connection strings.')
+        
+        func = None
+        if callable(connection_strings[0]):
+            func = connection_strings[0]
+            connection_strings = connection_strings[1:]
+        
+        for s in connection_strings:
+            if not (isinstance(s, str) and len(s) > 0):
+                raise ValueError('Connection string must be nonempty strings.')
+        
+        def _connect(func):
+            if not callable(func):
+                raise TypeError('connect() decotator requires a callable.')
+            return Handler(func, connection_strings, self)
+        
+        if func is not None:
+            return _connect(func)
+        else:
+            return _connect
diff --git a/flexx/event/_js.py b/flexx/event/_js.py
new file mode 100644
index 0000000..b3e501f
--- /dev/null
+++ b/flexx/event/_js.py
@@ -0,0 +1,259 @@
+"""
+Implementation of flexx.event in JS via PyScript.
+"""
+
+import json
+
+from flexx.pyscript import py2js as py2js_
+from flexx.pyscript.parser2 import get_class_definition
+
+from flexx.event._emitters import BaseEmitter, Property
+from flexx.event._handler import HandlerDescriptor, Handler
+from flexx.event._hasevents import HasEvents
+
+
+Object = Date = None  # fool pyflake
+
+reprs = json.dumps
+
+
+def py2js(*args, **kwargs):
+    kwargs['inline_stdlib'] = False
+    kwargs['docstrings'] = False
+    return py2js_(*args, **kwargs)
+
+
+class HasEventsJS:
+    """ An implementation of the HasEvents class in PyScript. It has
+    some boilerplate code to create handlers and emitters, but otherwise
+    shares most of the code with the Python classes by transpiling their
+    methods via PyScript. This ensures that the Python and JS
+    implementation of this event system have the same API and behavior.
+    
+    The Python version of this class has a ``JSCODE`` attribute that
+    contains the auto-generated JavaScript for this class.
+    """
+    
+    _HANDLER_COUNT = 0
+    _IS_HASEVENTS = True
+    
+    def __init__(self):
+        
+        # Init some internal variables
+        self._he_handlers = {}
+        self._he_props_being_set = {}
+        
+        # Create a handler for methods that start with "on_"
+        for name in Object.keys(self):
+            if name.startswith('on_'):
+                val = self[name]
+                if callable(val):
+                    self.__create_Handler(val, name, [name[3:]])
+        # Create handlers
+        for name in self.__handlers__:
+            func = self['_' + name + '_func']
+            self[name] = self.__create_Handler(func, name, func._connection_strings)
+        # Create properties
+        for name in self.__properties__:
+            self._he_handlers.setdefault(name, [])
+            func = self['_' + name + '_func']
+            creator = self['__create_' + func._emitter_type]
+            creator(name)
+        # Create emitters
+        for name in self.__emitters__:
+            self._he_handlers.setdefault(name, [])
+            func = self['_' + name + '_func']
+            self.__create_Emitter(name)
+    
+    def connect(self, func, *connection_strings):
+        # The JS version (no decorator functionality)
+        if not connection_strings:
+            raise RuntimeError('connect() needs one or more connection strings.')
+        
+        for s in connection_strings:
+            if not (isinstance(s, str) and len(s) > 0):
+                raise ValueError('Connection string must be nonempty strings.')
+        
+        if not callable(func):
+            raise TypeError('connect() decotator requires a callable.')
+        return self.__create_Handler(func, func.name or 'anonymous', connection_strings)
+    
+    def __create_Property(self, name):
+        private_name = '_' + name + '_value'
+        def getter():
+            return self[private_name]
+        def setter(x):
+            self._set_prop(name, x)
+        self[private_name] = value2 = self['_' + name + '_func']()  # init
+        self.emit(name, dict(new_value=value2, old_value=value2))
+        opts = {'enumerable': True, 'configurable': True,  # i.e. overloadable
+                'get': getter, 'set': setter}
+        Object.defineProperty(self, name, opts)
+    
+    def __create_Readonly(self, name):
+        private_name = '_' + name + '_value'
+        def getter():
+            return self[private_name]
+        def setter(x):
+            raise AttributeError('Readonly %s is not settable' % name)
+        self[private_name] = value2 = self['_' + name + '_func']()  # init
+        self.emit(name, dict(new_value=value2, old_value=value2))
+        opts = {'enumerable': True, 'configurable': True,  # i.e. overloadable
+                'get': getter, 'set': setter}
+        Object.defineProperty(self, name, opts)
+    
+    def __create_Emitter(self, name):
+        def getter():
+            return self._get_emitter(name)
+        def setter(x):
+            raise AttributeError('Emitter %s is not settable' % name)
+        opts = {'enumerable': True, 'configurable': True,  # i.e. overloadable
+                'get': getter, 'set': setter}
+        Object.defineProperty(self, name, opts)
+    
+    def __create_Handler(self, func, name, connection_strings):
+        
+        # Create function that becomes our "handler object"
+        def handler(*events):
+            return func.apply(self, events)
+        
+        # Attach methods to the function object (this gets replaced)
+        HANDLER_METHODS_HOOK  # noqa
+        
+        # Init handler
+        that = self
+        HasEvents.prototype._HANDLER_COUNT += 1
+        handler._name = name
+        handler._id = 'h' + str(HasEvents.prototype._HANDLER_COUNT)
+        handler._ob = lambda : that  # no weakref in JS
+        handler._init(connection_strings, self)
+        
+        return handler
+
+
+def get_HasEvents_js():
+    """ Get the final code for the JavaScript version of the HasEvents class.
+    """
+    # Start with our special JS version
+    jscode = py2js(HasEventsJS, 'HasEvents')
+    # Add the Handler methods
+    code = '\n'
+    for name, val in sorted(Handler.__dict__.items()):
+        if not name.startswith('__') and callable(val):
+            code += py2js(val, 'handler.' + name, indent=1)
+            code += '\n'
+    jscode = jscode.replace('HANDLER_METHODS_HOOK', code)
+    # Add the methods from the Python HasEvents class
+    code = '\n'
+    for name, val in sorted(HasEvents.__dict__.items()):
+        if name.startswith('__') or not callable(val) or name in ['connect', ]:
+            continue
+        code += py2js(val, 'HasEvents.prototype.' + name)
+        code += '\n'
+    jscode += code
+    # Almost done
+    jscode = jscode.replace('new Dict()', '{}').replace('new Dict(', '_pyfunc_dict(')
+    return jscode
+
+
+HasEventsJS.JSCODE = get_HasEvents_js()
+
+
+def create_js_hasevents_class(cls, cls_name, base_class='HasEvents.prototype'):
+    """ Create the JS equivalent of a subclass of the HasEvents class.
+    
+    Given a Python class with handlers, properties and emitters, this
+    creates the code for the JS version of this class. It also supports
+    class constants that are int/float/str, or a tuple/list thereof.
+    """
+    
+    assert cls_name != 'HasEvents'  # we need this special class above instead
+    
+    handlers = []
+    emitters = []
+    properties = []
+    total_code = []
+    funcs_code = []  # functions and emitters go below class constants
+    const_code = []
+    err = ('Objects on JS HasEvents classes can only be int, float, str, '
+           'or a list/tuple thereof. Not %s -> %r.')
+    
+    total_code.extend(get_class_definition(cls_name, base_class))
+    prefix = '' if cls_name.count('.') else 'var '
+    total_code[0] = prefix + total_code[0]
+    
+    for name, val in sorted(cls.__dict__.items()):
+        name = name.replace('_JS__', '_%s__' % cls_name.split('.')[-1])  # fix mangling
+        funcname = '_' + name + '_func'
+        if isinstance(val, BaseEmitter):
+            if isinstance(val, Property):
+                properties.append(name)
+            else:
+                emitters.append(name)
+            # Add function def
+            code = py2js(val._func, cls_name + '.prototype.' + funcname)
+            code = code.replace('super()', base_class)  # fix super
+            funcs_code.append(code.rstrip())
+            # Mark to not bind the func
+            t = '%s.prototype.%s.nobind = true;'
+            funcs_code.append(t % (cls_name, funcname))
+            # Add type of emitter
+            t = '%s.prototype.%s._emitter_type = %s;'
+            emitter_type = val.__class__.__name__
+            funcs_code.append(t % (cls_name, funcname, reprs(emitter_type)))
+            funcs_code.append('')
+        elif isinstance(val, HandlerDescriptor):
+            handlers.append(name)
+            # Add function def
+            code = py2js(val._func, cls_name + '.prototype.' + funcname)
+            code = code.replace('super()', base_class)  # fix super
+            funcs_code.append(code.rstrip())
+            # Mark to not bind the func
+            t = '%s.prototype.%s.nobind = true;'
+            funcs_code.append(t % (cls_name, funcname))
+            # Add connection strings to the function object
+            t = '%s.prototype.%s._connection_strings = %s;'
+            funcs_code.append(t % (cls_name, funcname, reprs(val._connection_strings)))
+            funcs_code.append('')
+        elif callable(val):
+            code = py2js(val, cls_name + '.prototype.' + name)
+            code = code.replace('super()', base_class)  # fix super
+            funcs_code.append(code.rstrip())
+            funcs_code.append('')
+        elif name.startswith('__'):
+            pass  # we create our own __emitters__ list
+        else:
+            try:
+                serialized = json.dumps(val)
+            except Exception as err:  # pragma: no cover
+                raise ValueError('Attributes on JS HasEvents class must be '
+                                 'JSON compatible.\n%s' % str(err))
+            const_code.append('%s.prototype.%s = JSON.parse(%s)' %
+                              (cls_name, name, reprs(serialized)))
+    
+    # Store handlers, properties and emitters that we found
+    if base_class in ('Object', 'HasEvents.prototype'):
+        t = '%s.prototype.__emitters__ = %s;'
+        total_code.append(t % (cls_name, reprs(list(sorted(emitters)))))
+        t = '%s.prototype.__properties__ = %s;'
+        total_code.append(t % (cls_name, reprs(list(sorted(properties)))))
+        t = '%s.prototype.__handlers__ = %s;'
+        total_code.append(t % (cls_name, reprs(list(sorted(handlers)))))
+    else:
+        t = '%s.prototype.__emitters__ = %s.__emitters__.concat(%s).sort();'
+        total_code.append(t % (cls_name, base_class, reprs(emitters)))
+        t = '%s.prototype.__properties__ = %s.__properties__.concat(%s).sort();'
+        total_code.append(t % (cls_name, base_class, reprs(properties)))
+        t = '%s.prototype.__handlers__ = %s.__handlers__.concat(%s).sort();'
+        total_code.append(t % (cls_name, base_class, reprs(handlers)))
+    
+    total_code.append('')
+    total_code.extend(const_code)
+    total_code.append('')
+    total_code.extend(funcs_code)
+    return '\n'.join(total_code)
+
+
+if __name__ == '__main__':
+    # Testing ...
+    print(HasEventsJS.JSCODE)
diff --git a/flexx/event/_loop.py b/flexx/event/_loop.py
new file mode 100644
index 0000000..afdad3a
--- /dev/null
+++ b/flexx/event/_loop.py
@@ -0,0 +1,128 @@
+"""
+Implementation of basic event loop object. Can be integrated a real
+event loop such as tornado or Qt.
+"""
+
+import sys
+import logging
+
+
+# todo: maybe this can be the base class for the tornado loop that we use in flexx.app
+
+class Loop:
+    """ A simple proxy event loop. There is one instance in 
+    ``flexx.event.loop``. This is used by handlers to register the
+    handling of pending events. Users typically don't need to be aware
+    of this.
+    
+    This proxy can integrate with an existing event loop (e.g. of Qt
+    and Tornado). If Qt or Tornado is imported at the time that
+    ``flexx.event`` gets imported, the loop is integrated automatically.
+    This object can also be used as a context manager; events get
+    processed when the context exits.
+    """
+    
+    def __init__(self):
+        self._pending_calls = []
+        self._calllaterfunc = lambda x: None
+        self._scheduled_update = 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_update:
+            self._scheduled_update = True
+            self._calllaterfunc(self.iter)
+    
+    def iter(self):
+        """ Do one event loop iteration; process all pending function calls.
+        """
+        self._scheduled_update = False
+        while self._pending_calls:
+            func = self._pending_calls.pop(0)
+            try:
+                func()
+            except Exception as err:
+                logging.exception(err)
+    
+    def __enter__(self):
+        return self
+    
+    def __exit__(self, type, value, traceback):
+        self.iter()
+    
+    def integrate(self, call_later_func=None, raise_on_fail=True):
+        """ Integrate with an existing event loop system.
+        
+        Params:
+            call_later_func (func): a function that can be called to
+                schedule the calling of a given function. If not given,
+                will try to connect to Tornado or Qt event loop, but only
+                if either library is already imported.
+            raise_on_fail (bool): whether to raise an error when the
+                integration could not be performed.
+        """
+        if call_later_func is not None:
+            if callable(call_later_func):
+                self._calllaterfunc = call_later_func
+            else:
+                raise ValueError('call_later_func must be a function')
+        elif 'tornado' in sys.modules:
+            self.integrate_tornado()
+        elif 'PyQt4.QtGui' in sys.modules:  # pragma: no cover
+            self.integrate_pyqt4()
+        elif 'PySide.QtGui' in sys.modules:  # pragma: no cover
+            self.integrate_pyside()
+        elif raise_on_fail:  # pragma: no cover
+            raise RuntimeError('Could not integrate flexx.event loop')
+    
+    def integrate_tornado(self):
+        """ Integrate with tornado.
+        """
+        import tornado
+        loop = tornado.ioloop.IOLoop.instance()
+        self._calllaterfunc = loop.add_callback
+    
+    def integrate_pyqt4(self):  # pragma: no cover
+        """ Integrate with PyQt4.
+        """
+        from PyQt4 import QtCore, QtGui
+        self._integrate_qt(QtCore, QtGui)
+    
+    def integrate_pyside(self):  # pragma: no cover
+        """ Integrate with PySide.
+        """
+        from PySide import QtCore, QtGui
+        self._integrate_qt(QtCore, QtGui)
+    
+    def _integrate_qt(self, QtCore, QtGui):  # pragma: no cover
+        from queue import Queue, Empty
+        
+        class _CallbackEventHandler(QtCore.QObject):
+            
+            def __init__(self):
+                QtCore.QObject.__init__(self)
+                self.queue = Queue()
+            
+            def customEvent(self, event):
+                while True:
+                    try:
+                        callback, args = self.queue.get_nowait()
+                    except Empty:
+                        break
+                    try:
+                        callback(*args)
+                    except Exception as why:
+                        print('callback failed: {}:\n{}'.format(callback, why))
+            
+            def postEventWithCallback(self, callback, *args):
+                self.queue.put((callback, args))
+                QtGui.qApp.postEvent(self, QtCore.QEvent(QtCore.QEvent.User))
+        
+        _callbackEventHandler = _CallbackEventHandler()
+        self._calllaterfunc = _callbackEventHandler.postEventWithCallback
+
+
+loop = Loop()
+loop.integrate(None, False)
diff --git a/flexx/pyscript/functions.py b/flexx/pyscript/functions.py
index 3229be6..4a82520 100644
--- a/flexx/pyscript/functions.py
+++ b/flexx/pyscript/functions.py
@@ -136,8 +136,16 @@ def evaljs(jscode, whitespace=True):
     returns:
         result (str): the last result as a string.
     """
+    
     # Call node
-    res = subprocess.check_output([get_node_exe(), '--use_strict', '-p', '-e', jscode])
+    cmd = [get_node_exe(), '--use_strict', '-p', '-e', jscode]
+    try:
+        res = subprocess.check_output(cmd)
+    except Exception as err:
+        err = str(err)
+        err = err[:200] + '...' if len(err) > 200 else err
+        raise Exception(err)
+    
     # process
     res = res.decode().rstrip()
     if res.endswith('undefined'):
diff --git a/flexx/pyscript/parser0.py b/flexx/pyscript/parser0.py
index 7a11d8b..93f76d2 100644
--- a/flexx/pyscript/parser0.py
+++ b/flexx/pyscript/parser0.py
@@ -135,6 +135,7 @@ class Parser0:
         'None'  : 'null',
         'unicode': 'str',  # legacy Py compat
         'unichr': 'chr',
+        'xrange': 'range',
     }
     
     BINARY_OP = {
diff --git a/flexx/pyscript/parser1.py b/flexx/pyscript/parser1.py
index 2a20399..ef40448 100644
--- a/flexx/pyscript/parser1.py
+++ b/flexx/pyscript/parser1.py
@@ -470,7 +470,7 @@ class Parser1(Parser0):
         if imported:
             return self.use_imported_object(imported + '.' + node.attr)
         # Handle normally
-        return "%s.%s" % (base_name, node.attr)
+        return "%s.%s" % (base_name, self.NAME_MAP.get(node.attr, node.attr))
     
     ## Statements
     
diff --git a/flexx/pyscript/parser2.py b/flexx/pyscript/parser2.py
index 5d3e431..807a456 100644
--- a/flexx/pyscript/parser2.py
+++ b/flexx/pyscript/parser2.py
@@ -476,7 +476,7 @@ class Parser2(Parser1):
         
         elif sure_is_dict:  # Enumeration over an object (i.e. a dict)
             # Create dummy vars
-            d_seq = self.dummy('sequence')
+            d_seq = self.dummy('seq')
             code.append(self.lf('%s = %s;' % (d_seq, iter)))
             # The loop
             code += self.lf(), 'for (', target[0], ' in ', d_seq, ') {'
@@ -498,7 +498,7 @@ class Parser2(Parser1):
             # objects probably slightly less.
             
             # Create dummy vars
-            d_seq = self.dummy('sequence')
+            d_seq = self.dummy('seq')
             d_iter = self.dummy('iter')
             d_target = target[0] if (len(target) == 1) else self.dummy('target')
             
@@ -804,8 +804,8 @@ class Parser2(Parser1):
         
         # Define function that acts as class constructor
         code = []
-        for line in get_class_definition(node.name, base_class, 
-                                         self.pop_docstring(node)):
+        docstring = self.pop_docstring(node) if self._docstrings else ''
+        for line in get_class_definition(node.name, base_class, docstring):
             code.append(self.lf(line))
         
         # Body ...
diff --git a/flexx/pyscript/parser3.py b/flexx/pyscript/parser3.py
index f878127..62ed3c0 100644
--- a/flexx/pyscript/parser3.py
+++ b/flexx/pyscript/parser3.py
@@ -208,7 +208,7 @@ class Parser3(Parser2):
     code, like ``self``, ``print()``, ``len()``, list methods, etc.
     """
     
-    NAME_MAP = {'self': 'this', }
+    NAME_MAP = {'self': 'this', '__class__': 'constructor.prototype'}
     NAME_MAP.update(Parser2.NAME_MAP)
     
     ## Python buildin functions
diff --git a/flexx/pyscript/stdlib.py b/flexx/pyscript/stdlib.py
index e07e68e..339c769 100644
--- a/flexx/pyscript/stdlib.py
+++ b/flexx/pyscript/stdlib.py
@@ -23,7 +23,7 @@ IMPORT_DOT = '__'
 
 
 def get_std_info(code):
-    """ Given the JS code for a std function or method. Determine the
+    """ Given the JS code for a std function or method, determine the
     number of arguments, function_deps and method_deps.
     """
     _, _, nargs = code.splitlines()[0].partition('nargs:')
@@ -83,6 +83,8 @@ def get_full_std_lib(indent=0):
 
 ## Hardcore functions
 
+FUNCTIONS['this_is_js'] = """function () {return true;} // nargs: 0"""
+
 FUNCTIONS['hasattr'] = """function (ob, name) { // nargs: 2
     return (ob !== undefined) && (ob !== null) && (ob[name] !== undefined);
 }"""
@@ -210,7 +212,8 @@ FUNCTIONS['reversed'] = """function (iter) { // nargs: 1
 
 FUNCTIONS['sorted'] = """function (iter, key, reverse) { // nargs: 1 2 3
     if ((typeof iter==="object") && (!Array.isArray(iter))) {iter = Object.keys(iter);}
-    var comp = function (a, b) {return key(a) - key(b);};
+    var comp = function (a, b) {a = key(a); b = key(b);
+        if (a<b) {return -1;} if (a>b) {return 1;} return 0;};
     comp = Boolean(key) ? comp : undefined; 
     iter = iter.slice().sort(comp);
     if (reverse) iter.reverse();
@@ -327,7 +330,8 @@ METHODS['reverse'] = """function () { // nargs: 0
 
 METHODS['sort'] = """function (key, reverse) { // nargs: 0 1 2
     if (!Array.isArray(this)) return this.KEY.apply(this, arguments);
-    var comp = function (a, b) {return key(a) - key(b);};
+    var comp = function (a, b) {a = key(a); b = key(b);
+        if (a<b) {return -1;} if (a>b) {return 1;} return 0;};
     comp = Boolean(key) ? comp : undefined; 
     this.sort(comp);
     if (reverse) this.reverse();
diff --git a/flexx/react/tests/test_both.py b/flexx/react/tests/test_both_react.py
similarity index 100%
rename from flexx/react/tests/test_both.py
rename to flexx/react/tests/test_both_react.py
diff --git a/conda.recipe/meta.yaml b/meta.yaml
similarity index 98%
rename from conda.recipe/meta.yaml
rename to meta.yaml
index 14a68f3..203f619 100644
--- a/conda.recipe/meta.yaml
+++ b/meta.yaml
@@ -12,7 +12,7 @@ source:
 build:
   script: python setup.py install
   noarch_python: True
-  number: 2
+  number: 1
   entry_points:
     - flexx = flexx.__main__:main
 


Test patch:
diff --git a/flexx/event/tests/test_both.py b/flexx/event/tests/test_both.py
new file mode 100644
index 0000000..71e0546
--- /dev/null
+++ b/flexx/event/tests/test_both.py
@@ -0,0 +1,1117 @@
+"""
+Tests that should run in both Python and JS.
+This helps ensure that both implementation work in the same way.
+
+Focus on use-cases rather than coverage.
+
+These tests work a bit awkward, but its very useful to be able to test
+that the two systems work exactly the same way. You define a HasEvents
+class, and then provide that class to a test function using a
+decorator. The test function will then be run both in Python and in JS.
+The test function should return an object, that when evaluated to a
+string matches with the reference string given to the decorator. The
+result string is made lowercase, and double quotes are converted to
+single quotes.
+"""
+
+from flexx.util.testing import run_tests_if_main, raises
+
+from flexx import event
+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
+
+import sys
+from math import isnan as isNaN
+
+def this_is_js():
+    return False
+
+
+def reduce_code(code):
+    # On Windows we can pass up to 2**15 chars
+    # over the command line before getting filename-too-long error.
+    # Doing this gives us just enough to be able to run our tests :)
+    if sys.platform.startswith('win') and len(code) > 2**15:
+        code = code.replace('    ', '')
+        code = code.replace('_pyfunc_', 'pf_').replace('_pymeth_', 'pm_')
+    code = code.replace('    ', '\t')
+    return code
+
+
+def run_in_both(cls, reference, extra_classes=()):
+    """ The test decorator.
+    """
+    
+    if reference.lower() != reference:
+        raise ValueError('Test reference should be lowercase!')
+    
+    def wrapper(func):
+        def runner():
+            # Run in JS
+            code = HasEventsJS.JSCODE
+            for c in cls.mro()[1:]:
+                if c is event.HasEvents:
+                    break
+                code += create_js_hasevents_class(c, c.__name__, c.__bases__[0].__name__+'.prototype')
+            for c in extra_classes:
+                code += create_js_hasevents_class(c, c.__name__)
+            code += create_js_hasevents_class(cls, cls.__name__, cls.__bases__[0].__name__+'.prototype')
+            code += py2js(func, 'test', inline_stdlib=False)
+            code += 'test(%s);' % cls.__name__
+            nargs, function_deps, method_deps = get_std_info(code)
+            code = get_partial_std_lib(function_deps, method_deps, []) + code
+            code = reduce_code(code)
+            jsresult = evaljs(code)
+            jsresult = jsresult.replace('[ ', '[').replace(' ]', ']').replace('\n  ', ' ')
+            jsresult = jsresult.replace('"', "'").split('!!!!')[-1]
+            print('js:', jsresult)
+            # Run in Python
+            pyresult = reprs(func(cls))
+            pyresult = pyresult.replace('"', "'").replace("\\'", "'").split('!!!!')[-1]
+            print('py:', pyresult)
+            #
+            assert pyresult.lower() == reference
+            assert jsresult.lower() == reference
+        return runner
+    return wrapper
+
+
+class Person(event.HasEvents):
+    
+    _foo = 3
+    _bar = 'bar'
+    spam = [1, 2, 3]
+    
+    def __init__(self):
+        self.r1 = []
+        self.r2 = []
+        self.r3 = []
+        self.r4 = []
+        super().__init__()
+    
+    @event.prop
+    def age(self, v=0):
+        v = float(v)
+        assert not isNaN(v)  # JS wont fail but make NaN
+        return v
+    
+    @event.readonly
+    def nchildren(self, v=0):
+        v = int(v)
+        assert not isNaN(v)  # JS wont fail but make NaN
+        return v
+    
+    @event.prop
+    def first_name(self, v='john'):
+        return str(v)
+    
+    @event.prop
+    def last_name(self, v='doe'):
+        return str(v)
+    
+    @event.readonly
+    def full_name(self, v=''):
+        return str(v)
+    
+    @event.emitter
+    def yell(self, v):
+        v = int(v)
+        assert not isNaN(v)  # JS wont fail but make NaN
+        return {'action': 'yell', 'value': v}
+    
+    @event.connect('first_name')
+    def _first_name_logger(self, *events):
+        for ev in events:
+            self.r1.append(ev.old_value + '-' + ev.new_value)
+    
+    @event.connect('full_name')
+    def _full_name_logger(self, *events):
+        for ev in events:
+            self.r2.append(ev.old_value + '-' + ev.new_value)
+    
+    @event.connect('yell')
+    def _yell_logger(self, *events):
+        for ev in events:
+            self.r3.append(ev.value)
+    
+    @event.connect('first_name', 'last_name')
+    def _set_full_name(self, *events):
+        self.r4.append('')
+        self._set_prop('full_name', self.first_name + ' ' + self.last_name)
+
+
+@run_in_both(Person, "['', 'john doe', '', 'almar klein', '', 'jorik klein']")
+def test_name(Person):
+    name = Person()
+    name._set_full_name.handle_now()
+    name.r4.append(name.full_name)
+    name.first_name = 'almar'
+    name.last_name = 'klein'
+    name._set_full_name.handle_now()
+    name.r4.append(name.full_name)
+    name.first_name = 'jorik'
+    name._set_full_name.handle_now()
+    name.r4.append(name.full_name)
+    return name.r4
+
+@run_in_both(Person, "[3, 'bar', [1, 2, 3]]")
+def test_class_attributes(Person):
+    name = Person()
+    return name._foo, name._bar, name.spam
+
+
+## Test prop
+
+@run_in_both(Person, "['john-john', 'john-jane']")
+def test_prop_init(Person):
+    # First_name gets default value
+    name = Person()
+    assert name.first_name == 'john'
+    assert not name.r1
+    name._first_name_logger.handle_now()
+    assert name.r1
+    name.first_name = 'jane'
+    #
+    assert name.first_name == 'jane'
+    name._first_name_logger.handle_now()
+    return name.r1
+
+
+@run_in_both(Person, "[]")
+def test_prop_setting(Person):
+    name = Person()
+    name.first_name = 'john'
+    name._first_name_logger.handle_now()
+    name.r1 = []
+    #
+    name.first_name = 'jane'
+    name._first_name_logger.handle_now()
+    assert len(name.r1) == 1
+    #
+    name.first_name = 'jane'  # same name!
+    name._first_name_logger.handle_now()
+    assert len(name.r1) == 1
+    return []
+
+
+@run_in_both(Person, "[]")
+def test_prop_fail(Person):
+    # Fail in setting prop should raise
+    name = Person()
+    name.age = 99
+    assert name.age == 99
+    try:
+        name.age = 'john'
+    except Exception:
+        return []
+    return ['fail']
+
+
+## Test readonly
+
+@run_in_both(Person, "['-', '-john doe', 'john doe-jane doe']")
+def test_readonly_init(Person):
+    # full name gets default value
+    name = Person()
+    assert name.full_name == ''
+    assert len(name.r2) ==0 
+    name._full_name_logger.handle_now()
+    assert len(name.r2) == 1
+    name._set_prop('full_name', 'john doe')
+    assert len(name.r2) == 1
+    name._full_name_logger.handle_now()
+    assert len(name.r2) == 2
+    #
+    name._set_prop('full_name', 'jane doe')
+    name._full_name_logger.handle_now()
+    return name.r2
+
+
+@run_in_both(Person, "[]")
+def test_readonly_setting(Person):
+    name = Person()
+    name._set_prop('full_name', 'john doe')
+    name._full_name_logger.handle_now()
+    name.r2 = []
+    #
+    name._set_prop('full_name', 'jane doe')
+    name._full_name_logger.handle_now()
+    assert len(name.r2) == 1
+    #
+    name._set_prop('full_name', 'jane doe')  # same name!
+    name._full_name_logger.handle_now()
+    assert len(name.r2) == 1
+    return []
+
+
+@run_in_both(Person, "[]")
+def test_readonly_fail(Person):
+    # Fail in setting readonly should raise
+    name = Person()
+    name._set_prop('nchildren', 3)
+    assert name.nchildren == 3
+    try:
+        name.nchildren = 'john'
+    except Exception:
+        return []
+    return ['fail']
+
+
+## Test emitter
+
+@run_in_both(Person, "[4, 4, 'ok']")
+def test_emitter(Person):
+    # emit
+    name = Person()
+    name._set_prop('nchildren', 3)
+    assert len(name.r3) == 0
+    name.yell(4)
+    name.yell(4)
+    assert len(name.r3) == 0
+    name._yell_logger.handle_now()
+    assert len(name.r3) == 2
+    # fail in emitter
+    try:
+        name.yell('john')
+    except Exception:
+        name.r3.append('ok')
+    return name.r3
+
+
+## Test emiter related
+
+@run_in_both(Person, "['setreadonly', 'setemitter', '_setprop1', '_setprop2']")
+def test_try_illegal_stuff(Person):
+    # we cannot test delete, because deleting *is* possible
+    # on JS
+    res = []
+    name = Person()
+    try:
+        name.full_name = 'john doe'
+    except AttributeError:
+        res.append('setreadonly')
+    try:
+        name.yell = 3
+    except AttributeError:
+        res.append('setemitter')
+    try:
+        name._set_prop(3, 3)  # Property name must be a string
+    except TypeError:
+        res.append('_setprop1')
+    try:
+        name._set_prop('spam', 3)  # MyObject has not spam property
+    except AttributeError:
+        res.append('_setprop2')
+    return res
+
+
+class PropRecursion(event.HasEvents):
+    count = 0
+    
+    @event.prop
+    def foo(self, v=1):
+        v = float(v)
+        self.bar = v + 1
+        return v
+    
+    @event.prop
+    def bar(self, v=2):
+        v = float(v)
+        self.foo = v - 1
+        return v
+    
+    @event.connect('foo', 'bar')
+    def handle(self, *events):
+        self.count += 1
+
+
+@run_in_both(PropRecursion, "[]")
+def test_prop_recursion(PropRecursion):
+    
+    m = PropRecursion()
+    m.handle.handle_now()
+    assert m.count == 1
+    assert m.foo == 1
+    assert m.bar== 2
+    
+    m.foo = 50
+    m.handle.handle_now()
+    assert m.count == 2
+    assert m.foo == 50
+    assert m.bar== 51
+    
+    m.bar = 50
+    m.handle.handle_now()
+    assert m.count == 3
+    assert m.foo == 49
+    assert m.bar== 50
+    return []
+
+
+## Test HasEvents class
+
+@run_in_both(Person, "[3, 'bar', [1, 2, 3]]")
+def test_hasevents_class_attributes(Person):
+    name = Person()
+    return name._foo, name._bar, name.spam
+
+
+@run_in_both(Person, "['yell']")
+def test_hasevents___emitters__(Person):
+    name = Person()
+    return name.__emitters__
+
+@run_in_both(Person, "['age', 'first_name', 'full_name', 'last_name', 'nchildren']")
+def test_hasevents___properties__(Person):
+    name = Person()
+    return name.__properties__
+
+@run_in_both(Person, "['_first_name_logger', '_full_name_logger', '_set_full_name', '_yell_logger']")
+def test_hasevents___handlers__(Person):
+    name = Person()
+    return name.__handlers__
+
+
+@run_in_both(Person, "['age', 'first_name', 'full_name', 'last_name', 'nchildren', 'yell']")
+def test_get_event_types(Person):
+    name = Person()
+    return name.get_event_types()
+
+
+@run_in_both(Person, "[2, 1, 0]")
+def test_get_event_handlers1(Person):
+    name = Person()
+    return [len(name.get_event_handlers('first_name')),
+            len(name.get_event_handlers('full_name')),
+            len(name.get_event_handlers('age')),
+           ]
+
+@run_in_both(Person, "[]")
+def test_get_event_handlers2(Person):
+    
+    def func1(*events):
+        for ev in events:
+            res1.append(ev.new_value)
+    def func2(*events):
+        for ev in events:
+            res2.append(ev.new_value)
+    
+    name = Person()
+    handler1 = name.connect(func1, 'first_name', 'last_name')
+    handler2 = name.connect(func2, 'first_name', 'last_name')
+    handler3 = name.connect(lambda x:None, 'foo')
+    
+    first_handlers = name.get_event_handlers('first_name')
+    assert handler1 in first_handlers
+    assert handler2 in first_handlers
+    assert handler3 not in first_handlers
+    
+    return []
+
+@run_in_both(Person, "['x1-y1', 'x2-y2']")
+def test_emit(Person):
+    name = Person()
+    name._first_name_logger.handle_now()
+    name.r1 = []
+    name.emit('first_name', dict(old_value='x1', new_value='y1'))
+    name.emit('first_name', dict(old_value='x2', new_value='y2'))
+    assert not name.r1
+    name._first_name_logger.handle_now()
+    return name.r1
+
+
+@run_in_both(Person, "['ok-label', 'ok-type']")
+def test_emit_fail(Person):
+    res = []
+    name = Person()
+    try:
+        name.emit('first_name:xx', dict(old_value='x1', new_value='y1'))
+    except ValueError:
+        res.append('ok-label')
+    try:
+        name.emit('first_name', 4)
+    except TypeError:
+        res.append('ok-type')
+    return res
+
+
+@run_in_both(Person, "[]")
+def test_dispose1(Person):
+    name = Person()
+    name.dispose()  # in Py we test cleanup, here we only test if is callable
+    return []
+
+
+@run_in_both(Person, "['jane', 'johnny', 1]")
+def test_connect1(Person):
+    res = []
+    res2 = []
+    def func(*events):
+        res2.append(1)
+        for ev in events:
+            res.append(ev.new_value)
+    
+    name = Person()
+    handler = name.connect(func, 'first_name')
+    assert handler is not func
+    
+    name.first_name = 'jane'
+    name.first_name = 'jane'
+    name.first_name = 'johnny'
+    handler.handle_now()
+    res.append(len(res2))
+    return res
+
+
+@run_in_both(Person, "['jane', 'jansen', 'johnny', 1]")
+def test_connect2(Person):
+    res = []
+    res2 = []
+    def func(*events):
+        res2.append(1)
+        for ev in events:
+            res.append(ev.new_value)
+    
+    name = Person()
+    handler = name.connect(func, 'first_name', 'last_name')
+    assert handler is not func
+    
+    name.first_name = 'jane'
+    name.last_name = 'jansen'
+    name.first_name = 'jane'
+    name.first_name = 'johnny'
+    handler.handle_now()
+    res.append(len(res2))
+    return res
+
+
+@run_in_both(Person, "['jane', 'jansen', '||', 'jane', 'jansen']")
+def test_disconnect_dispose(Person):
+    res1 = []
+    res2 = []
+    
+    def func1(*events):
+        for ev in events:
+            res1.append(ev.new_value)
+    def func2(*events):
+        for ev in events:
+            res2.append(ev.new_value)
+    
+    name = Person()
+    handler1 = name.connect(func1, 'first_name', 'last_name')
+    handler2 = name.connect(func2, 'first_name', 'last_name')
+    
+    name.first_name = 'jane'
+    name.last_name = 'jansen'
+    name.dispose()  # disconnect handler
+    name.first_name = 'xx'
+    name.last_name = 'yy'
+    handler1.handle_now()
+    handler2.handle_now()
+    return res1 + ['||'] + res2
+
+
+@run_in_both(Person, "['jane', 'jansen', '||', 'jane', 'jansen']")
+def test_disconnect1(Person):
+    res1 = []
+    res2 = []
+    
+    def func1(*events):
+        for ev in events:
+            res1.append(ev.new_value)
+    def func2(*events):
+        for ev in events:
+            res2.append(ev.new_value)
+    
+    name = Person()
+    handler1 = name.connect(func1, 'first_name', 'last_name')
+    handler2 = name.connect(func2, 'first_name', 'last_name')
+    
+    name.first_name = 'jane'
+    name.last_name = 'jansen'
+    name.disconnect('first_name')  # disconnect handler
+    name.disconnect('last_name')  # disconnect handler
+    name.first_name = 'xx'
+    name.last_name = 'yy'
+    handler1.handle_now()
+    handler2.handle_now()
+    return res1 + ['||'] + res2
+
+
+
+@run_in_both(Person, "['jane', 'jansen', 'yy', '||', 'jane', 'jansen', 'xx']")
+def test_disconnect2(Person):
+    res1 = []
+    res2 = []
+    
+    def func1(*events):
+        for ev in events:
+            res1.append(ev.new_value)
+    def func2(*events):
+        for ev in events:
+            res2.append(ev.new_value)
+    
+    name = Person()
+    handler1 = name.connect(func1, 'first_name', 'last_name')
+    handler2 = name.connect(func2, 'first_name', 'last_name')
+    
+    name.first_name = 'jane'
+    name.last_name = 'jansen'
+    name.disconnect('first_name', handler1)  # disconnect handler
+    name.disconnect('last_name', handler2)  # disconnect handler
+    name.first_name = 'xx'
+    name.last_name = 'yy'
+    handler1.handle_now()
+    handler2.handle_now()
+    return res1 + ['||'] + res2
+
+
+@run_in_both(Person, "['jane', 'jansen', '||', 'jane', 'jansen', 'xx', 'yy']")
+def test_disconnect3(Person):
+    res1 = []
+    res2 = []
+    
+    def func1(*events):
+        for ev in events:
+            res1.append(ev.new_value)
+    def func2(*events):
+        for ev in events:
+            res2.append(ev.new_value)
+    
+    name = Person()
+    handler1 = name.connect(func1, 'first_name:label1', 'last_name:label1')
+    handler2 = name.connect(func2, 'first_name:label2', 'last_name:label2')
+    
+    name.first_name = 'jane'
+    name.last_name = 'jansen'
+    name.disconnect('first_name:label1')  # disconnect handler
+    name.disconnect('last_name:label1')  # disconnect handler
+    name.first_name = 'xx'
+    name.last_name = 'yy'
+    handler1.handle_now()
+    handler2.handle_now()
+    return res1 + ['||'] + res2
+
+
+class InheritedPerson(Person):
+    
+    @event.prop
+    def foo2(self, v=3):
+        return int(v)
+    
+    @event.connect('foo2')
+    def _foo2_logger(self, *events):
+        for ev in events:
+            self.r1.append(ev.new_value)
+
+
+@run_in_both(InheritedPerson, "['john-john', 'john-jane', 3, 42]")
+def test_inheritance(InheritedPerson):
+    name = InheritedPerson()
+    
+    name.age = 42
+    name.first_name = 'jane'
+    name.foo2 = 42
+    
+    name._first_name_logger.handle_now()
+    name._foo2_logger.handle_now()
+    
+    return name.r1
+
+
+## Test handlers
+
+@run_in_both(Person, "[]")
+def test_event_object_persistence(Person):
+    res1 = []
+    res2 = []
+    
+    def func1(*events):
+        for ev in events:
+            res1.append(ev)
+    def func2(*events):
+        for ev in events:
+            res2.append(ev)
+    
+    name = Person()
+    handler1 = name.connect(func1, 'first_name', 'last_name')
+    handler2 = name.connect(func2, 'first_name', 'last_name')
+    
+    name.first_name = 'jane'
+    name.last_name = 'jansen'
+    handler1.handle_now()
+    handler2.handle_now()
+    
+    assert len(res1) == 2
+    assert len(res2) == 2
+    assert res1[0] is res2[0]
+    assert res1[1] is res2[1]
+    return []
+
+
+@run_in_both(Person, "['jane', 'jansen', 0, 0, 2]")
+def test_handler_calling(Person):
+    res1 = []
+    res2 = []
+    
+    def func1(*events):
+        res2.append(len(events))
+        for ev in events:
+            res1.append(ev.new_value)
+    
+    name = Person()
+    handler1 = name.connect(func1, 'first_name', 'last_name')
+    
+    assert len(res2) == 0
+    handler1()
+    assert len(res2) == 1
+    func1()
+    assert len(res2) == 2
+    name.first_name = 'jane'
+    name.last_name = 'jansen'
+    assert len(res2) == 2
+    handler1.handle_now()
+    assert len(res2) == 3
+    return res1 + res2
+
+
+@run_in_both(Person, "['jane', 'jansen', '||', 'jane', 'jansen', 'zz', 'xx', 'yy']")
+def test_handler_dispose(Person):
+    res1 = []
+    res2 = []
+    
+    def func1(*events):
+        for ev in events:
+            res1.append(ev.new_value)
+    def func2(*events):
+        for ev in events:
+            res2.append(ev.new_value)
+    
+    name = Person()
+    handler1 = name.connect(func1, 'first_name', 'last_name')
+    handler2 = name.connect(func2, 'first_name', 'last_name')
+    
+    name.first_name = 'jane'
+    name.last_name = 'jansen'
+    handler1.handle_now()
+    handler2.handle_now()
+    name.first_name = 'zz'  # event is pended, but discarted when disposed
+    handler1.dispose()
+    name.first_name = 'xx'
+    name.last_name = 'yy'
+    handler1.handle_now()
+    handler2.handle_now()
+    return res1 + ['||'] + res2
+
+
+@run_in_both(Person, "['_first_name_logger']")
+def test_handler_get_name(Person):
+    res1 = []
+    
+    def func1(*events):
+        for ev in events:
+            res1.append(ev.new_value)
+    
+    name = Person()
+    handler1 = name.connect(func1, 'first_name', 'last_name')
+    
+    res1.append(name._first_name_logger.get_name())
+    assert handler1.get_name()  # on JS we cannot know the name
+    
+    return res1
+
+
+@run_in_both(Person, "[['first_name', ['first_name']], ['last_name', ['last_name']]]")
+def test_handler_get_connection_info(Person):
+    res1 = []
+    
+    def func1(*events):
+        for ev in events:
+            res1.append(ev.new_value)
+    
+    name = Person()
+    handler1 = name.connect(func1, 'first_name', 'last_name')
+    
+    return handler1.get_connection_info()
+
+
+class Simple1(event.HasEvents):
+    
+    def __init__(self):
+        super().__init__()
+        self._r1 = []
+    
+    @event.prop
+    def val(self, v=0):
+        return int(v)
+    
+    def on_val(self, *events):
+        for ev in events:
+            self._r1.append(ev.new_value)
+
+@run_in_both(Simple1, "[0, 7, 42]")
+def test_handler_on_method(Simple1):
+    
+    s = Simple1()
+    s.val = 7
+    s.val = 42
+    for h in s.get_event_handlers('val'):
+        h.handle_now()
+    return s._r1
+
+
+class Order1(event.HasEvents):
+    
+    def __init__(self):
+        super().__init__()
+        self.r = []
+    
+    @event.connect('foo')
+    def bbb(self, *events):
+        for ev in events:
+            self.r.append('b')
+    
+    @event.connect('foo')
+    def mmm(self, *events):
+        for ev in events:
+            self.r.append('m')
+    
+    @event.connect('foo:aaa')
+    def ppp(self, *events):
+        for ev in events:
+            self.r.append('p')
+    
+    @event.connect('foo:ddd')
+    def www(self, *events):
+        for ev in events:
+            self.r.append('w')
+
+@run_in_both(Order1, "['m', 'p', 'b', 'w']")
+def test_event_order1(Order1):
+    # Hah! we determine order simply by using handle_now()
+    m = Order1()
+    m.emit('foo', {})
+    
+    m.mmm.handle_now()
+    m.ppp.handle_now()
+    m.bbb.handle_now()
+    m.www.handle_now()
+    
+    return m.r
+
+@run_in_both(Order1, "['p', 'b', 'w', 'm']")
+def test_event_order2(Order1):
+    
+    # monkey patch JS event system
+    if this_is_js():
+        pending_funcs = []
+        def setTimeout(func):
+            pending_funcs.append(func)
+        root.setTimeout = setTimeout
+    
+    m = Order1()
+    m.emit('foo', {})
+    
+    if this_is_js():
+        for func in pending_funcs:
+            func()
+    else:
+        event.loop.iter()
+    
+    return m.r
+
+@run_in_both(Order1, "['p', 'b', '!', 'w', 'm']")
+def test_event_order3(Order1):
+    
+    # monkey patch JS event system
+    if this_is_js():
+        pending_funcs = []
+        def setTimeout(func):
+            pending_funcs.append(func)
+        root.setTimeout = setTimeout
+    
+    m = Order1()
+    def myfunc(*events):
+        for ev in events:
+            m.r.append('!')
+    m.connect(myfunc, 'foo:ccc')
+    m.emit('foo', {})
+    
+    if this_is_js():
+        for func in pending_funcs:
+            func()
+    else:
+        event.loop.iter()
+    
+    return m.r
+
+
+class ConnectFail(event.HasEvents):
+    
+    @event.connect('foo.bar')
+    def failing_handler(self, *events):
+        pass
+
+@run_in_both(ConnectFail, "['ok']")
+def test_handler_connection_fail1(ConnectFail):
+    
+    try:
+        m = ConnectFail()
+    except RuntimeError:
+        return ['ok']
+    return ['fail']
+
+
+@run_in_both(Person, "['ok']")
+def test_handler_connection_fail2(Person):
+    
+    name = Person()
+    def handler(*events):
+        pass
+    
+    try:
+        name.connect(handler, 'foo.bar')
+    except RuntimeError:
+        return ['ok']
+    return ['fail']
+
+
+class HandlerFail(event.HasEvents):
+    
+    @event.prop
+    def foo(self, v=0):
+        return int(v)
+        
+    @event.connect('foo')
+    def failing_handler(self, *events):
+        raise IndexError('bla')
+
+
+@run_in_both(HandlerFail, "', 1, 1]")
+def test_handler_exception1(HandlerFail):
+    res = []
+    
+    m = HandlerFail()
+    def handler(*events):
+        raise IndexError('bla')
+    handler = m.connect(handler, 'foo')
+    
+    # Does not fail when triggered
+    m.foo = 3
+    m.foo = 42
+    m.failing_handler.handle_now()
+    handler.handle_now()
+    
+    # But calling directly fails
+    try:
+        m.failing_handler()
+    except IndexError:
+        res.append(1)
+    try:
+        handler()
+    except IndexError:
+        res.append(1)
+    
+    return ["!!!!"] + res  # trick to ditch stderr in JS
+
+
+## Dynamism
+
+class Node(event.HasEvents):
+    
+    def __init__(self):
+        super().__init__()
+        self._r1 = []
+        self._r2 = []
+    
+    @event.prop
+    def val(self, v=0):
+        return int(v)
+    
+    @event.prop
+    def parent(self, v=None):
+        return v
+    
+    @event.prop
+    def children(self, v=()):
+        return tuple(v)
+    
+    @event.connect('parent.val')
+    def handle_parent_val(self, *events):
+        for ev in events:
+            if self.parent:
+                self._r1.append(self.parent.val)
+            else:
+                self._r1.append(None)
+    
+    @event.connect('children.*.val')
+    def handle_children_val(self, *events):
+        for ev in events:
+            if isinstance(ev.new_value, (int, float)):
+                self._r2.append(ev.new_value)
+            else:
+                self._r2.append('null')
+
+
+@run_in_both(Node, "[0, 0, 17, 18, 28, 29, null]")
+def test_dynamism1(Node):
+    n = Node()
+    n1 = Node()
+    n2 = Node()
+    
+    n.parent = n1
+    n.val = 42
+    n.handle_parent_val.handle_now()
+    n1.val = 17
+    n2.val = 27
+    n.handle_parent_val.handle_now()
+    n1.val = 18
+    n2.val = 28
+    n.handle_parent_val.handle_now()
+    n.parent = n2
+    n.handle_parent_val.handle_now()
+    n1.val = 19
+    n2.val = 29
+    n.handle_parent_val.handle_now()
+    n.parent = None
+    n.handle_parent_val.handle_now()
+    n1.val = 11
+    n2.val = 21
+    n.handle_parent_val.handle_now()
+    return n._r1
+
+
+@run_in_both(Node, "[0, 17, 18, 28, 29, null]")
+def test_dynamism2(Node):
+    n = Node()
+    n1 = Node()
+    n2 = Node()
+    
+    res = []
+    
+    def func(*events):
+        for ev in events:
+            if n.parent:
+                res.append(n.parent.val)
+            else:
+                res.append(None)
+    handler = n.connect(func, 'parent.val')
+    
+    n.parent = n1
+    n.val = 42
+    handler.handle_now()
+    n1.val = 17
+    n2.val = 27
+    handler.handle_now()
+    n1.val = 18
+    n2.val = 28
+    handler.handle_now()
+    n.parent = n2
+    handler.handle_now()
+    n1.val = 19
+    n2.val = 29
+    handler.handle_now()
+    n.parent = None
+    handler.handle_now()
+    n1.val = 11
+    n2.val = 21
+    handler.handle_now()
+    return res
+
+
+@run_in_both(Node, "['null', 'null', 17, 27, 18, 28, 'null', 29, 'null']")
+def test_dynamism3(Node):
+    n = Node()
+    n1 = Node()
+    n2 = Node()
+    
+    n.children = n1, n2
+    n.val = 42
+    n.handle_children_val.handle_now()
+    n1.val = 17
+    n2.val = 27
+    n.handle_children_val.handle_now()
+    n1.val = 18
+    n2.val = 28
+    n.handle_children_val.handle_now()
+    n.children = (n2, )
+    n.handle_children_val.handle_now()
+    n1.val = 19
+    n2.val = 29
+    n.handle_children_val.handle_now()
+    n.children = ()
+    n.handle_children_val.handle_now()
+    n1.val = 11
+    n2.val = 21
+    n.handle_children_val.handle_now()
+    return n._r2
+
+
+@run_in_both(Node, "['null', 17, 27, 18, 28, 'null', 29, 'null']")
+def test_dynamism4(Node):
+    n = Node()
+    n1 = Node()
+    n2 = Node()
+    
+    res = []
+    
+    def func(*events):
+        for ev in events:
+            if isinstance(ev.new_value, (float, int)):
+                res.append(ev.new_value)
+            else:
+                res.append('null')
+    handler = n.connect(func, 'children.*.val')
+    
+    n.children = n1, n2
+    n.val = 42
+    handler.handle_now()
+    n1.val = 17
+    n2.val = 27
+    handler.handle_now()
+    n1.val = 18
+    n2.val = 28
+    handler.handle_now()
+    n.children = (n2, )
+    handler.handle_now()
+    n1.val = 19
+    n2.val = 29
+    handler.handle_now()
+    n.children = ()
+    handler.handle_now()
+    n1.val = 11
+    n2.val = 21
+    handler.handle_now()
+    return res
+
+
+@run_in_both(Node, "[17, 18, 19]")
+def test_dynamism5(Node):
+    # connection strings with static attributes - no reconnect
+    n = Node()
+    n1 = Node()
+    n.foo = n1
+    
+    res = []
+    def func(*events):
+        for ev in events:
+            if isinstance(ev.new_value, (float, int)):
+                res.append(ev.new_value)
+            else:
+                res.append('null')
+    handler = n.connect(func, 'foo.val')
+    
+    n.val = 42
+    handler.handle_now()
+    n1.val = 17
+    n1.val = 18
+    handler.handle_now()
+    n.foo = None  # no reconnect in this case
+    handler.handle_now()
+    n1.val = 19
+    handler.handle_now()
+    return res
+
+
+run_tests_if_main()
diff --git a/flexx/event/tests/test_dict.py b/flexx/event/tests/test_dict.py
new file mode 100644
index 0000000..1f744e5
--- /dev/null
+++ b/flexx/event/tests/test_dict.py
@@ -0,0 +1,86 @@
+
+from flexx.util.testing import run_tests_if_main, skipif, skip, raises
+
+from flexx import event
+from flexx.event._dict import isidentifier
+
+
+def test_isidentifier():
+    
+    for name in ['foo', 'bar', 'asdasdaskjdbf', 'Bar', 'FOO',  # simple
+                 'fóó', 'é', 'élé',  # unicode
+                 '_', '_1', '_foo', 'f_', 'k_k',  # with underscore
+                 'f1', 'x0ff',  # with numbers
+                 ]: 
+        assert isidentifier(name)
+    
+    for name in ['', '*', '(', '^',  'foo,', 'b&b',  # empty string and strange chars
+                 ' ', ' 12', '', 'k k', ' foo', 'foo ', '\tf', 'f\t', ' _', # with whitespace
+                 '2', '42', '2foo', '2_', '123', '0xff',  # numbers
+                 3, None, [],  # not a string
+                 ]:
+        assert not isidentifier(name)
+    
+
+def test_dict_ok():
+    
+    d = event.Dict(foo=3)
+    assert d.foo == 3
+    assert d['foo'] == 3
+    
+    d.foo = 4
+    assert d.foo == 4
+    assert d['foo'] == 4
+    
+    d['foo'] = 5
+    assert d.foo == 5
+    assert d['foo'] == 5
+    
+    d._x = 9
+    assert d._x == 9
+    
+    d.__x = 8
+    assert d.__x == 8
+    
+    d.x0123 = 7
+    assert d.x0123 == 7
+
+
+def test_dict_keys_that_are_methods():
+
+    d = event.Dict(foo=3)
+    
+    with raises(AttributeError):
+        d.copy = 3
+    d['copy'] = 3
+    assert d.copy != 3
+    assert d['copy'] == 3
+
+
+def test_dir_and_repr():
+    
+    d = event.Dict(foo=3, bar=4)
+    d.spam = 5
+    d.eggs = 6
+    
+    names = dir(d)
+    r = repr(d)
+    for name in ['foo', 'bar', 'spam', 'eggs']:
+        assert name in names
+        assert ('%s=' % name) in r
+    
+    # Add a non-identifier element
+    d[42] = None
+    
+    names = dir(d)
+    r = repr(d)
+    for name in ['foo', 'bar', 'spam', 'eggs']:
+        assert name in names
+        assert ('%s=' % name) in r
+    
+    assert '(42, None)' in r
+    assert '42' not in names
+    assert 42 not in names
+
+
+run_tests_if_main()
diff --git a/flexx/event/tests/test_emitters.py b/flexx/event/tests/test_emitters.py
new file mode 100644
index 0000000..8e53f3f
--- /dev/null
+++ b/flexx/event/tests/test_emitters.py
@@ -0,0 +1,267 @@
+""" Basic tests for emitters. Does not contain an awefull extensive
+test suite, as we test emitters quite well in test_both.py.
+"""
+
+from flexx.util.testing import run_tests_if_main, skipif, skip, raises
+
+from flexx import event
+
+
+def test_property():
+    
+    class MyObject(event.HasEvents):
+        
+        @event.prop
+        def foo(self, v=1.2):
+            return float(v)
+        
+        @event.prop
+        def bar(self, v=1.3):
+            return float(v)
+    
+    m = MyObject()
+    assert m.foo == 1.2
+    assert m.bar == 1.3
+    
+    m = MyObject(foo=3)
+    assert m.foo == 3.0
+    
+    m.foo = 5.1
+    m.bar = 5.1
+    assert m.foo == 5.1
+    assert m.bar == 5.1
+    
+    m.foo = '9.3'
+    assert m.foo == 9.3
+    
+    m = MyObject(foo=3)
+    assert m.foo == 3.0
+    
+    # Hacky, but works
+    def foo():
+        pass
+    x = event.prop(foo)
+    assert 'foo' in repr(x)
+    
+    spam = lambda x:None
+    x = event.prop(spam)
+    assert '<lambda>' in repr(x)
+    
+    # fails
+    
+    with raises(ValueError):
+        m.foo = 'bla'
+    
+    with raises(ValueError):
+        MyObject(foo='bla')
+    
+    with raises(TypeError):
+        m._set_prop(3, 3)  # Property name must be a string
+        
+    with raises(AttributeError):
+        m._set_prop('spam', 3)  # MyObject has not spam property
+    
+    with raises(AttributeError):
+        MyObject(spam='bla')  # MyObject has not spam property
+    
+    with raises(TypeError):
+        event.prop(3)  # prop decorator needs callable
+    
+    with raises(AttributeError):
+        del m.foo  # cannot delete a property
+    
+    class MyObject2(event.HasEvents):
+        
+        @event.prop
+        def foo(self, v):
+            return float(v)
+    
+    with raises(RuntimeError):
+        MyObject2()  # no default value for foo
+
+
+def test_prop_recursion():
+    class MyObject(event.HasEvents):
+        count = 0
+        
+        @event.prop
+        def foo(self, v=1):
+            v = float(v)
+            self.bar = v + 1
+            return v
+        
+        @event.prop
+        def bar(self, v=2):
+            v = float(v)
+            self.foo = v - 1
+            return v
+        
+        @event.connect('foo', 'bar')
+        def handle(self, *events):
+            self.count += 1
+    
+    m = MyObject()
+    event.loop.iter()
+    assert m.count == 1
+    assert m.foo == 1
+    assert m.bar== 2
+    
+    m = MyObject(foo=3)
+    event.loop.iter()
+    assert m.count == 1
+    assert m.foo == 3
+    assert m.bar== 4
+    
+    m.foo = 50
+    event.loop.iter()
+    assert m.count == 2
+    assert m.foo == 50
+    assert m.bar== 51
+    
+    m.bar = 50
+    event.loop.iter()
+    assert m.count == 3
+    assert m.foo == 49
+    assert m.bar== 50
+
+
+def test_prop_init():
+    class MyObject(event.HasEvents):
+        
+        @event.prop
+        def foo(self, v=1):
+            return float(v)
+        
+        @event.connect('foo')
+        def foo_handler(self, *events):
+            pass
+    
+    m = MyObject()
+    assert len(m.foo_handler._pending) == 1
+    m.foo = 2
+    m.foo = 3
+    assert len(m.foo_handler._pending) == 3
+    m.foo = 3
+    assert len(m.foo_handler._pending) == 3
+    
+    # Specifying the value in the init will result
+    # in two events, one for the default and one for the initial value
+    m = MyObject(foo=9)
+    assert len(m.foo_handler._pending) == 2
+    m.foo = 2
+    m.foo = 3
+    assert len(m.foo_handler._pending) == 4
+    m.foo = 3
+    assert len(m.foo_handler._pending) == 4
+    
+
+def test_readonly():
+    class MyObject(event.HasEvents):
+        
+        @event.readonly
+        def foo(self, v=1.2):
+            return float(v)
+    
+    m = MyObject()
+    assert m.foo == 1.2
+    
+    
+    m._set_prop('foo', 5.1)
+    assert m.foo == 5.1
+    
+    m._set_prop('foo', '9.3')
+    assert m.foo == 9.3
+    
+    # fails
+    
+    with raises(AttributeError):
+        m.foo = 3.1
+    
+    with raises(AttributeError):
+        m.foo = 'bla'
+    
+    with raises(AttributeError):
+        MyObject(foo=3.2)
+    
+    with raises(TypeError):
+        event.readonly(3)  # readonly decorator needs callable
+    
+    with raises(AttributeError):
+        del m.foo  # cannot delete a readonly
+        
+    class MyObject2(event.HasEvents):
+        
+        @event.readonly
+        def foo(self, v):
+            return float(v)
+    
+    with raises(RuntimeError):
+        MyObject2()  # no default value for foo
+
+
+def test_emitter():
+    
+    class MyObject(event.HasEvents):
+        
+        @event.emitter
+        def foo(self, v):
+            return dict(value=float(v))
+        
+        @event.emitter
+        def bar(self, v):
+            return dict(value=float(v)+1)  # note plus 1
+        
+        def on_foo(self, *events):
+            self.the_val = events[0].value  # so we can test it
+        
+        def on_bar(self, *events):
+            self.the_val = events[0].value  # so we can test it
+    
+    m = MyObject()
+    
+    the_vals = []
+    @m.connect('foo', 'bar')
+    def handle_foo(*events):
+        the_vals.append(events[0].value)
+    
+    with event.loop:
+        m.foo(3.2)
+    assert m.the_val == 3.2
+    assert the_vals[-1] == 3.2
+    
+    with event.loop:
+        m.foo('9.1')
+    assert m.the_val == 9.1
+    assert the_vals[-1] == 9.1
+    
+    with event.loop:
+        m.bar(3.2)
+    assert m.the_val == 4.2
+    assert the_vals[-1] == 4.2
+    
+    # Fail
+    
+    with raises(ValueError):
+        m.foo('bla')
+    
+    with raises(TypeError):
+        event.emitter(3)  # emitter decorator needs callable
+    
+    with raises(AttributeError):
+        del m.foo  # cannot delete an emitter
+    
+    with raises(AttributeError):
+        m.foo = None  # cannot set an emitter
+    
+    class MyObject2(event.HasEvents):
+        
+        @event.emitter
+        def foo(self, v):
+            return float(v)
+    
+    with raises(TypeError):
+        m = MyObject2()
+        m.foo(3.2)  # return value of emitter must be a dict
+
+
+run_tests_if_main()
diff --git a/flexx/event/tests/test_handlers.py b/flexx/event/tests/test_handlers.py
new file mode 100644
index 0000000..0251eea
--- /dev/null
+++ b/flexx/event/tests/test_handlers.py
@@ -0,0 +1,323 @@
+""" Tests for the bare handlers.
+
+Does not test dynamism currently, as we will test that extensively in
+test_both.
+"""
+
+import gc
+import sys
+import weakref
+
+from flexx.util.testing import run_tests_if_main, skipif, skip, raises
+
+from flexx import event
+
+
+def test_method_handlers():
+    
+    events1 = []
+    events2 = []
+    class Foo(event.HasEvents):
+        
+        @event.connect('x1')
+        def handle1(self, *events):
+            events1.extend(events)
+        
+        @event.connect('x1', 'x2')
+        def handle2(self, *events):
+            events2.extend(events)
+    
+    foo = Foo()
+    with event.loop:
+        foo.emit('x1', {})
+        foo.emit('x2', {})
+    
+    assert len(events1) == 1
+    assert len(events2) == 2
+    
+    assert isinstance(foo.handle1, event._handler.Handler)
+    assert repr(foo.handle1)
+    assert hasattr(foo.handle1, 'dispose')
+    assert foo.handle1.get_name() == 'handle1'
+    assert foo.handle1.get_connection_info() == [('x1', ['x1', ])]
+    assert foo.handle2.get_connection_info() == [('x1', ['x1', ]), ('x2', ['x2', ])]
+    
+    # Can't touch this
+    with raises(AttributeError):
+        foo.handle1 = 3
+    with raises(AttributeError):
+        del foo.handle1
+
+
+def test_method_handlers_nodecorator():
+    
+    events1 = []
+    events2 = []
+    class Foo(event.HasEvents):
+        
+        def _handle1(self, *events):
+            events1.extend(events)
+        handle1 = event.connect(_handle1, 'x1')
+        
+        def _handle2(self, *events):
+            events2.extend(events)
+        handle2 = event.connect(_handle2, 'x1', 'x2')
+    
+    foo = Foo()
+    with event.loop:
+        foo.emit('x1', {})
+        foo.emit('x2', {})
+    
+    assert len(events1) == 1
+    assert len(events2) == 2
+    
+    assert isinstance(foo.handle1, event._handler.Handler)
+    assert repr(foo.handle1)
+    assert hasattr(foo.handle1, 'dispose')
+    assert foo.handle1.get_name() == '_handle1'  # note the name ...
+    
+    # Can't touch this
+    with raises(AttributeError):
+        foo.handle1 = 3
+    with raises(AttributeError):
+        del foo.handle1
+
+
+def test_func_handlers():
+    
+    events1 = []
+    events2 = []
+    class Foo(event.HasEvents):
+        pass
+    
+    foo = Foo()
+    
+    @foo.connect('x1')
+    def handle1(*events):
+        events1.extend(events)
+    
+    @foo.connect('x1', 'x2')
+    def handle2(*events):
+        events2.extend(events)
+    
+    with event.loop:
+        foo.emit('x1', {})
+        foo.emit('x2', {})
+    
+    assert len(events1) == 1
+    assert len(events2) == 2
+    
+    assert isinstance(handle1, event._handler.Handler)
+    assert repr(handle1)
+    assert hasattr(handle1, 'dispose')
+    assert handle1.get_name() == 'handle1'
+    assert handle1.get_connection_info() == [('x1', ['x1', ])]
+    assert handle2.get_connection_info() == [('x1', ['x1', ]), ('x2', ['x2', ])]
+
+
+def test_func_handlers_nodecorator():
+    
+    events1 = []
+    events2 = []
+    class Foo(event.HasEvents):
+        pass
+    
+    foo = Foo()
+    
+    def _handle1(*events):
+        events1.extend(events)
+    handle1 = foo.connect(_handle1, 'x1')
+    
+    def _handle2(*events):
+        events2.extend(events)
+    handle2 = foo.connect(_handle2, 'x1', 'x2')
+    
+    with event.loop:
+        foo.emit('x1', {})
+        foo.emit('x2', {})
+    
+    assert len(events1) == 1
+    assert len(events2) == 2
+    
+    assert isinstance(handle1, event._handler.Handler)
+    assert repr(handle1)
+    assert hasattr(handle1, 'dispose')
+    assert handle1.get_name() == '_handle1'  # note the name
+
+
+def test_func_handlers_with_method_decorator():
+
+    @event.connect('x')
+    def foo(*events):
+        pass
+    
+    assert isinstance(foo, event._handler.HandlerDescriptor)
+    assert repr(foo)
+    # too bad we cannot raise an error when a function in decorated this way
+
+
+def test_method_handler_invoking():
+    called = []
+    
+    class MyObject(event.HasEvents):
+        
+        @event.connect('x1', 'x2')
+        def handler(*events):
+            called.append(len(events))
+        
+        @event.connect('x3')
+        def handler3(self, *events):
+            called.append(len(events))
+    
+    h = MyObject()
+    handler = h.handler
+    
+    handler()
+    handler.handle_now()
+    
+    h.emit('x1', {})
+    handler.handle_now()
+    
+    h.emit('x1', {})
+    h.emit('x1', {})
+    handler.handle_now()
+    
+    h.emit('x1', {})
+    h.emit('x2', {})
+    handler.handle_now()
+    
+    handler()
+    handler.handle_now()
+    
+    h.handler3()
+    handler.handle_now()
+    
+    assert called == [0, 1, 2, 2, 0, 0]
+
+
+def test_func_handler_invoking():
+    called = []
+    
+    h = event.HasEvents()
+    
+    @h.connect('x1', 'x2')
+    def handler(*events):
+        called.append(len(events))
+    
+    handler()
+    handler.handle_now()
+    
+    h.emit('x1', {})
+    handler.handle_now()
+    
+    h.emit('x1', {})
+    h.emit('x1', {})
+    handler.handle_now()
+    
+    h.emit('x1', {})
+    h.emit('x2', {})
+    handler.handle_now()
+    
+    handler()
+    handler.handle_now()
+    
+    assert called == [0, 1, 2, 2, 0]
+
+
+def test_connecting():
+    
+    # We've done all the normal connections. We test mosly fails here
+    
+    h = event.HasEvents()
+    
+    with raises(RuntimeError):  # connect() needs args
+        @event.connect
+        def foo(*events):
+            pass
+    with raises(RuntimeError):
+        @h.connect
+        def foo(*events):
+            pass
+    
+    with raises(ValueError):  # connect() needs strings
+        @event.connect(3)
+        def foo(*events):
+            pass
+    with raises(ValueError):
+        @h.connect(3)
+        def foo(*events):
+            pass
+    
+    with raises(TypeError):  # connect() needs callable
+        event.connect('x')(3)
+    with raises(TypeError):  # connect() needs callable
+        h.connect('x')(3)
+    
+    with raises(RuntimeError):  # cannot connect
+        h.xx = None
+        @h.connect('xx.foobar')
+        def foo(*events):
+            pass
+
+
+def test_exceptions():
+    h = event.HasEvents()
+    
+    @h.connect('foo')
+    def handle_foo(*events):
+        1/0
+    
+    h.emit('foo', {})
+    
+    sys.last_traceback = None
+    assert sys.last_traceback is None
+    
+    # No exception should be thrown here
+    event.loop.iter()
+    event.loop.iter()
+    
+    # But we should have prepared for PM debugging
+    assert sys.last_traceback
+    
+    # Its different for a direct call
+    with raises(ZeroDivisionError):
+        handle_foo()
+
+
+def test_dispose1():
+    
+    h = event.HasEvents()
+    
+    @h.connect('x1', 'x2')
+    def handler(*events):
+        pass
+    
+    handler_ref = weakref.ref(handler)
+    del handler
+    gc.collect()
+    assert handler_ref() is not None  # h is holding on
+    
+    handler_ref().dispose()
+    gc.collect()
+    assert handler_ref() is None
+
+
+def test_dispose2():
+    
+    h = event.HasEvents()
+    
+    @h.connect('x1', 'x2')
+    def handler(*events):
+        pass
+    
+    handler_ref = weakref.ref(handler)
+    del handler
+    gc.collect()
+    assert handler_ref() is not None  # h is holding on
+    
+    h.dispose()  # <=== only this line is different from test_dispose1()
+    gc.collect()
+    assert handler_ref() is None
+
+
+run_tests_if_main()
diff --git a/flexx/event/tests/test_hasevents.py b/flexx/event/tests/test_hasevents.py
new file mode 100644
index 0000000..a72fafe
--- /dev/null
+++ b/flexx/event/tests/test_hasevents.py
@@ -0,0 +1,352 @@
+""" Tests for the bare HasEvents class.
+"""
+
+import gc
+import sys
+import weakref
+
+from flexx.util.testing import run_tests_if_main, skipif, skip, raises
+
+from flexx import event
+
+
+def test_basics():
+    
+    # Simplest
+    h = event.HasEvents()
+    assert not h.__handlers__
+    assert not h.__emitters__
+    
+    # Test __emitters__
+    class Foo(event.HasEvents):
+        @event.prop
+        def a_prop(self, v=0):
+            return v
+        @event.readonly
+        def a_readonly(self, v=0):
+            return v
+        @event.emitter
+        def a_emitter(self, v):
+            return {}
+        @event.emitter  # deliberately define it twice
+        def a_emitter(self, v):
+            return {'x':1}
+    #
+    foo = Foo()
+    assert not foo.__handlers__
+    assert len(foo.__emitters__) == 1
+    assert len(foo.__properties__) == 2
+    assert 'a_prop' in foo.__properties__
+    assert 'a_readonly' in foo.__properties__
+    assert 'a_emitter' in foo.__emitters__
+    #
+    assert foo.get_event_types() == ['a_emitter', 'a_prop', 'a_readonly']
+    assert foo.get_event_handlers('a_prop') == []
+    
+    # Test __handlers__
+    class Bar(event.HasEvents):
+        @event.connect('x')
+        def spam(self, *events):
+            pass
+        def on_eggs(self, *events):
+            pass
+    #
+    foo = Bar()
+    assert not foo.__emitters__
+    #assert len(foo.__handlers__) == 2
+    assert len(foo.__handlers__) == 1
+    assert 'spam' in foo.__handlers__
+    #assert 'eggs' in foo.__handlers__
+    #
+    assert foo.get_event_types() == ['eggs', 'x']
+    assert foo.get_event_handlers('x') == [foo.spam]
+
+
+def test_get_event_handlers():
+    class Foo(event.HasEvents):
+        @event.connect('x')
+        def spam(self, *events):
+            pass
+        @event.connect('x')
+        def eggs(self, *events):
+            pass
+    foo = Foo()
+    @foo.connect('x')
+    def bar(*events):
+        pass
+    
+    # sorted by label name
+    assert foo.get_event_handlers('x') == [bar, foo.eggs, foo.spam]
+    
+    @foo.connect('x')
+    def zz1(*events):
+        pass
+    @foo.connect('x:a')
+    def zz2(*events):
+        pass
+    
+    # sorted by label name
+    assert foo.get_event_handlers('x') == [zz2, bar, foo.eggs, foo.spam, zz1]
+    
+    # Nonexisting event type is ok
+    assert foo.get_event_handlers('y') == []
+    
+    # No labels allowed
+    with raises(ValueError):
+        foo.get_event_handlers('x:a')
+
+
+def test_collect_classes():
+    skip('we did not go the metaclass way')  # Not if we don't use the meta class
+    class Foo123(event.HasEvents):
+        pass
+    class Bar456(event.HasEvents):
+        pass
+    
+    assert Foo123 in event.HasEvents.CLASSES
+    assert Bar456 in event.HasEvents.CLASSES
+
+
+def test_emit():
+    
+    h = event.HasEvents()
+    events = []
+    @h.connect('foo', 'bar')
+    def handler(*evts):
+        events.extend(evts)
+    
+    h.emit('foo', {})
+    h.emit('bar', {'x': 1, 'y': 2})
+    h.emit('spam', {})  # not registered
+    handler.handle_now()
+    
+    assert len(events) == 2
+    for ev in events:
+        assert isinstance(ev, dict)
+        assert ev.source is h
+    assert len(events[0]) == 2
+    assert len(events[1]) == 4
+    assert events[0].type == 'foo'
+    assert events[1].type == 'bar'
+    assert events[1].x == 1
+    assert events[1]['y'] == 2
+    
+    # Fail
+    with raises(ValueError):
+        h.emit('foo:a', {})
+    with raises(TypeError):
+        h.emit('foo', 4)
+    with raises(TypeError):
+        h.emit('foo', 'bla')
+
+
+def test_registering_handlers():
+    h = event.HasEvents()
+    
+    @h.connect('foo')
+    def handler1(*evts):
+        events.extend(evts)
+    @h.connect('foo')
+    def handler2(*evts):
+        events.extend(evts)
+    @h.connect('foo')
+    def handler3(*evts):
+        events.extend(evts)
+    handler1.dispose()
+    handler2.dispose()
+    handler3.dispose()
+    
+    # Checks before we start
+    assert h.get_event_types() == ['foo']
+    assert h.get_event_handlers('foo') == []
+    
+    # Test adding handlers
+    h._register_handler('foo', handler1)
+    h._register_handler('foo:a', handler2)
+    h._register_handler('foo:z', handler3)
+    assert h.get_event_handlers('foo') == [handler2, handler1, handler3]
+    
+    # Unregestering one handler
+    h.disconnect('foo', handler2)
+    assert h.get_event_handlers('foo') == [handler1, handler3]
+    
+    # Reset
+    h._register_handler('foo:a', handler2)
+    assert h.get_event_handlers('foo') == [handler2, handler1, handler3]
+    
+    # Unregestering one handler + invalid label -> no unregister
+    h.disconnect('foo:xx', handler2)
+    assert h.get_event_handlers('foo') == [handler2, handler1, handler3]
+    
+    # Reset
+    assert h.get_event_handlers('foo') == [handler2, handler1, handler3]
+    
+    # Unregestering one handler by label
+    h.disconnect('foo:a')
+    assert h.get_event_handlers('foo') == [handler1, handler3]
+    
+    # Reset
+    h._register_handler('foo:a', handler2)
+    assert h.get_event_handlers('foo') == [handler2, handler1, handler3]
+    
+    # Unregestering by type
+    h.disconnect('foo')
+    assert h.get_event_handlers('foo') == []
+
+
+## Disposing ...
+
+def test_disposing_method_handler1():
+    """ handlers on object don't need cleaning. """ 
+    
+    class Foo(event.HasEvents):
+        @event.connect('xx')
+        def handle_xx(self, *events):
+            pass
+    
+    foo = Foo()
+    assert foo.get_event_handlers('xx')
+    foo_ref = weakref.ref(foo)
+    
+    del foo
+    gc.collect()
+    assert foo_ref() is None
+
+
+def test_disposing_method_handler2():
+    """ handlers on object don't need cleaning but pending events need purge. """ 
+    
+    class Foo(event.HasEvents):
+        @event.connect('xx')
+        def handle_xx(self, *events):
+            pass
+    
+    foo = Foo()
+    assert foo.get_event_handlers('xx')
+    foo.emit('xx', {})  # <---------
+    foo_ref = weakref.ref(foo)
+    
+    del foo
+    gc.collect()
+    assert foo_ref() is not None  # pending event
+    
+    event.loop.iter()
+    gc.collect()
+    assert foo_ref() is None
+
+
+def test_disposing_method_handler1():
+    """ can call dispose with handlers on object. """ 
+    
+    class Foo(event.HasEvents):
+        @event.connect('xx')
+        def handle_xx(self, *events):
+            pass
+    
+    foo = Foo()
+    assert foo.get_event_handlers('xx')
+    foo_ref = weakref.ref(foo)
+    foo.dispose()  # <----
+    
+    del foo
+    gc.collect()
+    assert foo_ref() is None
+    
+
+def test_disposing_handler2():
+    """ handlers outside object need cleaning. """ 
+    
+    def _():
+        class Foo(event.HasEvents):
+            pass
+        foo = Foo()
+        @foo.connect('xx')
+        def handle_xx(*events):
+            pass
+        return foo
+    
+    foo = _()
+    assert foo.get_event_handlers('xx')
+    foo_ref = weakref.ref(foo)
+    
+    del foo
+    gc.collect()
+    assert foo_ref() is None
+
+
+def test_disposing_handler3():
+    """ handlers outside object need cleaning. """ 
+    
+    class Foo(event.HasEvents):
+        pass
+    foo = Foo()
+    
+    @foo.connect('xx')
+    def handle_xx(*events):
+        pass
+    foo_ref = weakref.ref(foo)
+    assert foo.get_event_handlers('xx')
+    
+    del foo
+    gc.collect()
+    assert foo_ref() is not None  # the handler still has refs
+    
+    foo_ref().dispose()
+    gc.collect()
+    assert foo_ref() is None
+
+
+def test_disposing_handler4():
+    """ handlers outside object plus pending event need cleaning. """ 
+    
+    class Foo(event.HasEvents):
+        pass
+    foo = Foo()
+    
+    @foo.connect('xx')
+    def handle_xx(*events):
+        pass
+    foo_ref = weakref.ref(foo)
+    assert foo.get_event_handlers('xx')
+    
+    foo.emit('xx', {})  # <---------
+    
+    del foo
+    gc.collect()
+    assert foo_ref() is not None  # the handler still has refs
+    
+    foo_ref().dispose()
+    gc.collect()
+    assert foo_ref() is not None  # pending event hold a ref
+    
+    handle_xx.handle_now()
+    gc.collect()
+    assert foo_ref() is None
+
+
+def test_disposing_emitter():
+    """ Emitters on object don't need cleaning. """ 
+    
+    class Foo(event.HasEvents):
+        @event.prop
+        def bar(self, v=0):
+            return v
+        
+        @event.readonly
+        def spam(self, v=0):
+            return v
+        
+        @event.emitter
+        def eggs(self, x):
+            return {}
+    
+    foo = Foo()
+    foo_ref = weakref.ref(foo)
+    
+    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
new file mode 100644
index 0000000..31a1526
--- /dev/null
+++ b/flexx/event/tests/test_loop.py
@@ -0,0 +1,82 @@
+
+from flexx.util.testing import run_tests_if_main, skipif, skip, raises
+
+from flexx import event
+from flexx.event._dict import isidentifier
+
+
+class Foo(event.HasEvents):
+    
+    def __init__(self):
+        super().__init__()
+        self.r = []
+    
+    def on_foo(self, *events):
+        self.r.append(len(events))
+
+
+def test_iter():
+    
+    foo = Foo()
+    foo.emit('foo', {})
+    assert len(foo.r) == 0
+    event.loop.iter()
+    assert len(foo.r) == 1
+    assert foo.r[0] == 1
+    
+    foo = Foo()
+    foo.emit('foo', {})
+    foo.emit('foo', {})
+    event.loop.iter()
+    assert len(foo.r) == 1
+    assert foo.r[0] == 2
+    
+    # Failing func call
+    res = []
+    def fail():
+        res.append(1)
+        1/0
+    raises(ZeroDivisionError, fail)
+    assert len(res) == 1
+    
+    # When handled by the loop, error is printed, but no fail
+    event.loop.call_later(fail)
+    event.loop.iter()
+    assert len(res) == 2
+
+def test_context():
+    
+    foo = Foo()
+    with event.loop:
+        foo.emit('foo', {})
+    assert len(foo.r) == 1
+    assert foo.r[0] == 1
+    
+    foo = Foo()
+    with event.loop:
+        foo.emit('foo', {})
+        foo.emit('foo', {})
+    assert len(foo.r) == 1
+    assert foo.r[0] == 2
+
+
+def test_integrate():
+    
+    res = []
+    def calllater(f):
+        res.append(f)
+    
+    foo = Foo()
+    event.loop.integrate(calllater)
+    foo.emit('foo', {})
+    foo.emit('foo', {})
+    assert len(res) == 1 and res[0].__name__ == 'iter'
+    
+    with raises(ValueError):
+        event.loop.integrate('not a callable')
+    
+    import tornado
+    event.loop.integrate()
+
+
+run_tests_if_main()
diff --git a/flexx/pyscript/tests/test_parser3.py b/flexx/pyscript/tests/test_parser3.py
index cf5a8a8..5764429 100644
--- a/flexx/pyscript/tests/test_parser3.py
+++ b/flexx/pyscript/tests/test_parser3.py
@@ -260,6 +260,9 @@ class TestOtherBuildins:
         assert evalpy('for x in reversed(sorted([1, 9, 3, 2, 7, 8, 4])): print(x)') == '9\n8\n7\n4\n3\n2\n1'
         assert evalpy('for x in sorted([1, 9, 3, 2, 7, 8, 4], key=lambda a: -a): print(x)') == '9\n8\n7\n4\n3\n2\n1'
         assert evalpy('for x in sorted([1, 9, 3, 2, 7, 8, 4], reverse=True): print(x)') == '9\n8\n7\n4\n3\n2\n1'
+        
+        assert evalpy('for x in sorted(["bb", "aa", "mm", "dd"]): print(x)') == 'aa\nbb\ndd\nmm'
+        assert evalpy('for x in sorted(["bb", "aa", "mm", "dd"], key=lambda x: x): print(x)') == 'aa\nbb\ndd\nmm'
     
     def test_filter(self):
         assert list(filter(lambda x:x>0, [-1, -2, 1, 2])) == [1, 2]
@@ -323,6 +326,9 @@ class TestListMethods:
         assert evalpy('a=[3,1,4,2]; a.sort(reverse=True); a') == '[ 4, 3, 2, 1 ]'
         assert evalpy('a=[3,1,4,2]; a.sort(key=lambda x: -x); a') == '[ 4, 3, 2, 1 ]'
         assert evalpy('a=[3,1,4,2]; a.sort(key=lambda x: -x, reverse=True); a') == '[ 1, 2, 3, 4 ]'
+        
+        assert evalpy('a=["bb", "aa", "mm", "dd"]; a.sort(); a') == "[ 'aa', 'bb', 'dd', 'mm' ]"
+        assert evalpy('a=["bb", "aa", "mm", "dd"]; a.sort(key=lambda x: x); a') == "[ 'aa', 'bb', 'dd', 'mm' ]"
     
     def test_clear(self):
         assert evalpy('a=[3,1,4,2]; a.clear(); a') == '[]'