Repository: zoofIO/flexx
Pull request #134 (2016-06-14 13:09:30)

Make it easier to add custom tornado handler and add example

almarklein: cc @maxyme

Patch:
diff --git a/flexx/app/__init__.py b/flexx/app/__init__.py
index 847699c..a775031 100644
--- a/flexx/app/__init__.py
+++ b/flexx/app/__init__.py
@@ -86,7 +86,7 @@ del logging
 from .session import manager, Session
 from .model import Model, get_active_model
 from .model import get_instance_by_id, get_model_classes
-from .funcs import run, start, stop, call_later
+from .funcs import init, run, start, stop, call_later
 from .funcs import init_notebook, serve, launch, export
 from .assetstore import assets
 from .clientcore import FlexxJS
diff --git a/flexx/app/examples/adding_handlers.py b/flexx/app/examples/adding_handlers.py
new file mode 100644
index 0000000..78e4733
--- /dev/null
+++ b/flexx/app/examples/adding_handlers.py
@@ -0,0 +1,54 @@
+"""
+Example that demonstrates how to add cusom Tornado handlers, for
+instance to serve a custom HTML page, or a (REST) api from the same
+process.
+
+For the sake of the example, we alo serve two example web apps.
+
+Goto:
+
+* http://localhost:port/ to see a list of served web apps
+* http://localhost:port/about to see a the custom about page
+* http://localhost:port/api/foo/bar to see the echo api in action
+
+
+"""
+
+from flexx import app
+from flexx.ui.examples.drawing import Drawing
+from flexx.ui.examples.chatroom import ChatRoom
+
+import tornado.web
+
+# Serve some web apps, just for fun
+app.serve(Drawing)
+app.serve(ChatRoom)
+
+
+class MyAboutHandler(tornado.web.RequestHandler):
+    
+    def get(self):
+        self.write('<html>This is just an <i>example</i>.</html>')
+
+
+class MyAPIHandler(tornado.web.RequestHandler):
+    
+    def get(self, path):
+        # self.request.path -> full path
+        # path -> the regexp group specified in add_handlers
+        self.write('echo ' + path)
+
+
+# Get a ref to the tornado.web.Application object
+tornado_app = app.init().native
+
+# Add our handler
+tornado_app.add_handlers(r".*", [(r"/about", MyAboutHandler),
+                                 (r"/api/(.*)", MyAPIHandler)])
+
+# Note: Tornado tries to match handlers in order, but the handlers
+# specified in the constructor come last. Therefore we can easily add
+# specific handlers here even though Flexx' main handler is very
+# generic.
+
+app.start()
diff --git a/flexx/app/funcs.py b/flexx/app/funcs.py
index efa3904..3aa87ec 100644
--- a/flexx/app/funcs.py
+++ b/flexx/app/funcs.py
@@ -22,10 +22,31 @@ reprs = json.dumps
 
 ## Main loop functions
 
-def _server_open(host=None, port=None):
-    """ Server.open() but with handling of defaults, and checking if
-    already serving.
+
+def init(**kwargs):
+    """ Initialize (bind) the server and return the server object.
+    
+    As a user, you typically do not need this function, as it is
+    automatically called by start() and run(). If called without
+    arguments, this function can safely be called multiple times.
+    
+    The returned server object is a small wrapper around a native server
+    object (available through ``server_object.native``). For now this
+    is always a Tornado Application, but in the future other types of
+    servers (e.g. Flask) may be supported.
+    
+    Arguments:
+        args: currently unused. Arguments may be added at a later time (e.g.
+            to chose between different server backends).
+        host (str): The hostname to serve on. By default
+            ``flexx.config.hostname`` is used.
+        port (int, str): The port number. If a string is given, it is
+            hashed to an ephemeral port number. By default
+            ``flexx.config.hostname`` is used.
     """
+    def _getargs(host=None, port=None):
+        return host, port
+    host, port = _getargs(**kwargs)
     # If already hosting, return or error
     if getattr(server, '_is_hosting', False):
         if host is None and port is None:
@@ -40,6 +61,7 @@ def _server_open(host=None, port=None):
     # Start hosting
     server.open(host, port)
     server._is_hosting = True
+    return server
 
 
 def start(host=None, port=None):
@@ -54,14 +76,14 @@ def start(host=None, port=None):
     from environment variables FLEXX_HOSTNAME and FLEXX_PORT.
     
     Arguments:
-        host (str): The hostname to serve on. Default 'localhost'. This
-            parameter is ignored if the server was already running.
+        host (str): The hostname to serve on. By default
+            ``flexx.config.hostname`` is used.
         port (int, str): The port number. If a string is given, it is
-            hashed to an ephemeral port number. If not given or None,
-            will try a series of ports until one is found that is free.
+            hashed to an ephemeral port number. By default
+            ``flexx.config.hostname`` is used.
     """
     # Get server up
