diff --git a/doc/user_guide/index.rst b/doc/user_guide/index.rst index 192fe05a54..cd108cced8 100644 --- a/doc/user_guide/index.rst +++ b/doc/user_guide/index.rst @@ -17,6 +17,9 @@ User Guide * `Parameters `_ Using Param to express panels in a self-contained class. +* `Pipelines `_ + Using Parameterized classes to declare linear workflows containing multiple panels. + .. toctree:: :titlesonly: :hidden: @@ -27,3 +30,4 @@ User Guide Widgets Layouts Parameters + Pipelines diff --git a/examples/user_guide/Param.ipynb b/examples/user_guide/Param.ipynb index f096b94077..457c995b96 100644 --- a/examples/user_guide/Param.ipynb +++ b/examples/user_guide/Param.ipynb @@ -74,28 +74,21 @@ "metadata": {}, "outputs": [], "source": [ - "import panel\n", + "import panel as pn\n", + "\n", + "pn.extension()\n", "\n", - "panel.extension()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ "base = BaseClass()\n", - "panel.Row(Example, base)" + "pn.Row(Example, base)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "As you can see, `panel` does not need to be provided with any knowledge of your domain-specific application, not even the names of your parameters; it simply displays widgets for whatever Parameters may have been defined on that object. Using Param with ``Panel`` thus achieves a nearly complete separation between your domain-specific code and your display code, making it vastly easier to maintain both of them over time. Here even the `msg` button behavior was specified declaratively, as an action that can be invoked (printing \"Hello\") independently of whether it is used in a GUI or other context.\n", + "As you can see, `Panel` does not need to be provided with any knowledge of your domain-specific application, not even the names of your parameters; it simply displays widgets for whatever Parameters may have been defined on that object. Using Param with ``Panel`` thus achieves a nearly complete separation between your domain-specific code and your display code, making it vastly easier to maintain both of them over time. Here even the `msg` button behavior was specified declaratively, as an action that can be invoked (printing \\\"Hello\\\") independently of whether it is used in a GUI or other context.\n", "\n", - "Interacting with the widgets above is only supported on a live Python-backed server, but you can also export static renderings of the widgets to a file or web page. \n", + "Interacting with the widgets above is only supported in the notebook and on bokeh server, but you can also export static renderings of the widgets to a file or web page. \n", "\n", "By default, editing values in this way requires you to run the notebook cell by cell -- when you get to the above cell, edit the values as you like, and then move on to execute subsequent cells, where any reference to those parameter values will use your interactively selected setting:" ] @@ -118,6 +111,22 @@ "Example.num_int" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The reverse is possible, editing a parameter from Python will automatically update any widgets that were generated from the parameter:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Example.int_list = [1, 7]" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -134,20 +143,100 @@ "Example.timestamps" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As you can see, you can access the parameter values at the class level from within the notebook to control behavior explicitly, e.g. to select what to show in subsequent cells. Moreover, any instances of the Parameterized classes in your own code will now use the new parameter values unless specifically overridden in that instance, so you can now import and use your domain-specific library however you like, knowing that it will use your interactive selections wherever those classes appear. None of the domain-specific code needs to know or care that you used ``Panel``; it will simply see new values for whatever attributes were changed interactively. ``Panel`` thus allows you to provide notebook-specific, domain-specific interactive functionality without ever tying your domain-specific code to the notebook environment. However that is only the beginning!\"" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Parameter dependencies\n", + "Declaring parameters is usually only the beginning of a workflow, in most applications these parameters are then tied to some computation. To express the relationship between a computation and the parameters it depends on the ``param.depends`` decorator may be used on Parameterized methods. This provides a hint to ``panel`` and other libraries (e.g. HoloViews) that the method should be recomputed when a parameter changes.\n", + "\n", + "As a straightforward example without any additional dependencies we will write a small class which returns an ASCII representation of a sine wave, which depends on `phase` and `frequency` parameters. If we supply the ``.view`` method to a ``Panel`` layout it will automatically recompute and update the view when one or more of the parameters change:" + ] + }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ - "#Example.print_param_defaults() # see all parameter values" + "import numpy as np\n", + "\n", + "class Sine(param.Parameterized):\n", + "\n", + " phase = param.Number(default=0, bounds=(0, np.pi))\n", + "\n", + " frequency = param.Number(default=1, bounds=(0.1, 2))\n", + "\n", + " @param.depends('phase', 'frequency')\n", + " def view(self):\n", + " y = np.sin(np.linspace(0, np.pi*3, 40)*self.frequency+self.phase)\n", + " y = ((y-y.min())/y.ptp())*20\n", + " array = np.array([list((' '*(int(round(d))-1) + '*').ljust(20)) for d in y])\n", + " return pn.pane.Str('\\n'.join([''.join(r) for r in array.T]), height=325, width=500)\n", + "\n", + "sine = Sine(name='ASCII Sine Wave')\n", + "pn.Row(sine.param, sine.view)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "As you can see, you can access the parameter values at the class level from within the notebook to control behavior explicitly, e.g. to select what to show in subsequent cells. Moreover, any instances of the Parameterized classes in your own code will now use the new parameter values unless specifically overridden in that instance, so you can now import and use your domain-specific library however you like, knowing that it will use your interactive selections wherever those classes appear. None of the domain-specific code needs to know or care that you used Panel; it will simply see new values for whatever attributes were changed interactively. Panel thus allows you to provide notebook-specific, domain-specific interactive functionality without ever tying your domain-specific code to the notebook environment." + "The parameterized and annotated ``view`` method could return any one of the types handled by the [Pane objects](./Panes.ipynb) panel provides, making it easy to link parameters and their associated widgets to a plot or other output. Parameterized classes can therefore be a very useful pattern for encapsulating a part of a computational workflow with an associated visualization, declaratively expressing the dependencies between the parameters and the computation.\n", + "\n", + "Another common pattern is linking the values of one parameter to another parameter, e.g. when dependencies between parameters exist. In the example below we will define two parameters one for the continent and one for the country. Since we want the selection of valid countries to change when we change the continent we define a method to do that for us. In order to link the two we express the dependency using the ``param.depends`` decorator and then ensure that we want to run the method whenever the continent changes by setting ``watch=True``.\n", + "\n", + "Additionally we define a ``view`` method which returns an HTML iframe displaying the country in a Google Map." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "class GoogleMapViewer(param.Parameterized):\n", + " \n", + " continent = param.ObjectSelector(default='Asia', objects=['Africa', 'Asia', 'Europe'])\n", + " \n", + " country = param.ObjectSelector(default='China', objects=['China', 'Thailand', 'Japan'])\n", + " \n", + " _countries = {'Africa': ['Ghana', 'Togo', 'South Africa', 'Tanzania'],\n", + " 'Asia' : ['China', 'Thailand', 'Japan'],\n", + " 'Europe': ['Austria', 'Bulgaria', 'Greece', 'Portugal', 'Switzerland']}\n", + " \n", + " @param.depends('continent', watch=True)\n", + " def _update_countries(self):\n", + " countries = self._countries[self.continent]\n", + " self.params('country').objects = countries\n", + " self.country = countries[0]\n", + "\n", + " @param.depends('country')\n", + " def view(self):\n", + " iframe = \"\"\"\n", + " \n", + " \"\"\".format(country=self.country)\n", + " return pn.pane.HTML(iframe, height=400)\n", + " \n", + "viewer = GoogleMapViewer(name='Google Map Viewer')\n", + "pn.Row(viewer.param, viewer.view)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Whenever the continent changes it will now eagerly execute the ``_update_countries`` method changing the list of countries that is displayed, which in turn triggers an update in the view method updating the map.\n", + "\n", + "In this user guide we have seen how to leverage ``param`` to declare parameters, which ``panel`` can turn into a GUI with no overhead. Additionally we have seen how to link parameters to views and other parameters using the ``param.depends`` operator. This approach allows building complex and reactive ``panels``. In the [next user guide](Pipelines.ipynb) we will discover how to link multiple such classes into pipelines, making it possible to encapsulate complex workflows in clean self-contained classes." ] } ], diff --git a/examples/user_guide/Pipelines.ipynb b/examples/user_guide/Pipelines.ipynb new file mode 100644 index 0000000000..19da4b55d7 --- /dev/null +++ b/examples/user_guide/Pipelines.ipynb @@ -0,0 +1,229 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import param\n", + "import panel as pn\n", + "import holoviews as hv\n", + "\n", + "pn.extension()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The [Param user guide](Param.ipynb) described how to set up classes which declare parameters and link them to some computation or visualization. In this section we will discover how to connect multiple such panels into a ``Pipeline`` to express complex workflows where the output of one stage feeds into the next stage. To start using a ``Pipeline`` let us declare an empty one by instantiating the class:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "pipeline = pn.pipeline.Pipeline()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Having set up a Pipeline it is now possible to start populating it. While we have already seen how to declare a ``Parameterized`` class with parameters which are linked to some visualization or computation on a method using the ``param.depends`` decorator, ``Pipelines`` make use of another decorator and a convention for displaying the objects.\n", + "\n", + "The ``param.output`` decorator provides a way to annotate the methods on a class by declaring its outputs. A ``Pipeline`` uses this information to determine what outputs are available to be fed into the next stage of the workflow. In the example below the ``Stage1`` class has to parameters of its own (``a`` and ``b``) and one output, which is named ``c``. The signature of the decorator allows a number of different ways of declaring the outputs:\n", + "\n", + "* ``param.output()``: Declaring an output without arguments will declare that the method returns an output which will inherit the name of the method and does not make any specific type declarations.\n", + "* ``param.output(param.Number)``: Declaring an output with a specific ``Parameter`` or Python type also declares an output with the name of the method but declares that the output will be of a specific type.\n", + "* ``param.output(c=param.Number)``: Declaring an output using a keyword argument allows overriding the method name as the name of the output and declares the type.\n", + "\n", + "In the example below the output is simply the result of multiplying the two inputs (``a`` and ``b``) which will produce output ``c``. Additionally we declare a ``view`` method which returns a ``LaTeX`` pane which will render the equation to ``LaTeX``. Finally a ``panel`` method declares returns a ``panel`` object rendering both the parameters and the view; this is the second convention that a ``Pipeline`` expects.\n", + "\n", + "Let's start by displaying this stage on its own:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "class Stage1(param.Parameterized):\n", + " \n", + " a = param.Number(default=5, bounds=(0, 10))\n", + "\n", + " b = param.Number(default=5, bounds=(0, 10))\n", + " \n", + " @param.output(c=param.Number)\n", + " def output(self):\n", + " return self.a * self.b\n", + " \n", + " @param.depends('a', 'b')\n", + " def view(self):\n", + " return pn.pane.LaTeX('$%s * %s = %s$' % (self.a, self.b, self.output()))\n", + "\n", + " def panel(self):\n", + " return pn.Row(self.param, self.view)\n", + " \n", + "stage1 = Stage1()\n", + "stage1.panel()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To summarize we have followed several conventions when setting up this stage of our ``Pipeline``:\n", + "\n", + "1. Declare a Parameterized class with some input parameters\n", + "2. Declare one or more output methods and name them appropriately\n", + "3. Declare a ``panel`` method which returns a view of the object that the ``Pipeline`` can render\n", + "\n", + "Now that the object has been instantiated we can also query it for its outputs:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "stage1.param.outputs()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can see that ``Stage1`` declares an output of name ``c`` of ``Number`` type which can be accessed by calling the ``output`` method on the object. Now let us add this stage to our ``Pipeline`` using the ``add_stage`` method:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "pipeline.add_stage('Stage 1', stage1)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "A ``Pipeline`` with only a single stage is not much of a ``Pipeline`` of course, so it's time to set up a second stage, which consumes the outputs of the first. Recall that ``Stage1`` declares one output named ``c``, this means that if the output from ``Stage1`` should flow to ``Stage2``, the latter should declare a ``Parameter`` named ``c`` which will consume the output of the first stage. Below we therefore define parameters ``c`` and ``exp`` and since ``c`` is the output of the first stage the ``c`` parameter will be declared with a negative precedence stopping ``panel`` from generating a widget for it. Otherwise this class is very similar to the first one, it declares both a ``view`` method which depends on the parameters of the class and a ``panel`` method which returns a view of the object." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "class Stage2(param.Parameterized):\n", + " \n", + " c = param.Number(default=5, precedence=-1, bounds=(0, None))\n", + "\n", + " exp = param.Number(default=0.1, bounds=(0, 3))\n", + " \n", + " @param.depends('c', 'exp')\n", + " def view(self):\n", + " return pn.pane.LaTeX('${%s}^{%s}={%.3f}$' % (self.c, self.exp, self.c**self.exp))\n", + "\n", + " def panel(self):\n", + " return pn.Row(self.param, self.view)\n", + " \n", + "stage2 = Stage2(c=stage1.output())\n", + "stage2.panel()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now that we have declared the second stage of the pipeline let us add it to the ``Pipeline`` object:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "pipeline.add_stage('Stage 2', stage2)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "And that's it, we have no declared a two stage pipeline, where the output ``c`` flows from the first stage into the second stage. To display it we can now view the ``pipeline.layout``:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "pipeline.layout" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As you can see the ``Pipeline`` renders a little diagram displaying the available stages in the workflow along with previous and next buttons to move between each stage. This allows setting up complex workflows with multiple stages, where each component is a self-contained unit, with minimal declarations about its outputs (using the ``param.output`` decorator) and how to render it (by declaring a ``panel`` method).\n", + "\n", + "Above we created the ``Pipeline`` as we went along which makes some sense in a notebook, when deploying the Pipeline as a server app or when there's no reason to instantiate each stage separately it is also possible to declare the stages as part of the constructor:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "stages = [\n", + " ('Stage 1', Stage1),\n", + " ('Stage 2', Stage2)\n", + "]\n", + "\n", + "pipeline = pn.pipeline.Pipeline(stages)\n", + "pipeline.layout" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As you will note the Pipeline stages may be either ``Parameterized`` instances or classes, however when working with instances you must ensure that updating the parameters of the class is sufficient to update the current state of the class." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.6.6" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/panel/__init__.py b/panel/__init__.py index d8f5f5354b..af4696c5ef 100644 --- a/panel/__init__.py +++ b/panel/__init__.py @@ -1,7 +1,10 @@ from __future__ import absolute_import +import sys + from . import holoviews # noqa from . import layout # noqa +from . import pipeline # noqa from . import plotly # noqa from . import vega # noqa from . import widgets # noqa @@ -47,8 +50,19 @@ def __call__(self, *args, **params): "hv-extension-comm") _load_nb(p.inline) self._loaded = True + Viewable._comm_manager = JupyterCommManager + if 'holoviews' in sys.modules: + import holoviews as hv + if hv.extension._loaded: + return + import holoviews.plotting.bokeh # noqa + if hasattr(hv.Store, 'set_current_backend'): + hv.Store.set_current_backend('bokeh') + else: + hv.Store.current_backend = 'bokeh' + @classmethod def _process_comm_msg(cls, msg): """ diff --git a/panel/holoviews.py b/panel/holoviews.py index 94b182cfa8..7d2b77c004 100644 --- a/panel/holoviews.py +++ b/panel/holoviews.py @@ -69,7 +69,8 @@ def _cleanup(self, model=None, final=False): Traverses HoloViews object to find and clean up any streams connected to existing plots. """ - self._plots.pop(model.ref['id']).cleanup() + if model is not None: + self._plots.pop(model.ref['id']).cleanup() super(HoloViews, self)._cleanup(model, final) def _render(self, doc, comm, root): diff --git a/panel/param.py b/panel/param.py index 15dd1d2fc8..7561151cad 100644 --- a/panel/param.py +++ b/panel/param.py @@ -4,6 +4,7 @@ """ from __future__ import absolute_import +import re import os import json import types @@ -342,7 +343,10 @@ def _get_widgets(self): if self.expand_layout is Tabs: widgets = [] elif self.show_name: - widgets = [('name', [StaticText(value='{0}'.format(self.object.name))])] + name = self.object.name + match = re.match('(.)+(\d){5}', name) + name = name[:-5] if match else name + widgets = [('name', [StaticText(value='{0}'.format(name))])] else: widgets = [] widgets += [(pname, self.widget(pname)) for pname in ordered_params] diff --git a/panel/pipeline.py b/panel/pipeline.py new file mode 100644 index 0000000000..66abdee816 --- /dev/null +++ b/panel/pipeline.py @@ -0,0 +1,159 @@ +import os + +import param +import numpy as np + +from .holoviews import HoloViews +from .layout import Row, Column, Spacer +from .pane import Markdown, Pane +from .param import Param +from .util import param_reprs + + +class Pipeline(param.Parameterized): + """ + Allows connecting a linear series of panels to define a workflow. + Each stage in a pipeline should declare a panel method which + returns a panel object that can be displayed and annotate its + outputs using the param.output decorator. + """ + + next = param.Action(default=lambda x: x.param.trigger('next')) + + previous = param.Action(default=lambda x: x.param.trigger('previous')) + + def __init__(self, stages=[], **params): + try: + import holoviews as hv + except: + raise ImportError('Pipeline requires holoviews to be installed') + + self._stages = list(stages) + self._stage = 0 + super(Pipeline, self).__init__(**params) + self._error = Markdown('') + self._states = [] + self._state = None + self._progress_sel = hv.streams.Selection1D() + self._progress_sel.add_subscriber(self._set_stage) + prev_button = Param(self.param, parameters=['previous'], show_name=False) + next_button = Param(self.param, parameters=['next'], show_name=False) + self._progress_bar = Row(self._make_progress, prev_button, next_button) + spinner = Pane(os.path.join(os.path.dirname(__file__), 'assets', 'spinner.gif')) + self._spinner_layout = Row(Spacer(width=300), Column(Spacer(height=200), spinner)) + stage_layout = Row() + if len(stages): + stage_layout.append(self._init_stage()) + self._layout = Column(self._progress_bar, self._error, stage_layout) + + def add_stage(self, name, stage): + self._validate(stage) + self._stages.append((name, stage)) + if len(self._stages) == 1: + self.layout[2].append(self._init_stage()) + + def _validate(self, stage): + if any(stage is s for n, s in self._stages): + raise ValueError('Stage %s is already in pipeline' % stage) + elif not ((isinstance(stage, type) and issubclass(stage, param.Parameterized)) + or isinstance(stage, param.Parameterized)): + raise ValueError('Pipeline stages must be Parameterized classes or instances.') + + def __repr__(self): + repr_str = 'Pipeline:' + for i, (name, stage) in enumerate(self._stages): + if isinstance(stage, param.Parameterized): + cls_name = type(stage).__name__ + else: + cls_name = stage.__name__ + params = ', '.join(param_reprs(stage)) + repr_str += '\n [%d] %s: %s(%s)' % (i, name, cls_name, params) + return repr_str + + def __getitem__(self, index): + return self._stages[index][1] + + @property + def layout(self): + self._progress_bar[0] = self._make_progress + return self._layout + + + def _init_stage(self): + name, stage = self._stages[self._stage] + kwargs = {} + if self._state: + outputs = self._state.param.outputs().items() + kwargs = {name: method() for name, (_, method) in outputs + if name in stage.params()} + if isinstance(stage, param.Parameterized): + stage.set_param(**kwargs) + self._state = stage + else: + self._state = stage(**kwargs) + if len(self._states) <= self._stage: + self._states.append(self._state) + else: + self._states[self._stage] = self._state + return self._state.panel() + + def _set_stage(self, index): + idx = index[0] + steps = idx-self._stage + if steps < 0: + for i in range(abs(steps)): + self._previous() + else: + for i in range(steps): + self._next() + + @param.depends('next', watch=True) + def _next(self): + self._stage += 1 + prev_state = self.layout[2][0] + self.layout[2][0] = self._spinner_layout + try: + self.layout[2][0] = self._init_stage() + except Exception as e: + self._stage -= 1 + self._error.object = str(e) + self.layout[2][0] = prev_state + else: + self._error.object = '' + + @param.depends('previous', watch=True) + def _previous(self): + self._stage -= 1 + try: + self._state = self._states[self._stage] + self.layout[2][0] = self._state.panel() + except Exception as e: + self._stage += 1 + self._error.object = str(e) + else: + self._error.object = '' + + @param.depends('previous', 'next') + def _make_progress(self): + import holoviews as hv + import holoviews.plotting.bokeh # noqa + stages = len(self._stages) + line = hv.Path([[(0, 0), (stages-1, 0)]]).options( + line_width=10, color='black', backend='bokeh' + ) + vals = np.arange(stages) + active = [1 if v == self._stage else 0 for v in vals] + points = hv.Points((vals, np.zeros(stages), active), vdims=['active']).options( + color_index='active', line_color='black', cmap={0: 'white', 1: 'gray'}, + show_legend=False, size=20, default_tools=[], tools=['tap'], + nonselection_alpha=1, backend='bokeh' + ) + point_labels = points.add_dimension('text', 0, [n for n, _ in self._stages], vdim=True) + labels = hv.Labels(point_labels).options(yoffset=-2.5, backend='bokeh') + self._progress_sel.source = points + hv_plot = (line * points * labels).options( + xaxis=None, yaxis=None, width=800, show_frame=False, toolbar=None, + height=80, xlim=(-0.5, stages-0.5), ylim=(-4, 1.5), + clone=False, backend='bokeh' + ) + return HoloViews(hv_plot, backend='bokeh') diff --git a/panel/tests/test_param.py b/panel/tests/test_param.py index e5f0ac1e5c..7c02ec6a43 100644 --- a/panel/tests/test_param.py +++ b/panel/tests/test_param.py @@ -93,7 +93,7 @@ class Test(param.Parameterized): div = box.children[0] assert isinstance(div, Div) - assert div.text == ''+test.name+'' + assert div.text == ''+test.name[:-5]+'' def test_get_model_tabs(document, comm): diff --git a/panel/tests/test_pipeline.py b/panel/tests/test_pipeline.py new file mode 100644 index 0000000000..2f1f0ce652 --- /dev/null +++ b/panel/tests/test_pipeline.py @@ -0,0 +1,198 @@ +import pytest +import param + +from panel.layout import Row, Column +from panel.param import Param, ParamMethod +from panel.pipeline import Pipeline + +from .test_holoviews import hv_available + + +class Stage1(param.Parameterized): + + a = param.Number(default=5, bounds=(0, 10)) + + b = param.Number(default=5, bounds=(0, 10)) + + @param.output(c=param.Number) + def output(self): + return self.a * self.b + + @param.depends('a', 'b') + def view(self): + return '%s * %s = %s' % (self.a, self.b, self.output()) + + def panel(self): + return Row(self.param, self.view) + + +class Stage2(param.Parameterized): + + c = param.Number(default=5, precedence=-1, bounds=(0, None)) + + exp = param.Number(default=0.1, bounds=(0, 3)) + + @param.depends('c', 'exp') + def view(self): + return '%s^%s=%.3f' % (self.c, self.exp, self.c**self.exp) + + def panel(self): + return Row(self.param, self.view) + +@hv_available +def test_pipeline_from_classes(): + import holoviews as hv + + pipeline = Pipeline([('Stage 1', Stage1), ('Stage 2', Stage2)]) + + layout = pipeline.layout + + assert isinstance(layout, Column) + assert isinstance(layout[0], Row) + progress, prev_button, next_button = layout[0].objects + + assert isinstance(prev_button, Param) + assert isinstance(next_button, Param) + assert isinstance(progress, ParamMethod) + + hv_obj = progress.object().object + points = hv_obj.get(1) + assert isinstance(points, hv.Points) + assert len(points) == 2 + labels = hv_obj.get(2) + assert isinstance(labels, hv.Labels) + assert list(labels['text']) == ['Stage 1', 'Stage 2'] + + stage = layout[2][0] + assert isinstance(stage, Row) + assert isinstance(stage[1], ParamMethod) + assert stage[1].object() == '5 * 5 = 25' + + pipeline.param.trigger('next') + stage = layout[2][0] + assert isinstance(stage, Row) + assert isinstance(stage[1], ParamMethod) + assert stage[1].object() == '25^0.1=1.380' + + pipeline.param.trigger('previous') + stage = layout[2][0] + assert isinstance(stage, Row) + assert isinstance(stage[1], ParamMethod) + assert stage[1].object() == '5 * 5 = 25' + + +@hv_available +def test_pipeline_from_instances(): + import holoviews as hv + + pipeline = Pipeline([('Stage 1', Stage1()), ('Stage 2', Stage2())]) + + layout = pipeline.layout + + assert isinstance(layout, Column) + assert isinstance(layout[0], Row) + progress, prev_button, next_button = layout[0].objects + + assert isinstance(prev_button, Param) + assert isinstance(next_button, Param) + assert isinstance(progress, ParamMethod) + + hv_obj = progress.object().object + points = hv_obj.get(1) + assert isinstance(points, hv.Points) + assert len(points) == 2 + labels = hv_obj.get(2) + assert isinstance(labels, hv.Labels) + assert list(labels['text']) == ['Stage 1', 'Stage 2'] + + stage = layout[2][0] + assert isinstance(stage, Row) + assert isinstance(stage[1], ParamMethod) + assert stage[1].object() == '5 * 5 = 25' + + pipeline.param.trigger('next') + stage = layout[2][0] + assert isinstance(stage, Row) + assert isinstance(stage[1], ParamMethod) + assert stage[1].object() == '25^0.1=1.380' + + pipeline.param.trigger('previous') + stage = layout[2][0] + assert isinstance(stage, Row) + assert isinstance(stage[1], ParamMethod) + assert stage[1].object() == '5 * 5 = 25' + + +@hv_available +def test_pipeline_from_add_stages(): + import holoviews as hv + + pipeline = Pipeline() + pipeline.add_stage('Stage 1', Stage1) + pipeline.add_stage('Stage 2', Stage2) + + layout = pipeline.layout + + assert isinstance(layout, Column) + assert isinstance(layout[0], Row) + progress, prev_button, next_button = layout[0].objects + + assert isinstance(prev_button, Param) + assert isinstance(next_button, Param) + assert isinstance(progress, ParamMethod) + + hv_obj = progress.object().object + points = hv_obj.get(1) + assert isinstance(points, hv.Points) + assert len(points) == 2 + labels = hv_obj.get(2) + assert isinstance(labels, hv.Labels) + assert list(labels['text']) == ['Stage 1', 'Stage 2'] + + stage = layout[2][0] + assert isinstance(stage, Row) + assert isinstance(stage[1], ParamMethod) + assert stage[1].object() == '5 * 5 = 25' + + pipeline.param.trigger('next') + stage = layout[2][0] + assert isinstance(stage, Row) + assert isinstance(stage[1], ParamMethod) + assert stage[1].object() == '25^0.1=1.380' + + pipeline.param.trigger('previous') + stage = layout[2][0] + assert isinstance(stage, Row) + assert isinstance(stage[1], ParamMethod) + assert stage[1].object() == '5 * 5 = 25' + + +@hv_available +def test_pipeline_add_stage_validate_wrong_type(): + pipeline = Pipeline() + with pytest.raises(ValueError): + pipeline.add_stage('Stage 1', 1) + + +@hv_available +def test_pipeline_add_stage_validate_add_twice(): + pipeline = Pipeline() + pipeline.add_stage('Stage 1', Stage1) + with pytest.raises(ValueError): + pipeline.add_stage('Stage 1', Stage1) + + +@hv_available +def test_pipeline_getitem(): + pipeline = Pipeline() + pipeline.add_stage('Stage 1', Stage1) + assert pipeline[0] == Stage1 + + +@hv_available +def test_pipeline_repr(): + pipeline = Pipeline() + pipeline.add_stage('Stage 1', Stage1) + + pipeline.add_stage('Stage 2', Stage2) + assert repr(pipeline) == 'Pipeline:\n [0] Stage 1: Stage1()\n [1] Stage 2: Stage2()'