diff --git a/holoviews/ipython/__init__.py b/holoviews/ipython/__init__.py index f4dfb03cac..f33f9c2b32 100644 --- a/holoviews/ipython/__init__.py +++ b/holoviews/ipython/__init__.py @@ -146,15 +146,22 @@ class notebook_extension(param.ParameterizedFunction): 'plotly': 'plotly'} def __call__(self, *args, **params): - imports = [(name, b) for name, b in self._backends.items() - if name in args or params.get(name, False)] - if not imports or 'matplotlib' not in Store.renderers: - imports = imports + [('matplotlib', 'mpl')] + # Get requested backends + imports = [(arg, self._backends[arg]) for arg in args + if arg in self._backends] + for p, val in sorted(params.items()): + if p in self._backends: + imports.append((p, self._backends[p])) + if not imports: + imports = [('matplotlib', 'mpl')] args = list(args) + selected_backend = None for backend, imp in imports: try: __import__('holoviews.plotting.%s' % imp) + if selected_backend is None: + selected_backend = backend except ImportError: if backend in args: args.pop(args.index(backend)) @@ -187,7 +194,7 @@ def __call__(self, *args, **params): ip = get_ipython() if ip is None else ip # noqa (get_ipython) param_ext.load_ipython_extension(ip, verbose=False) load_magics(ip) - OutputMagic.initialize(list( self._backends.keys())) + OutputMagic.initialize([backend for backend, _ in imports]) set_display_hooks(ip) notebook_extension._loaded = True @@ -199,6 +206,11 @@ def __call__(self, *args, **params): if css: display(HTML(css)) + for r in [r for r in resources if r != 'holoviews']: + Store.renderers[r].load_nb(inline=p.inline) + if selected_backend is not None: + Store.current_backend = selected_backend + resources = list(resources) if len(resources) == 0: return @@ -209,11 +221,7 @@ def __call__(self, *args, **params): message = '' if not p.banner else '%s successfully loaded in this cell.' % loaded load_hvjs(logo=p.banner, JS=('holoviews' in resources), message = message) - for r in [r for r in resources if r != 'holoviews']: - Store.renderers[r].load_nb(inline=p.inline) - if resources[-1] != 'holoviews': - get_ipython().magic(u"output backend=%r" % resources[-1]) # noqa (get_ipython)) def _get_resources(self, args, params): diff --git a/holoviews/ipython/magics.py b/holoviews/ipython/magics.py index 5cbf9d8311..9af6d22537 100644 --- a/holoviews/ipython/magics.py +++ b/holoviews/ipython/magics.py @@ -97,7 +97,6 @@ def get_options(cls, line, options, linemagic): info = (keyword,value)+allowed raise ValueError("Value %r for key %r not between %s and %s" % info) options[keyword] = value - return cls._validate(options, items, linemagic) @classmethod @@ -231,21 +230,28 @@ class OutputMagic(OptionsMagic): 'max-width', 'min-width', 'max-height', 'min-height', 'outline', 'float']}} - defaults = OrderedDict([('backend' , 'matplotlib'), - ('fig' , 'png'), - ('holomap' , 'widgets'), - ('widgets' , 'embed'), - ('fps' , 20), + defaults = OrderedDict([('backend' , None), + ('fig' , None), + ('holomap' , None), + ('widgets' , None), + ('fps' , None), ('max_frames' , 500), ('max_branches', 2), - ('size' , 100), - ('dpi' , 72), + ('size' , None), + ('dpi' , None), ('charwidth' , 80), ('filename' , None), ('info' , False), - ('css' , {})]) + ('css' , None)]) + + # Defines the options the OutputMagic remembers. All other options + # are held by the backend specific Renderer. + remembered = ['max_frames', 'max_branches', 'charwidth', 'info', 'filename'] + + # Remaining backend specific options renderer options + render_params = ['fig', 'holomap', 'size', 'fps', 'dpi', 'css', 'widget_mode', 'mode'] - options = OrderedDict(defaults.items()) + options = OrderedDict() _backend_options = defaultdict(dict) # Used to disable info output in testing @@ -286,22 +292,23 @@ def info(cls, obj): @classmethod def _generate_docstring(cls): + renderer = Store.renderers[Store.current_backend] intro = ["Magic for setting HoloViews display options.", "Arguments are supplied as a series of keywords in any order:", ''] backend = "backend : The backend used by HoloViews %r" % cls.allowed['backend'] fig = "fig : The static figure format %r" % cls.allowed['fig'] holomap = "holomap : The display type for holomaps %r" % cls.allowed['holomap'] - widgets = "widgets : The widget mode for widgets %r" % cls.allowed['widgets'] + widgets = "widgets : The widget mode for widgets %r" % renderer.widget_mode fps = ("fps : The frames per second for animations (default %r)" - % cls.defaults['widgets']) + % renderer.fps) frames= ("max_frames : The max number of frames rendered (default %r)" % cls.defaults['max_frames']) branches=("max_branches : The max number of Layout branches rendered (default %r)" % cls.defaults['max_branches']) size = ("size : The percentage size of displayed output (default %r)" - % cls.defaults['size']) + % renderer.size) dpi = ("dpi : The rendered dpi of the figure (default %r)" - % cls.defaults['dpi']) + % renderer.dpi) chars = ("charwidth : The max character width for displaying the output magic (default %r)" % cls.defaults['charwidth']) fname = ("filename : The filename of the saved output, if any (default %r)" @@ -337,25 +344,48 @@ def output(self, line, cell=None): print("\nFor help with the %output magic, call %output?") return - restore_copy = OrderedDict(OutputMagic.options.items()) + # 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.get_param_values() + if k in self.render_params} + prev_restore = dict(OutputMagic.options) try: - options = OrderedDict(OutputMagic.options.items()) - new_options = self.get_options(line, options, cell is None) - self._set_render_options(new_options) + # Process magic + new_options = self.get_options(line, {}, cell is None) + + # Make backup of options on selected renderer + if 'backend' in new_options: + backend_spec = new_options['backend'] + if ':' not in backend_spec: + backend_spec += ':default' + else: + backend_spec = prev_backend_spec + renderer = Store.renderers[backend_spec.split(':')[0]] + render_params = {k: v for k, v in renderer.get_param_values() + if k in self.render_params} + + # Set options on selected renderer and set display hook options OutputMagic.options = new_options + self._set_render_options(new_options, backend_spec) except Exception as e: - self.update_options(options, {'backend': restore_copy['backend']}) - OutputMagic.options = restore_copy - self._set_render_options(restore_copy) + # If setting options failed ensure they are reset + OutputMagic.options = prev_restore + self.set_backend(prev_backend) print('Error: %s' % str(e)) print("For help with the %output magic, call %output?\n") return if cell is not None: self.shell.run_cell(cell, store_history=STORE_HISTORY) - self.update_options(options, {'backend': restore_copy['backend']}) - OutputMagic.options = restore_copy - self._set_render_options(restore_copy) + # After cell magic restore previous options and restore + # temporarily selected renderer + OutputMagic.options = prev_restore + self._set_render_options(render_params, backend_spec) + if backend_spec.split(':')[0] != prev_backend: + self.set_backend(prev_backend) + self._set_render_options(prev_params, prev_backend_spec) @classmethod @@ -364,56 +394,61 @@ def update_options(cls, options, items): Switch default options and backend if new backend is supplied in items. """ - backend = items.get('backend', '') + # Get new backend + backend_spec = items.get('backend', Store.current_backend) + split = backend_spec.split(':') + backend, mode = split if len(split)==2 else (split[0], 'default') + if ':' not in backend_spec: + backend_spec += ':default' + + # Get previous backend prev_backend = Store.current_backend - renderer = Store.renderers[Store.current_backend] - prev_backend += ':%s' % renderer.mode + renderer = Store.renderers[prev_backend] + prev_backend_spec = prev_backend+':'+renderer.mode + + # Update allowed formats + for p in ['fig', 'holomap']: + cls.allowed[p] = list_formats(p, backend_spec) - available = backend in Store.renderers.keys() - if (not backend) or (not available) or backend == prev_backend: + # Return if backend invalid and let validation error + if backend not in Store.renderers: + options['backend'] = backend_spec return options - cls._backend_options[prev_backend] = cls.options + # Get backend specific options + backend_options = dict(cls._backend_options[backend_spec]) + cls._backend_options[prev_backend_spec] = {k: v for k, v in cls.options.items() + if k in cls.remembered} + + # Fill in remembered options with defaults + for opt in cls.remembered: + if opt not in backend_options: + backend_options[opt] = cls.defaults[opt] - backend_options = cls._backend_options[backend] + # Switch format if mode does not allow it for p in ['fig', 'holomap']: - opts = list_formats(p, backend) - cls.allowed[p] = opts - cls.defaults[p] = opts[0] - if p not in backend_options: - backend_options[p] = opts[0] - - backend = backend.split(':')[0] - render_params = ['fig', 'holomap', 'size', 'fps', 'dpi', 'css'] - for p in render_params: - if p in backend_options: - opt = backend_options[p] - cls.defaults[p] = opt - else: - opt = cls.defaults[p] - backend_options[p] = opt + if backend_options.get(p) not in cls.allowed[p]: + backend_options[p] = cls.allowed[p][0] - for opt in options: - if opt not in backend_options: - backend_options[opt] = options[opt] + # Ensure backend and mode are set + backend_options['backend'] = backend_spec + backend_options['mode'] = mode - cls.set_backend(backend) return backend_options @classmethod def initialize(cls, backend_list): cls.backend_list = backend_list - backend = cls.options.get('backend', cls.defaults['backend']) + backend = cls.options.get('backend', Store.current_backend) if backend in Store.renderers: - cls.options = dict(cls.defaults) - cls._set_render_options(cls.defaults) + cls.options = dict({k: cls.defaults[k] for k in cls.remembered}) cls.set_backend(backend) else: cls.options['backend'] = None - cls.defaults['backend'] = None cls.set_backend(None) + @classmethod def set_backend(cls, backend): cls.last_backend = Store.current_backend @@ -421,17 +456,21 @@ def set_backend(cls, backend): @classmethod - def _set_render_options(cls, options): + def _set_render_options(cls, options, backend=None): """ Set options on current Renderer. """ - split = options['backend'].split(':') - backend, mode = split if len(split)==2 else (split[0], 'default') + if backend: + backend = backend.split(':')[0] + else: + backend = Store.current_backend + + cls.set_backend(backend) + if 'widgets' in options: + options['widget_mode'] = options['widgets'] renderer = Store.renderers[backend] - render_params = ['fig', 'holomap', 'size', 'fps', 'dpi', 'css'] - render_options = {k: options[k] for k in render_params} - renderer.set_param(**dict(render_options, widget_mode=options['widgets'], - mode=mode)) + render_options = {k: options[k] for k in cls.render_params if k in options} + renderer.set_param(**render_options) @magics_class diff --git a/holoviews/plotting/mpl/renderer.py b/holoviews/plotting/mpl/renderer.py index e9f80cd1a8..10728df9bf 100644 --- a/holoviews/plotting/mpl/renderer.py +++ b/holoviews/plotting/mpl/renderer.py @@ -43,6 +43,9 @@ class MPLRenderer(Renderer): backend = param.String('matplotlib', doc="The backend name.") + dpi=param.Integer(72, doc=""" + The render resolution in dpi (dots per inch)""") + fig = param.ObjectSelector(default='auto', objects=['png', 'svg', 'pdf', 'html', None, 'auto'], doc=""" Output render format for static figures. If None, no figure diff --git a/holoviews/plotting/renderer.py b/holoviews/plotting/renderer.py index 13cec36302..e78dba8d98 100644 --- a/holoviews/plotting/renderer.py +++ b/holoviews/plotting/renderer.py @@ -84,7 +84,7 @@ class Renderer(Exporter): The full, lowercase name of the rendering backend or third part plotting package used e.g 'matplotlib' or 'cairo'.""") - dpi=param.Integer(None, allow_None=True, doc=""" + dpi=param.Integer(None, doc=""" The render resolution in dpi (dots per inch)""") fig = param.ObjectSelector(default='auto', objects=['auto'], doc=""" @@ -387,7 +387,7 @@ def html_assets(cls, core=True, extras=True, backends=None): backends = [cls.backend] if cls.backend else [] # Get all the widgets and find the set of required js widget files - widgets = [wdgt for r in Renderer.__subclasses__() + widgets = [wdgt for r in [Renderer]+Renderer.__subclasses__() for wdgt in r.widgets.values()] css = list({wdgt.css for wdgt in widgets}) basejs = list({wdgt.basejs for wdgt in widgets}) diff --git a/tests/testmagics.py b/tests/testmagics.py index feef8bc860..ba5346e62e 100644 --- a/tests/testmagics.py +++ b/tests/testmagics.py @@ -95,7 +95,6 @@ def test_cell_opts_norm(self): class TestOutputMagic(ExtensionTestCase): def tearDown(self): - ipython.OutputMagic.options = ipython.OutputMagic.defaults super(TestOutputMagic, self).tearDown() def test_output_svg(self): @@ -126,7 +125,7 @@ def test_output_size(self): def test_output_invalid_size(self): self.line_magic('output', "size=-50") - self.assertEqual(ipython.OutputMagic.options.get('size', None), 100) + self.assertEqual(ipython.OutputMagic.options.get('size', None), None) class TestCompositorMagic(ExtensionTestCase):