Repository: holoviz/holoviews

Issue:
Better error message if no backend has been loaded 
In a few places you will get an error and traceback like this if you try to plot things without having loaded an extension:

```python
~/miniconda3/lib/python3.6/site-packages/holoviews/util/settings.py in output(cls, line, cell, cell_runner, help_prompt, warnfn, **kwargs)
    260         # Make backup of previous options
    261         prev_backend = Store.current_backend
--> 262         prev_renderer = Store.renderers[prev_backend]
    263         prev_backend_spec = prev_backend+':'+prev_renderer.mode
    264         prev_params = {k: v for k, v in prev_renderer.get_param_values()
 
KeyError: 'matplotlib'
```


Discussion:

Resolving patch:
diff --git a/holoviews/util/__init__.py b/holoviews/util/__init__.py
--- a/holoviews/util/__init__.py
+++ b/holoviews/util/__init__.py
@@ -333,6 +333,19 @@ def _expand_options(cls, options, backend=None):
             {'Image': {'plot': dict(show_title=False), 'style': dict(cmap='viridis')}}
         """
         current_backend = Store.current_backend
+
+        if not Store.renderers:
+            raise ValueError("No plotting extension is currently loaded. "
+                             "Ensure you load an plotting extension with "
+                             "hv.extension or import it explicitly from "
+                             "holoviews.plotting before applying any "
+                             "options.")
+        elif current_backend not in Store.renderers:
+            raise ValueError("Currently selected plotting extension {ext} "
+                             "has not been loaded, ensure you load it "
+                             "with hv.extension({ext}) before setting "
+                             "options".format(ext=repr(current_backend)))
+
         try:
             backend_options = Store.options(backend=backend or current_backend)
         except KeyError as e:
diff --git a/holoviews/util/settings.py b/holoviews/util/settings.py
--- a/holoviews/util/settings.py
+++ b/holoviews/util/settings.py
@@ -273,13 +273,25 @@ def output(cls, line=None, cell=None, cell_runner=None,
         if line and kwargs:
             raise ValueError('Please either specify a string to '
                              'parse or keyword arguments')
+        elif not Store.renderers:
+            raise ValueError("No plotting extension is currently loaded. "
+                             "Ensure you load an plotting extension with "
+                             "hv.extension or import it explicitly from "
+                             "holoviews.plotting before using hv.output.")
 
         # Make backup of previous options
         prev_backend = Store.current_backend
-        prev_renderer = Store.renderers[prev_backend]
-        prev_backend_spec = prev_backend+':'+prev_renderer.mode
-        prev_params = {k: v for k, v in prev_renderer.param.get_param_values()
-                       if k in cls.render_params}
+        if prev_backend in Store.renderers:
+            prev_renderer = Store.renderers[prev_backend]
+            prev_backend_spec = prev_backend+':'+prev_renderer.mode
+            prev_params = {k: v for k, v in prev_renderer.param.get_param_values()
+                           if k in cls.render_params}
+        else:
+            prev_renderer = None
+            prev_backend_spec = prev_backend+':default'
+            prev_params = {}
+        backend = prev_backend
+
         prev_restore = dict(OutputSettings.options)
         try:
             if line is not None:
@@ -296,7 +308,8 @@ def output(cls, line=None, cell=None, cell_runner=None,
                     backend_spec += ':default'
             else:
                 backend_spec = prev_backend_spec
-            renderer = Store.renderers[backend_spec.split(':')[0]]
+            backend = backend_spec.split(':')[0]
+            renderer = Store.renderers[backend]
             render_params = {k: v for k, v in renderer.param.get_param_values()
                              if k in cls.render_params}
 
@@ -307,6 +320,11 @@ def output(cls, line=None, cell=None, cell_runner=None,
             # If setting options failed ensure they are reset
             OutputSettings.options = prev_restore
             cls.set_backend(prev_backend)
+            if backend not in Store.renderers:
+                raise ValueError("The selected plotting extension {ext} "
+                                 "has not been loaded, ensure you load it "
+                                 "with hv.extension({ext}) before using "
+                                 "hv.output.".format(ext=repr(backend)))
             print('Error: %s' % str(e))
             if help_prompt:
                 print(help_prompt)


Test patch:
diff --git a/holoviews/tests/core/testdimensioned.py b/holoviews/tests/core/testdimensioned.py
--- a/holoviews/tests/core/testdimensioned.py
+++ b/holoviews/tests/core/testdimensioned.py
@@ -8,6 +8,11 @@
 class TestObj(Element):
     pass
 
+class MockRenderer(object):
+
+    def __init__(self, backend):
+        self.backend = backend
+
 
 class CustomBackendTestCase(LoggingComparisonTestCase):
     """
@@ -29,6 +34,8 @@ def tearDown(self):
         Store._custom_options.pop('backend_1')
         Store._custom_options.pop('backend_2')
         Store.set_current_backend(self.current_backend)
+        Store.renderers.pop('backend_1')
+        Store.renderers.pop('backend_2')
 
     @classmethod
     def register_custom(cls, objtype, backend, custom_plot=[], custom_style=[]):
@@ -43,6 +50,7 @@ def register_custom(cls, objtype, backend, custom_plot=[], custom_style=[]):
                       'style': Options(allowed_keywords=style_opts),
                       'output': Options(allowed_keywords=['backend'])}
         Store._options[backend][name] = opt_groups
+        Store.renderers[backend] = MockRenderer(backend)
 
 
 
@@ -207,7 +215,10 @@ def test_opts_multiple_resassignment_cleans_unused_tree(self):
         obj = HoloMap({0: TestObj([]), 1: TestObj([])}).opts(style_opt1='A').opts(plot_opt1='B')
         custom_options = Store._custom_options['backend_1']
         self.assertIn(obj.last.id, custom_options)
-        self.assertEqual(len(custom_options), 1)
+        self.assertEqual(len(custom_options), 2)
+        for o in obj:
+            o.id = None
+        self.assertEqual(len(custom_options), 0)
 
     def test_opts_resassignment_cleans_unused_tree_cross_backend(self):
         obj = TestObj([]).opts(style_opt1='A').opts(plot_opt1='B', backend='backend_2')