Repository: zoofIO/flexx
Pull request #155 (2016-07-06 22:14:10)

Improvements for the notebook


Patch:
diff --git a/flexx/app/assetstore.py b/flexx/app/assetstore.py
index c86737a..7f7d071 100644
--- a/flexx/app/assetstore.py
+++ b/flexx/app/assetstore.py
@@ -97,7 +97,7 @@ table{border-collapse:collapse;border-spacing:0}
 td,th{padding:0}
 """
 
-HEADER = '/* Autogenerated JS from Flexx. Code Subject to the new BSD license.*/'
+HEADER = '/* Autogenerated code from Flexx. Code Subject to the new BSD license.*/'
 
 
 reprs = json.dumps
@@ -300,6 +300,37 @@ class SessionAssets:
         """
         return self._id
     
+    def _load_asset_dynamically(self, fname, code=None):
+        """ Load an asset (JS or CSS) in a running session.
+        """
+        # Register that we loaded the asset. If code is given -> not an asset
+        if code is None:
+            self._asset_names.append(fname)
+        # Get code
+        if code is None:
+            code = self._store.load_asset(fname).decode()
+        if not code:
+            return
+        # Check
+        if not fname.lower().endswith(('.js', '.css')):
+            raise ValueError('Can only dynamically load JS and CSS.')
+        # In notebook?
+        from .session import manager  # noqa - avoid circular import
+        is_interactive = self is manager.get_default_session()  # e.g. in notebook
+        in_notebook = is_interactive and getattr(self, 'init_notebook_done', False)
+        
+        if in_notebook:
+            # Load using IPython constructs
+            from IPython.display import display, HTML
+            if fname.upper().endswith('.JS'):
+                display(HTML("<script>%s</script>" % code))
+            else:
+                display(HTML("<style>%s</style>" % code))
+        else:
+            # Load using Flexx construct
+            suffix = fname.split('.')[-1].upper()
+            self._send_command('DEFINE-%s %s' % (suffix, code))
+    
     def get_used_asset_names(self):
         """ Get a list of names of the assets used by this session, in
         the order that they were added.
@@ -341,13 +372,13 @@ class SessionAssets:
         if fname not in self._store.get_asset_names():
             raise IndexError('Asset %r is not present in the store.' % fname)
         
-        if self._served and (fname.endswith('.js') or fname.endswith('.css')):
-            suffix = fname.split('.')[-1].upper()
-            code = self._store.load_asset(fname).decode()
-            self._send_command('DEFINE-%s %s' % (suffix, code))
-            #logger.warn('Adding asset %r but the page was already "served".' % fname)
-        
-        if before:
+        if self._served:
+            if fname.endswith('.js') or fname.endswith('.css'):
+                self._load_asset_dynamically(fname)
+            else:
+                logger.warn('Cannot load asset %r '
+                            'because the page is already served.' % fname)
+        elif before:
             index = self._asset_names.index(before)
             self._asset_names.insert(index, fname)
         else:
@@ -434,16 +465,9 @@ class SessionAssets:
             fname = module_name.replace('.', '-')
             if (fname + '.js') not in self._asset_names:
                 if self._served:
-                    # todo: if we knew the deps of a module, this would work better
-                    t = ('Trying to use a class from module %r, but that module '
-                         'is not loaded. Use session.register_model_class(%s) '
-                         'to preload it. You might also see this because the runtime '
-                         'started before the relevant Python module was imported. '
-                         'Proceeding by dynamic loading, but things may not work.')
-                    logger.warning(t % (module_name, cls.__name__))
-                    load_asset = lambda f: self._store.load_asset(f).decode()
-                    self._send_command('DEFINE-JS ' + load_asset(fname + '.js'))
-                    self._send_command('DEFINE-CSS ' + load_asset(fname + '.js'))
+                    logger.debug('Dynamically defining %r' % (fname + '.js'))
+                    self._load_asset_dynamically(fname + '.js')
+                    self._load_asset_dynamically(fname + '.css')
                 else:
                     self.use_global_asset(fname + '.css')
                     self.use_global_asset(fname + '.js')
@@ -454,9 +478,8 @@ class SessionAssets:
             # Define class dynamically - assuming we're a session subclass ...
             logger.debug('Dynamically defining class %r' % cls)
             js, css = cls.JS.CODE, cls.CSS
-            self._send_command('DEFINE-JS ' + js)
-            if css.strip():
-                self._send_command('DEFINE-CSS ' + css)
+            self._load_asset_dynamically('.js', js)
+            self._load_asset_dynamically('.js', css)
     
     def _get_js_and_css_assets(self, with_reset=False):
         """ Get an ordered dictionary with the JS and CSS assets.
diff --git a/flexx/app/clientcore.py b/flexx/app/clientcore.py
index fe48fde..c2b6263 100644
--- a/flexx/app/clientcore.py
+++ b/flexx/app/clientcore.py
@@ -179,10 +179,12 @@ class FlexxJS:
         elif msg.startswith('EXEC '):
             eval(msg[5:])  # like eval, but do not return result
         elif msg.startswith('DEFINE-JS '):
