Repository: bokeh/bokeh

Issue:
Make raw_components version of bokeh.embed.components
When I'm embedding Bokeh plots I currently need to do some workarounds so that:
- the components function does not wrap the generated bokeh javascript in a script tag
- i can have a list of the plot ids

I propose a raw_components method that works very like the components method, but instead of returning `(script, divs)` returns `(unwrapped_script, plot_ids)`.



Discussion:

Resolving patch:
diff --git a/bokeh/embed.py b/bokeh/embed.py
--- a/bokeh/embed.py
+++ b/bokeh/embed.py
@@ -28,54 +28,99 @@
 from collections import Sequence
 from six import string_types
 
+
 def _wrap_in_function(code):
     # Indent and wrap Bokeh function def around
-    code = "\n".join([ "    " + line for line in code.split("\n") ])
+    code = "\n".join(["    " + line for line in code.split("\n")])
     return 'Bokeh.$(function() {\n%s\n});' % code
 
 
-def components(plot_objects, resources=None):
-    ''' Return HTML components to embed a Bokeh plot.
+def components(plot_objects, resources=None, wrap_script=True, wrap_plot_info=True):
+    '''
+    Return HTML components to embed a Bokeh plot. The data for the plot is
+    stored directly in the returned HTML.
+
+    An example can be found in examples/embed/embed_multiple.py
 
-    The data for the plot is stored directly in the returned HTML.
 
     .. note:: The returned components assume that BokehJS resources
               are **already loaded**.
 
-    Args:
-        plot_objects (PlotObject|list|dict|tuple) :
-        The |components| function takes either a single PlotObject, a list/tuple of
-        PlotObjects, or a dictionary of keys and PlotObjects. Each returns
-        a corresponding data structure of script and div pairs.
+    Args
+    ----
+    plot_objects : PlotObject|list|dict|tuple
+        A single PlotObject, a list/tuple of PlotObjects, or a dictionary of keys and PlotObjects.
+
+    resources :
+        Deprecated argument
+
+    wrap_script : boolean, optional
+        If True, the returned javascript is wrapped in a script tag. (default: True)
+
+    wrap_plot_info : boolean, optional
+        If True, then a set of divs are returned.
+        If set to False, then dictionaries are returned that can be used to manually
+        build your own divs. (default: True)
 
-        The following illustrates how different input types correlate to outputs:
+        If False, the returned dictionary contains the following information::
+
+            {
+                'modelid':  'The plots id, which can be used in the Bokeh.index',
+                'elementid': 'The css identifier the BokehJS will look for to target the plot',
+                'modeltype': 'The Bokeh model name e.g. Plot, Slider',
+            }
+
+
+
+    Returns
+    -------
+    (script, div[s]) :  UTF-8 encoded
+
+        The output depends on the input as follows::
 
             components(plot)
-            #=> (script, plot_div)
+            # => (script, plot_div)
 
-            components((plot_1, plot_2))
-            #=> (script, (plot_1_div, plot_2_div))
+            components((plot1, plot2))
+            # => (script, (plot1_div, plot2_div))
 
-            components({"Plot 1": plot_1, "Plot 2": plot_2})
-            #=> (script, {"Plot 1": plot_1_div, "Plot 2": plot_2_div})
+            components({"Plot 1": plot1, "Plot 2": plot2})
+            # => (script, {"Plot 1": plot1_div, "Plot 2": plot2_div})
 
-        An example can be found in examples/embed/embed_multiple.py
+    (raw_script, plot_info[s]) : UTF-8 encoded
 
-        resources : Deprecated argument
+        The output depends on the input as follows::
 