-    _server_open(host, port)
+    init(host=host, port=port)
     # Start event loop
     logger.info('Starting Flexx event loop.')
     server.start()
@@ -139,7 +161,7 @@ def init_notebook():
             pass  # Ok if it fails; assets can be loaded dynamically.
     
     # Open server - we only use websocket for JS-to-Py communication
-    _server_open()
+    init()
     host, port = server.serving_at
     asset_elements = session.get_assets_as_html()
     
@@ -197,7 +219,7 @@ def launch(cls, runtime=None, **runtime_kwargs):
     session = manager.create_session(cls.__name__)
     
     # Launch web runtime, the server will wait for the connection
-    _server_open()
+    init()
     host, port = server.serving_at
     if runtime == 'nodejs':
         all_js = session.get_js_only()
diff --git a/flexx/app/tornadoserver.py b/flexx/app/tornadoserver.py
index c0f5f0e..069160f 100644
--- a/flexx/app/tornadoserver.py
+++ b/flexx/app/tornadoserver.py
@@ -49,6 +49,11 @@ class AbstractServer:
     def call_later(self, delay, callback, *args, **kwargs):
         """ Call a function in a later event loop iteration. """
         raise NotImplementedError()
+    
+    @property
+    def native(self):
+        """ Get the native server object (e.g. the Tornado Application). """
+        return self._native
 
 
 class TornadoServer(AbstractServer):
@@ -56,29 +61,29 @@ class TornadoServer(AbstractServer):
     """
     
     def __init__(self):
-        self._app = None
+        self._native = None
         self._loop = tornado.ioloop.IOLoop.instance()
     
     def open(self, host, port):
         
         # Check that its not already running
-        if self._app is not None:
+        if self._native is not None:
             # return
             raise RuntimeError('flexx server is already hosting.')
         
         # Create server
-        self._app = tornado.web.Application([(r"/(.*)/ws", WSHandler), 
-                                             (r"/(.*)", MainHandler), ])
+        self._native = tornado.web.Application([(r"/(.*)/ws", WSHandler), 
+                                                (r"/(.*)", MainHandler), ])
         
         # Start server (find free port number if port not given)
         if port:
             port = int(port)
-            self._app.listen(port, host)
+            self._native.listen(port, host)
         else:
             for i in range(100):
                 port = port_hash('flexx%i' % i)
                 try:
-                    self._app.listen(port, host)
+                    self._native.listen(port, host)
                     break
                 except OSError:
                     pass  # address already in use
@@ -86,7 +91,7 @@ class TornadoServer(AbstractServer):
                 raise RuntimeError('Could not bind to free address')    
         
         # Notify address, so its easy to e.g. copy and paste in the browser
-        self.serving_at = self._app.serving_at = host, port
+        self.serving_at = self._native.serving_at = host, port
         logger.info('Serving apps at http://%s:%i/' % (host, port))
     
     def start(self):


Test patch:
diff --git a/flexx/app/tests/test_funcs.py b/flexx/app/tests/test_funcs.py
new file mode 100644
index 0000000..34345be
--- /dev/null
+++ b/flexx/app/tests/test_funcs.py
@@ -0,0 +1,11 @@
+from flexx.util.testing import run_tests_if_main, raises
+
+from flexx import app
+
+
+def test_add_handlers():
+    server = app.init()
+    tornado_app = server.native
+    assert tornado_app.add_handlers
+
+run_tests_if_main()