-            eval(msg[10:])
-            #el = window.document.createElement("script")
-            #el.innerHTML = msg[10:]
-            #window.document.body.appendChild(el)
+            if self.nodejs:
+                eval(msg[10:])  # best we can do
+            else:
+                el = window.document.createElement("script")
+                el.innerHTML = msg[10:]
+                window.document.body.appendChild(el)
         elif msg.startswith('DEFINE-CSS '):
             # http://stackoverflow.com/a/707580/2271927
             el = window.document.createElement("style")
diff --git a/flexx/app/funcs.py b/flexx/app/funcs.py
index 7a34ca6..f3fa01a 100644
--- a/flexx/app/funcs.py
+++ b/flexx/app/funcs.py
@@ -245,22 +245,24 @@ def init_notebook():
     session = manager.get_default_session()
     if session is None:
         session = manager.create_default_session()
-    if hasattr(session, '_init_notebook_done'):
+    if getattr(session, 'init_notebook_done', False):
         display(HTML("<i>Flexx already loaded</i>"))
         return  # Don't inject twice
     else:
-        session._init_notebook_done = True
+        session.init_notebook_done = True  # also used in assetstore
     
     # Install helper to make things work in exported notebooks
     NoteBookHelper(session)
     
-    # Load assets
+    # Try loading assets for flexx.ui. This will only work if flexx.ui
+    # is imported. This is not strictly necessary, since Flexx can
+    # dynamically load the assets, but it seems nicer to do it here.
     try:
         session.use_global_asset('phosphor-all.js')
         session.use_global_asset('flexx-ui.css')
         session.use_global_asset('flexx-ui.js')
     except IndexError:
-        pass  # Ok if it fails; assets can be loaded dynamically.
+        pass
     
     # Open server - the notebook helper takes care of the JS resulting
     # from running a cell, but any interaction goes over the websocket.
diff --git a/flexx/pyscript/parser0.py b/flexx/pyscript/parser0.py
index b10e1d9..0cf95c1 100644
--- a/flexx/pyscript/parser0.py
+++ b/flexx/pyscript/parser0.py
@@ -75,6 +75,13 @@ UMD = """
 """.lstrip()  # "dep", require("dep"), name, root.dep, dep
 
 
+UMD_LIGHT = """
+(function (root, factory) {
+    root.%s = factory();
+}(this, function () {
+""".lstrip()
+
+
 def get_module_preamble(name, deps):
     """ Wrap code in a module compatible with UMD (Universal Module
     Definition), making it work with AMD (i.e. require), Node, plain
@@ -252,7 +259,10 @@ class Parser0:
             exports = [name for name in sorted(ns) if not name.startswith('_')]
             export_keyvals = [reprs(name) + ': ' + name for name in exports]
             code = self._parts
-            code.insert(0, get_module_preamble(module, []))
+            if module.endswith('+'):  # todo: document - proper UMD
+                code.insert(0, get_module_preamble(module[:-1], []))
+            else:  # Light module
+                code.insert(0, UMD_LIGHT % module)
             code.append('\n    return {%s};\n' % ', '.join(export_keyvals))
             code.append('}));\n')
             
diff --git a/flexx/ui/_widget.py b/flexx/ui/_widget.py
index ce6cc06..1efbb1e 100644
--- a/flexx/ui/_widget.py
+++ b/flexx/ui/_widget.py
@@ -404,7 +404,10 @@ class Widget(Model):
                 self.outernode.parentNode.removeChild(self.outernode)
             # Attach
             if id:
-                el = window.document.getElementById(id)
+                if id == 'body':
+                    el = window.document.body
+                else:
+                    el = window.document.getElementById(id)
                 self.phosphor.attach(el)
                 window.addEventListener('resize', lambda: (self.phosphor.update(),
                                                            self._check_real_size()))


Test patch:
diff --git a/flexx/pyscript/tests/test_functions.py b/flexx/pyscript/tests/test_functions.py
index 70aade3..ed06220 100644
--- a/flexx/pyscript/tests/test_functions.py
+++ b/flexx/pyscript/tests/test_functions.py
@@ -170,9 +170,17 @@ def test_scripts():
     assert 'foo = 42;' in jscode
     assert 'define(' not in jscode
     
-    # Convert - module
+    # Convert - module light
     script2js(pyname, 'mymodule')
     
+    # Check result
+    jscode = open(jsname, 'rb').read().decode()
+    assert 'foo = 42;' in jscode
+    assert 'define(' not in jscode
+    
+    # Convert - module UMD
+    script2js(pyname, 'mymodule+')  # at the moment, this is the undocumented API
+    
     # Check result
     jscode = open(jsname, 'rb').read().decode()
     assert 'foo = 42;' in jscode