-    Returns:
-        (script, div[s]): UTF-8 encoded
+            components(plot, wrap_script=False, wrap_plot_info=False)
+            # => (javascript, plot_dict)
+
+            components((plot1, plot2), wrap_script=False, wrap_plot_info=False)
+            # => (javascript, (plot1_dict, plot2_dict))
+
+            components({"Plot 1": plot1, "Plot 2": plot2}, wrap_script=False, wrap_plot_info=False)
+            # => (javascript, {"Plot 1": plot1_dict, "Plot 2": plot2_dict})
     '''
-    from .document import Document
-    if isinstance(plot_objects, (PlotObject, Document)):
-        plot_objects = [plot_objects]
-    if resources is not None:
-        warn('Because the ``resources`` argument is no longer needed, '
-             'it is deprecated and will be removed in'
-             'a future version.', DeprecationWarning, stacklevel=2)
+    all_models, plots, plot_info, divs = _get_components(plot_objects, resources)
+
+    if wrap_script:
+        script = _get_script(all_models, plots)
+    else:
+        script = _get_js(all_models, plots)
+    script = encode_utf8(script)
+
+    if wrap_plot_info:
+        return script, divs
+    else:
+        return script, plot_info
+
+
+def _get_components(plot_objects, resources=None):
+    plot_objects = _check_components_input(plot_objects, resources)
+
     all_models = dict()
     plots = []
-    if isinstance(plot_objects, Sequence) and all(isinstance(x, (PlotObject, Document)) for x in plot_objects):
+
+    if isinstance(plot_objects, Sequence):
         divs = []
         for idx, plot_object in enumerate(plot_objects):
             elementid = str(uuid.uuid4())
@@ -83,44 +128,82 @@ def components(plot_objects, resources=None):
             divs = _append_div(elementid, divs)
         if len(divs) == 1:
             divs = divs[0]
+            plot_info = plots[0]
         else:
             divs = tuple(divs)
-        return _component_pair(list(all_models.values()), plots, divs)
-    elif isinstance(plot_objects, dict) and \
-         all(isinstance(x, string_types) for x in plot_objects.keys()) and \
-         all(isinstance(x, (PlotObject, Document)) for x in plot_objects.values()):
+            plot_info = tuple(plots)
+
+    if isinstance(plot_objects, dict):
         divs = {}
+        plot_info = {}
         for key in plot_objects.keys():
             elementid = str(uuid.uuid4())
-            _append_plot(all_models, plots, plot_objects[key], elementid)
+            plot_info[key] = _append_plot(all_models, plots, plot_objects[key], elementid)
             divs = _append_div(elementid, divs, key)
-        return _component_pair(list(all_models.values()), plots, divs)
-    else:
+
+    return list(all_models.values()), plots, plot_info, divs
+
+
+def _check_components_input(plot_objects, resources=None):
+    from .document import Document
+    if resources is not None:
+        warn('Because the ``resources`` argument is no longer needed, '
+             'it is deprecated and will be removed in'
+             'a future version.', DeprecationWarning, stacklevel=2)
+
+    input_type_valid = False
+
+    # Check for single item
+    if isinstance(plot_objects, (PlotObject, Document)):
+        plot_objects = [plot_objects]
+
+    # Check for sequence
+    if isinstance(plot_objects, Sequence) and all(isinstance(x, (PlotObject, Document)) for x in plot_objects):
+        input_type_valid = True
+
+    if isinstance(plot_objects, dict) and \
+       all(isinstance(x, string_types) for x in plot_objects.keys()) and \
+       all(isinstance(x, (PlotObject, Document)) for x in plot_objects.values()):
+        input_type_valid = True
+
+    if not input_type_valid:
         raise ValueError('Input must be a PlotObject, a Sequence of PlotObjects, or a mapping of string to PlotObjects')
 
-def _component_pair(all_models, plots, divs):
+    return plot_objects
+
+
+def _get_js(all_models, plots):
     js = PLOT_JS.render(
-        all_models = serialize_json(all_models),
-        plots = plots
+        all_models=serialize_json(all_models),
+        plots=plots
     )
+    return _wrap_in_function(js)
+
+
+def _get_script(all_models, plots):
+    js = _get_js(all_models, plots)
     script = PLOT_SCRIPT.render(
-        plot_js = _wrap_in_function(js),
+        plot_js=js,
     )
-    return encode_utf8(script), divs
+    return script
+
 
 def _append_plot(all_models, plots, plot_object, elementid):
     ref = plot_object.ref
     for item in plot_object.dump():
         all_models[item['id']] = item
-    plots.append({
+    plot_info = {
         'modelid': ref["id"],
-        'elementid': '#' + elementid,
+        'elementid': elementid,
         'modeltype': ref["type"]
-    })
+    }
+    plots.append(plot_info)
+    return plot_info
+
 
 def _append_div(elementid, divs=None, key=None):
     div = PLOT_DIV.render(
-        elementid = elementid
+        elementid=elementid
     )
     if isinstance(divs, list):
         divs.append(encode_utf8(div))
@@ -131,6 +214,7 @@ def _append_div(elementid, divs=None, key=None):
     else:
         return encode_utf8(div)
 
+
 def notebook_div(plot_object):
     ''' Return HTML for a div that will display a Bokeh plot in an
     IPython Notebook


Test patch:
diff --git a/bokeh/tests/test_embed.py b/bokeh/tests/test_embed.py
--- a/bokeh/tests/test_embed.py
+++ b/bokeh/tests/test_embed.py
@@ -1,5 +1,6 @@
 from __future__ import absolute_import
 
+import mock
 import unittest
 
 import bs4
@@ -51,6 +52,42 @@ def test_div_attrs(self):
         self.assertEqual(div.attrs['class'], ['plotdiv'])
         self.assertEqual(div.text, "")
 
+    def test_script_is_utf8_encoded(self):
+        script, div = embed.components(_embed_test_plot)
+        self.assertTrue(isinstance(script, str))
+
+    @mock.patch('bokeh.embed.uuid')
+    def test_output_is_without_script_tag_when_wrap_script_is_false(self, mock_uuid):
+        mock_uuid.uuid4 = mock.Mock()
+        mock_uuid.uuid4.return_value = 'uuid'
+
+        script, div = embed.components(_embed_test_plot)
+        html = bs4.BeautifulSoup(script)
+        scripts = html.findAll(name='script')
+        self.assertEqual(len(scripts), 1)
+        script_content = scripts[0].getText()
+
+        rawscript, div = embed.components(_embed_test_plot, wrap_script=False)
+        self.maxDiff = None
+        self.assertEqual(rawscript.strip(), script_content.strip())
+
+    @mock.patch('bokeh.embed.uuid')
+    def test_plot_dict_returned_when_wrap_plot_info_is_false(self, mock_uuid):
+        mock_uuid.uuid4 = mock.Mock()
+        mock_uuid.uuid4.return_value = 'uuid'
+
+        plot = _embed_test_plot
+        expected_plotdict = {"modelid": plot.ref["id"], "elementid": "uuid", "modeltype": "Plot"}
+        script, plotdict = embed.components(_embed_test_plot, wrap_plot_info=False)
+        self.assertEqual(plotdict, expected_plotdict)
+
+        script, plotids = embed.components((_embed_test_plot, _embed_test_plot), wrap_plot_info=False)
+        self.assertEqual(plotids, (expected_plotdict, expected_plotdict))
+
+        script, plotiddict = embed.components({'p1': _embed_test_plot, 'p2': _embed_test_plot}, wrap_plot_info=False)
+        self.assertEqual(plotiddict, {'p1': expected_plotdict, 'p2': expected_plotdict})
+
+
 class TestNotebookDiv(unittest.TestCase):
 
     def test_return_type(self):