├── .appveyor.yml ├── .gitattributes ├── .gitignore ├── .travis.yml ├── LICENSE.txt ├── MANIFEST.in ├── README.md ├── conda.recipe ├── README.md └── meta.yaml ├── dodo.py ├── examples └── user_guide │ ├── AdditionalFeatures.ipynb │ ├── JSONInit.ipynb │ ├── images │ ├── intro.gif │ └── stock_dashboard.png │ └── index.ipynb ├── paramnb ├── __init__.py ├── __main__.py ├── tests │ ├── __init__.py │ └── test_dummy.py ├── util.py ├── view.py └── widgets.py ├── pyproject.toml ├── setup.cfg ├── setup.py └── tox.ini /.appveyor.yml: -------------------------------------------------------------------------------- 1 | environment: 2 | global: 3 | CHANS_DEV: "-c pyviz/label/dev" 4 | matrix: 5 | - PY: "2.7" 6 | CONDA: "C:\\Miniconda-x64" 7 | - PY: "3.6" 8 | CONDA: "C:\\Miniconda36-x64" 9 | 10 | install: 11 | - "SET PATH=%CONDA%;%CONDA%\\Scripts;%PATH%" 12 | - "conda install -y -c pyviz pyctdev && doit ecosystem_setup" 13 | - "doit env_create %CHANS_DEV% --name=test --python=%PY%" 14 | - "activate test" 15 | - "doit develop_install -o examples -o tests %CHANS_DEV%" 16 | - "doit env_capture" 17 | 18 | build: off 19 | 20 | test_script: 21 | - "doit test_all" 22 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | __init__.py export-subst 2 | setup.cfg export-subst 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | 55 | # Sphinx documentation 56 | docs/_build/ 57 | 58 | # PyBuilder 59 | target/ 60 | 61 | #Ipython Notebook 62 | .ipynb_checkpoints 63 | 64 | # Editor files 65 | *~ 66 | 67 | paramnb/.version 68 | .doit* 69 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | git: 2 | depth: 100 3 | 4 | language: generic 5 | 6 | os: 7 | - linux 8 | 9 | sudo: false 10 | 11 | env: 12 | global: 13 | - PYENV_VERSION=3.6 14 | - CHANS_DEV="-c pyviz/label/dev" 15 | - CHANS_REL="-c pyviz" 16 | - LABELS_DEV="--label dev" 17 | - LABELS_REL="--label dev --label main" 18 | - PKG_TEST_PYTHON="--test-python=py27 --test-python=py36" 19 | 20 | stages: 21 | - test 22 | - name: conda_dev_package 23 | if: tag =~ ^v(\d+|\.)*[a-z]\d*$ 24 | - name: pip_dev_package 25 | if: tag =~ ^v(\d+|\.)*[a-z]\d*$ 26 | - name: conda_package 27 | if: tag =~ ^v(\d+|\.)*[^a-z]\d*$ 28 | - name: pip_package 29 | if: tag =~ ^v(\d+|\.)*[^a-z]\d*$ 30 | 31 | 32 | jobs: 33 | include: 34 | 35 | ########## DEVELOPER INSTALL ########## 36 | 37 | - &conda_default 38 | stage: test 39 | env: DESC="dev test_all" 40 | before_install: 41 | # install doit/pyctdev and use to install miniconda... 42 | - pip install pyctdev && doit miniconda_install && pip uninstall -y doit pyctdev 43 | - export PATH="$HOME/miniconda/bin:$PATH" && hash -r 44 | - conda config --set always_yes True 45 | # ...and now install doit/pyctdev into miniconda 46 | - conda install -c pyviz pyctdev && doit ecosystem_setup 47 | install: 48 | - doit env_create $CHANS_DEV --python=$PYENV_VERSION 49 | - source activate test-environment 50 | - doit develop_install -o examples -o tests $CHANS_DEV 51 | - doit env_capture 52 | script: doit test_all 53 | 54 | # python 2 flake checking typically catches python 2 syntax 55 | # errors where python 3's been assumed... 56 | - <<: *conda_default 57 | env: DESC="py2 flakes" PYENV_VERSION=2.7 58 | script: doit test_lint 59 | 60 | ########## END-USER PACKAGES ########## 61 | 62 | ## dev packages 63 | 64 | - &pip_default 65 | env: PYPI=testpypi PYPIUSER=$TPPU PYPIPASS=$TPPP 66 | stage: pip_dev_package 67 | before_install: pip install pyctdev && doit ecosystem=pip ecosystem_setup 68 | install: 69 | - unset PYENV_VERSION && pyenv global 3.6 2.7 70 | - doit ecosystem=pip package_build $PKG_TEST_PYTHON --test-group=unit --sdist-install-build-deps 71 | - doit ecosystem=pip package_build $PKG_TEST_PYTHON --test-group=examples --sdist-install-build-deps 72 | script: doit ecosystem=pip package_upload -u $PYPIUSER -p $PYPIPASS --pypi ${PYPI} 73 | 74 | - &conda_pkg 75 | <<: *conda_default 76 | stage: conda_dev_package 77 | env: DESC="" LABELS=$LABELS_DEV CHANS=$CHANS_DEV 78 | install: 79 | - doit package_build $CHANS $PKG_TEST_PYTHON --test-group=unit 80 | - doit package_test $CHANS $PKG_TEST_PYTHON --test-group=examples --test-requires=examples 81 | script: doit package_upload --token=$ANACONDA_TOKEN $LABELS 82 | 83 | ## release packages 84 | 85 | - <<: *pip_default 86 | env: PYPI=pypi PYPIUSER=$PPU PYPIPASS=$PPP 87 | stage: pip_package 88 | 89 | - <<: *conda_pkg 90 | stage: conda_package 91 | env: DESC="" TRAVIS_NOCACHE=$TRAVIS_JOB_ID LABELS=$LABELS_REL CHANS=$CHANS_REL 92 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, IOAM 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * Neither the name of paramnb nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE.txt 2 | include README.md 3 | include paramnb/.version 4 | graft examples 5 | graft paramnb/examples 6 | global-exclude *.py[co] 7 | global-exclude *~ 8 | global-exclude *.ipynb_checkpoints/* 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/ioam/paramnb.svg?branch=master)](https://travis-ci.org/ioam/paramnb) 2 | 3 | [![Build status](https://ci.appveyor.com/api/projects/status/oyx4q6tqj74ld5b9/branch/master?svg=true)](https://ci.appveyor.com/project/Ioam/paramnb/branch/master) 4 | 5 | 6 | # ParamNB 7 | 8 | ### Note: ParamNB is no longer maintained; use the much more capable [Panel](https://github.com/pyviz/panel) library instead 9 | 10 | Automatically generate ipywidgets from 11 | [Parameterized](https://github.com/ioam/param) objects in a Jupyter 12 | notebook. 13 | 14 | ![Auto-generated 15 | widgets](https://github.com/ioam/paramnb/blob/master/examples/user_guide/images/intro.gif) 16 | 17 | 18 | ## Demo notebooks 19 | 20 | The following links go to static versions of the notebooks, so after 21 | looking at them it's best to try them out yourself interactively! 22 | 23 | * Introduction: [examples/user_guide/index.ipynb](http://nbviewer.jupyter.org/urls/notebooks.anaconda.org/cball/ioam-paramnb-index/download?version=) 24 | 25 | * Additional Features: [examples/user_guide/AdditionalFeatures.ipynb](http://nbviewer.jupyter.org/urls/notebooks.anaconda.org/cball/ioam-paramnb-additionalfeatures/download?version=) 26 | 27 | * Setting parameters via JSON: [examples/user_guide/JSONInit.ipynb](http://nbviewer.jupyter.org/urls/notebooks.anaconda.org/cball/ioam-paramnb-jsoninit/download?version=) 28 | 29 | 30 | ## Installation 31 | 32 | You can easily install paramnb using conda: 33 | 34 | ``` 35 | conda install -c pyviz paramnb 36 | ``` 37 | 38 | or pip: 39 | 40 | ``` 41 | pip install paramnb 42 | ``` 43 | -------------------------------------------------------------------------------- /conda.recipe/README.md: -------------------------------------------------------------------------------- 1 | ## Release Procedure 2 | 3 | - Ensure all tests pass. 4 | 5 | - Update version number in `conda.recipe/meta.yaml`, `paramnb/__init__.py`, 6 | and `setup.py`. Commit. 7 | 8 | - Tag commit and push to github 9 | 10 | ```bash 11 | git tag -a x.x.x -m 'Version x.x.x' 12 | git push upstream master --tags 13 | ``` 14 | 15 | - Build conda packages 16 | 17 | The exact procedure is platform/setup specific, so I'll define a few variables 18 | here, to fill in with your specifics: 19 | 20 | ```bash 21 | # Location of your conda install. For me it's `~/miniconda/` 22 | CONDA_DIR=~/miniconda/ 23 | 24 | # Platform code. For me it's `osx-64` 25 | PLATFORM=osx-64 26 | 27 | # Version number of paramnb being released (e.g. 2.0.2) 28 | VERSION=2.0.2 29 | ``` 30 | 31 | This assumes `conda`, `conda-build`, and `anaconda-client` are installed (if 32 | not, install `conda`, then use `conda` to install the others). From inside the 33 | toplevel directory: 34 | 35 | ```bash 36 | conda build conda.recipe/ --python 2.7 --python 3.4 --python 3.5 37 | ``` 38 | 39 | Next, `cd` into the folder where the builds end up. 40 | 41 | ```bash 42 | cd $CONDA_DIR/conda-bld/$PLATFORM 43 | ``` 44 | 45 | Use `conda convert` to convert over the missing platforms (skipping the one for 46 | the platform you're currently on): 47 | 48 | ```bash 49 | conda convert --platform osx-64 paramnb-$VERSION*.tar.bz2 -o ../ 50 | conda convert --platform linux-32 paramnb-$VERSION*.tar.bz2 -o ../ 51 | conda convert --platform linux-64 paramnb-$VERSION*.tar.bz2 -o ../ 52 | conda convert --platform win-32 paramnb-$VERSION*.tar.bz2 -o ../ 53 | conda convert --platform win-64 paramnb-$VERSION*.tar.bz2 -o ../ 54 | ``` 55 | 56 | Use `anaconda upload` to upload the build to the `ioam` channel. This requires 57 | you to be setup on `anaconda.org`, and have the proper credentials to push to 58 | the bokeh channel. 59 | 60 | ```bash 61 | anaconda login 62 | anaconda upload $CONDA_DIR/conda-bld/*/paramnb-$VERSION*.tar.bz2 -u ioam 63 | ``` 64 | -------------------------------------------------------------------------------- /conda.recipe/meta.yaml: -------------------------------------------------------------------------------- 1 | {% set sdata = load_setup_py_data() %} 2 | 3 | package: 4 | name: paramnb 5 | version: {{ sdata['version'] }} 6 | 7 | source: 8 | path: .. 9 | 10 | build: 11 | noarch: python 12 | script: python setup.py install --single-version-externally-managed --record=record.txt 13 | entry_points: 14 | {% for group,epoints in sdata.get("entry_points",{}).items() %} 15 | {% for entry_point in epoints %} 16 | - {{ entry_point }} 17 | {% endfor %} 18 | {% endfor %} 19 | 20 | requirements: 21 | host: 22 | # duplicates pyproject.toml (not supported in conda build) 23 | - python 24 | - setuptools >=30.3.0 25 | - param >=1.7.0 26 | - pyct-core >=0.4.4 27 | run: 28 | - python {{ sdata['python_requires'] }} 29 | {% for dep in sdata.get('install_requires',{}) %} 30 | - "{{ dep }}" 31 | {% endfor %} 32 | 33 | test: 34 | imports: 35 | - paramnb 36 | requires: 37 | {% for dep in sdata['extras_require']['tests'] %} 38 | - "{{ dep }}" 39 | {% endfor %} 40 | 41 | about: 42 | home: {{ sdata['url'] }} 43 | summary: {{ sdata['description'] }} 44 | license: {{ sdata['license'] }} 45 | -------------------------------------------------------------------------------- /dodo.py: -------------------------------------------------------------------------------- 1 | import os 2 | if "PYCTDEV_ECOSYSTEM" not in os.environ: 3 | os.environ["PYCTDEV_ECOSYSTEM"] = "conda" 4 | 5 | from pyctdev import * # noqa: api 6 | -------------------------------------------------------------------------------- /examples/user_guide/AdditionalFeatures.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "import param\n", 10 | "import paramnb" 11 | ] 12 | }, 13 | { 14 | "cell_type": "markdown", 15 | "metadata": {}, 16 | "source": [ 17 | "# Tooltips\n", 18 | "\n", 19 | "Doc strings supplied when a parameter is declared are displayed as a 'tooltip' when the mouse hovers over the widget label (``x`` in the widget below)" 20 | ] 21 | }, 22 | { 23 | "cell_type": "code", 24 | "execution_count": null, 25 | "metadata": {}, 26 | "outputs": [], 27 | "source": [ 28 | "class TooltipExample(param.Parameterized):\n", 29 | " x = param.Number(default=1,bounds=(0,2),doc=\"X position\")" 30 | ] 31 | }, 32 | { 33 | "cell_type": "code", 34 | "execution_count": null, 35 | "metadata": {}, 36 | "outputs": [], 37 | "source": [ 38 | "paramnb.Widgets(TooltipExample)" 39 | ] 40 | }, 41 | { 42 | "cell_type": "markdown", 43 | "metadata": {}, 44 | "source": [ 45 | "# Recursive editing\n", 46 | "\n", 47 | "Parameter types such as ``param.ObjectSelector`` allow selection of another Parameterized object; paramNB also allows the parameters of the selected object to be edited by clicking the **...** button next to any selected Parameterized object:" 48 | ] 49 | }, 50 | { 51 | "cell_type": "code", 52 | "execution_count": null, 53 | "metadata": {}, 54 | "outputs": [], 55 | "source": [ 56 | "class Location(param.Parameterized):\n", 57 | " duration = param.Integer(default=5, bounds=(0,10))\n", 58 | "\n", 59 | "locations = [Location(name=\"One\"),Location(name=\"Two\")]\n", 60 | "\n", 61 | "class Employee(param.Parameterized):\n", 62 | " age = param.Integer(default=30,bounds=(18,100))\n", 63 | " location = param.ObjectSelector(default=locations[0],objects=locations)\n", 64 | "\n", 65 | "class Task(param.Parameterized):\n", 66 | " employee = param.ObjectSelector(objects=[Employee(name=\"A\"),Employee(name=\"B\")])" 67 | ] 68 | }, 69 | { 70 | "cell_type": "code", 71 | "execution_count": null, 72 | "metadata": {}, 73 | "outputs": [], 74 | "source": [ 75 | "paramnb.Widgets(Task)" 76 | ] 77 | }, 78 | { 79 | "cell_type": "code", 80 | "execution_count": null, 81 | "metadata": {}, 82 | "outputs": [], 83 | "source": [ 84 | "Task.employee.location.duration" 85 | ] 86 | }, 87 | { 88 | "cell_type": "markdown", 89 | "metadata": {}, 90 | "source": [ 91 | "# View parameters" 92 | ] 93 | }, 94 | { 95 | "cell_type": "markdown", 96 | "metadata": {}, 97 | "source": [ 98 | "The paramNB library provides an easy way to manipulate parameters on ``Parameterized`` using the widgets in the notebook. In addition to controlling input parameters a common usecase for using widgets in the notebook is to dynamically control some visual display output. In addition to all the standard parameters supplied by the ``param`` library, ``paramNB`` also supplies so called ``View`` parameters, which render their output in a widget area. The output parameters may be updated simply by setting the parameter on the class.\n", 99 | "\n", 100 | "In the first simple example we will declare a Parameterized class with a ``Number`` parameter called magnitude and an ``HTML`` parameter which will let us display some arbitrary HTML. In this case we will simply generate a pandas dataframe with random data within the update method and use the ``to_html`` method to convert it to an HTML table. If we define the ``update`` method as the callback of the widgets the table will now update whenever the slider is dragged. To ensure that the output is drawn on initialization we set ``on_init=True``." 101 | ] 102 | }, 103 | { 104 | "cell_type": "code", 105 | "execution_count": null, 106 | "metadata": {}, 107 | "outputs": [], 108 | "source": [ 109 | "import numpy as np\n", 110 | "import pandas as pd" 111 | ] 112 | }, 113 | { 114 | "cell_type": "code", 115 | "execution_count": null, 116 | "metadata": {}, 117 | "outputs": [], 118 | "source": [ 119 | "class HTMLExample(param.Parameterized):\n", 120 | " \n", 121 | " magnitude = param.Number(1, bounds=(0, 10))\n", 122 | "\n", 123 | " output = paramnb.view.HTML()\n", 124 | " \n", 125 | " def update(self, **kwargs):\n", 126 | " self.output = pd.DataFrame(np.random.rand(10,2)*self.magnitude).to_html()\n", 127 | "\n", 128 | "example = HTMLExample(name='HTMLExample')\n", 129 | "paramnb.Widgets(example, on_init=True, callback=example.update)" 130 | ] 131 | }, 132 | { 133 | "cell_type": "markdown", 134 | "metadata": {}, 135 | "source": [ 136 | "The ``HTML`` parameter accepts any arbitrary HTML string but for convenience paramNB also allows supplying a custom ``renderer`` function, which converts the view data to HTML. In this case we declare to parameters to control the ``amplitude`` and ``frequency`` of a sine curve and then declare an ``HTML`` parameter which uses a HoloViews MPLRenderer to render the output. Note that we can additionally supply the size of the output as a tuple of ``(width, height)`` in pixels, in this case (300, 300).\n", 137 | "\n", 138 | "Additionally we can declare the ``view_position``, which specifies where the viewing widget will be placed in relation to the input widgets:" 139 | ] 140 | }, 141 | { 142 | "cell_type": "code", 143 | "execution_count": null, 144 | "metadata": {}, 145 | "outputs": [], 146 | "source": [ 147 | "import holoviews as hv\n", 148 | "renderer = hv.renderer('matplotlib')\n", 149 | "\n", 150 | "class ImageExample(param.Parameterized):\n", 151 | "\n", 152 | " color = param.Color(default='#000000', precedence=0)\n", 153 | "\n", 154 | " element = param.ObjectSelector(default=hv.Curve,\n", 155 | " objects=[hv.Curve, hv.Scatter, hv.Area],\n", 156 | " precedence=0)\n", 157 | "\n", 158 | " amplitude = param.Number(default=2, bounds=(2, 5))\n", 159 | " \n", 160 | " frequency = param.Number(default=2, bounds=(1, 10))\n", 161 | " \n", 162 | " output = paramnb.view.Image(renderer=lambda x: (renderer(x)[0], (300, 300)))\n", 163 | "\n", 164 | " def update(self, **kwargs):\n", 165 | " self.output = self.element(self.amplitude*np.sin(np.linspace(0, np.pi*self.frequency)),\n", 166 | " vdims=[hv.Dimension('y', range=(-5, 5))])(style=dict(color=self.color))\n", 167 | "\n", 168 | "example = ImageExample(name='HoloViews Example')\n", 169 | "paramnb.Widgets(example, callback=example.update, on_init=True, view_position='right')" 170 | ] 171 | }, 172 | { 173 | "cell_type": "markdown", 174 | "metadata": {}, 175 | "source": [ 176 | "Finally, the generic ``View`` parameter may also be used to display the rich ``repr`` of any object, effectively mirroring the output of IPython's ``display`` function. If we load the HoloViews extension with the bokeh backend, and subclass the ImageExample, we can also render bokeh plots in this way:" 177 | ] 178 | }, 179 | { 180 | "cell_type": "code", 181 | "execution_count": null, 182 | "metadata": {}, 183 | "outputs": [], 184 | "source": [ 185 | "hv.extension('bokeh')\n", 186 | "\n", 187 | "class ViewExample(ImageExample):\n", 188 | " \n", 189 | " output = paramnb.view.View()\n", 190 | " \n", 191 | "example = ViewExample(name='HoloViews+Bokeh Example')\n", 192 | "paramnb.Widgets(example, callback=example.update, on_init=True, view_position='right')" 193 | ] 194 | } 195 | ], 196 | "metadata": { 197 | "language_info": { 198 | "name": "python", 199 | "pygments_lexer": "ipython3" 200 | } 201 | }, 202 | "nbformat": 4, 203 | "nbformat_minor": 2 204 | } 205 | -------------------------------------------------------------------------------- /examples/user_guide/JSONInit.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Setting parameters via JSON" 8 | ] 9 | }, 10 | { 11 | "cell_type": "markdown", 12 | "metadata": {}, 13 | "source": [ 14 | "\n", 15 | "\n", 16 | "For all the examples in this notebook to work, launch the notebook server using:\n", 17 | "\n", 18 | "```\n", 19 | "PARAMNB_INIT='{\"p1\":5}' \\\n", 20 | " TARGETED='{\"Target1\":{\"p1\":3}, \"Target2\":{\"s\":\"test\"}}' \\\n", 21 | " CUSTOM='{\"custom\":{\"val\":99}}' jupyter notebook\n", 22 | "```\n", 23 | "\n" 24 | ] 25 | }, 26 | { 27 | "cell_type": "code", 28 | "execution_count": null, 29 | "metadata": {}, 30 | "outputs": [], 31 | "source": [ 32 | "import os\n", 33 | "import param\n", 34 | "import paramnb" 35 | ] 36 | }, 37 | { 38 | "cell_type": "markdown", 39 | "metadata": {}, 40 | "source": [ 41 | "## Example 1" 42 | ] 43 | }, 44 | { 45 | "cell_type": "markdown", 46 | "metadata": {}, 47 | "source": [ 48 | "The first minimal example will work if the notebook server is launched as follows:\n", 49 | "\n", 50 | "```\n", 51 | "PARAMNB_INIT='{\"p1\":5}' jupyter notebook\n", 52 | "```\n", 53 | "\n", 54 | "First let's show that the ``'PARAMNB_INIT'`` environment is defined:" 55 | ] 56 | }, 57 | { 58 | "cell_type": "code", 59 | "execution_count": null, 60 | "metadata": {}, 61 | "outputs": [], 62 | "source": [ 63 | "os.environ['PARAMNB_INIT']" 64 | ] 65 | }, 66 | { 67 | "cell_type": "markdown", 68 | "metadata": {}, 69 | "source": [ 70 | "This string is JSON and the 'PARAMNB_INIT' is the default environment variable name to set parameters via the commandline. Lets make a simple parameterized class with a ``p1`` parameter:" 71 | ] 72 | }, 73 | { 74 | "cell_type": "code", 75 | "execution_count": null, 76 | "metadata": {}, 77 | "outputs": [], 78 | "source": [ 79 | "class Test(param.Parameterized):\n", 80 | " \n", 81 | " p1 = param.Number(default=1, bounds=(0,10))" 82 | ] 83 | }, 84 | { 85 | "cell_type": "markdown", 86 | "metadata": {}, 87 | "source": [ 88 | "Now if we supply ``paramnb.JSONInit`` as an initializer, the ``p1`` parameter is set from the default of 1 to the value of 5 specified by the environment variable:" 89 | ] 90 | }, 91 | { 92 | "cell_type": "code", 93 | "execution_count": null, 94 | "metadata": {}, 95 | "outputs": [], 96 | "source": [ 97 | "paramnb.Widgets(Test, initializer=paramnb.JSONInit())" 98 | ] 99 | }, 100 | { 101 | "cell_type": "markdown", 102 | "metadata": {}, 103 | "source": [ 104 | "## Example 2" 105 | ] 106 | }, 107 | { 108 | "cell_type": "markdown", 109 | "metadata": {}, 110 | "source": [ 111 | "The second example will work if the notebook server is launched as follows:\n", 112 | "\n", 113 | "```\n", 114 | "TARGETED='{\"Target1\":{\"p1\":3}, \"Target2\":{\"s\":\"test\"}}' jupyter notebook\n", 115 | "```\n", 116 | "\n", 117 | "In this example, we show how you can target parameters to different classes using a different environment variable called ``TARGETED``:" 118 | ] 119 | }, 120 | { 121 | "cell_type": "code", 122 | "execution_count": null, 123 | "metadata": {}, 124 | "outputs": [], 125 | "source": [ 126 | "os.environ['TARGETED']" 127 | ] 128 | }, 129 | { 130 | "cell_type": "markdown", 131 | "metadata": {}, 132 | "source": [ 133 | "Here the keys are class names and the corresponding dictionary values are the parameter values to override. Let's defined classes ``Target1`` and ``Target2`` with parameters ``p1`` and ``s`` respectively:" 134 | ] 135 | }, 136 | { 137 | "cell_type": "code", 138 | "execution_count": null, 139 | "metadata": {}, 140 | "outputs": [], 141 | "source": [ 142 | "class Target1(param.Parameterized):\n", 143 | " \n", 144 | " p1 = param.Number(default=1, bounds=(0,10))\n", 145 | "\n", 146 | "class Target2(param.Parameterized):\n", 147 | " \n", 148 | " s = param.String(default=\"default\")\n" 149 | ] 150 | }, 151 | { 152 | "cell_type": "markdown", 153 | "metadata": {}, 154 | "source": [ 155 | "Now lets use ``paramnb.Widgets`` on ``Target1``:" 156 | ] 157 | }, 158 | { 159 | "cell_type": "code", 160 | "execution_count": null, 161 | "metadata": {}, 162 | "outputs": [], 163 | "source": [ 164 | "paramnb.Widgets(Target1, initializer=paramnb.JSONInit(varname='TARGETED'))" 165 | ] 166 | }, 167 | { 168 | "cell_type": "markdown", 169 | "metadata": {}, 170 | "source": [ 171 | "The value of ``p1`` is now ``3`` as requested.\n", 172 | "\n", 173 | "Now lets use ``paramnb.Widgets`` on ``Target2``:" 174 | ] 175 | }, 176 | { 177 | "cell_type": "code", 178 | "execution_count": null, 179 | "metadata": {}, 180 | "outputs": [], 181 | "source": [ 182 | "paramnb.Widgets(Target2, initializer=paramnb.JSONInit(varname='TARGETED'))" 183 | ] 184 | }, 185 | { 186 | "cell_type": "markdown", 187 | "metadata": {}, 188 | "source": [ 189 | "## Example 3" 190 | ] 191 | }, 192 | { 193 | "cell_type": "markdown", 194 | "metadata": {}, 195 | "source": [ 196 | "\n", 197 | "The third example will work if the notebook server is launched as follows:\n", 198 | "\n", 199 | "```\n", 200 | "CUSTOM='{\"custom\":{\"val\":99}}' jupyter notebook\n", 201 | "```\n", 202 | "\n", 203 | "In this example, we show how you can target a specific instance using an environment variable called ``CUSTOM``:" 204 | ] 205 | }, 206 | { 207 | "cell_type": "code", 208 | "execution_count": null, 209 | "metadata": {}, 210 | "outputs": [], 211 | "source": [ 212 | "os.environ['CUSTOM']" 213 | ] 214 | }, 215 | { 216 | "cell_type": "code", 217 | "execution_count": null, 218 | "metadata": {}, 219 | "outputs": [], 220 | "source": [ 221 | "class Example(param.Parameterized):\n", 222 | " \n", 223 | " val = param.Number(default=1, bounds=(0,100))\n", 224 | " \n", 225 | "instance = Example()\n", 226 | "paramnb.Widgets(instance, initializer=paramnb.JSONInit(varname='CUSTOM', target='custom'))" 227 | ] 228 | }, 229 | { 230 | "cell_type": "markdown", 231 | "metadata": {}, 232 | "source": [ 233 | "## Example 4" 234 | ] 235 | }, 236 | { 237 | "cell_type": "markdown", 238 | "metadata": {}, 239 | "source": [ 240 | "You can also use a JSON file ending with the '.json' extension. For instance, if you execute:" 241 | ] 242 | }, 243 | { 244 | "cell_type": "code", 245 | "execution_count": null, 246 | "metadata": {}, 247 | "outputs": [], 248 | "source": [ 249 | "import json\n", 250 | "json.dump({\"p1\":5}, open('param_init.json', 'w'))" 251 | ] 252 | }, 253 | { 254 | "cell_type": "markdown", 255 | "metadata": {}, 256 | "source": [ 257 | "You cam specify the full path or relative path to the JSON file with:\n", 258 | "\n", 259 | "\n", 260 | "```\n", 261 | "PARAMNB_INIT=param_init.json jupyter notebook\n", 262 | "```" 263 | ] 264 | }, 265 | { 266 | "cell_type": "code", 267 | "execution_count": null, 268 | "metadata": {}, 269 | "outputs": [], 270 | "source": [ 271 | "os.environ['PARAMNB_INIT']" 272 | ] 273 | }, 274 | { 275 | "cell_type": "code", 276 | "execution_count": null, 277 | "metadata": {}, 278 | "outputs": [], 279 | "source": [ 280 | "class Test(param.Parameterized):\n", 281 | " \n", 282 | " p1 = param.Number(default=1, bounds=(0,10))\n", 283 | " \n", 284 | "paramnb.Widgets(Test, initializer=paramnb.JSONInit())" 285 | ] 286 | }, 287 | { 288 | "cell_type": "markdown", 289 | "metadata": {}, 290 | "source": [ 291 | "Note that you can use ``JSONInit`` without setting any environment variables by specifying the JSON file directly:" 292 | ] 293 | }, 294 | { 295 | "cell_type": "code", 296 | "execution_count": null, 297 | "metadata": {}, 298 | "outputs": [], 299 | "source": [ 300 | "paramnb.Widgets(Test, initializer=paramnb.JSONInit(json_file='param_init.json'))" 301 | ] 302 | }, 303 | { 304 | "cell_type": "markdown", 305 | "metadata": {}, 306 | "source": [ 307 | "## Tips" 308 | ] 309 | }, 310 | { 311 | "cell_type": "markdown", 312 | "metadata": {}, 313 | "source": [ 314 | "\n", 315 | "* It is recommended that you target at the class or instance level if you ever intend to use ``JSONInit`` to customize different sets of parameters.\n", 316 | "\n", 317 | "* It is recommended that you validate (and pretty print) the JSON at the commandline using ``json.tool``. For instance, you can validate the JSON for the first example before launching the server as follows:\n", 318 | "\n", 319 | "```\n", 320 | "PARAMNB_INIT=`echo '{\"p1\":5}' | python -mjson.tool` jupyter notebook\n", 321 | "```" 322 | ] 323 | } 324 | ], 325 | "metadata": { 326 | "language_info": { 327 | "name": "python", 328 | "pygments_lexer": "ipython3" 329 | } 330 | }, 331 | "nbformat": 4, 332 | "nbformat_minor": 0 333 | } 334 | -------------------------------------------------------------------------------- /examples/user_guide/images/intro.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ioam/paramnb/bdf18566d6279d6612537fadaa5460e1961aa359/examples/user_guide/images/intro.gif -------------------------------------------------------------------------------- /examples/user_guide/images/stock_dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ioam/paramnb/bdf18566d6279d6612537fadaa5460e1961aa359/examples/user_guide/images/stock_dashboard.png -------------------------------------------------------------------------------- /examples/user_guide/index.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "[ParamNB](https://github.com/ioam/paramnb) is a small library that represents Parameters graphically in a Jupyter notebook. Parameters are Python attributes extended using the [Param library](https://github.com/ioam/param) to support types, ranges, and documentation, which turns out to be just the information you need to automatically create widgets for each parameter. ParamNB currently uses [ipywidgets](https://ipywidgets.readthedocs.io) to display the widgets, but the design of Param and ParamNB allows your code to be completely independent of the underlying widgets library, and ParamNB can be updated to use other widget libraries as they are developed without needing changes in your code.\n", 8 | "\n", 9 | "# Parameters and widgets\n", 10 | "\n", 11 | "To use ParamNB, first declare some Parameterized classes with various Parameters:" 12 | ] 13 | }, 14 | { 15 | "cell_type": "code", 16 | "execution_count": null, 17 | "metadata": {}, 18 | "outputs": [], 19 | "source": [ 20 | "import param\n", 21 | "import datetime as dt\n", 22 | "\n", 23 | "def hello(x, **kwargs):\n", 24 | " print(\"Hello %s\" % x)\n", 25 | " \n", 26 | "class BaseClass(param.Parameterized):\n", 27 | " x = param.Parameter(default=3.14,doc=\"X position\")\n", 28 | " y = param.Parameter(default=\"Not editable\",constant=True)\n", 29 | " string_value = param.String(default=\"str\",doc=\"A string\")\n", 30 | " num_int = param.Integer(50000,bounds=(-200,100000))\n", 31 | " unbounded_int = param.Integer(23)\n", 32 | " float_with_hard_bounds = param.Number(8.2,bounds=(7.5,10))\n", 33 | " float_with_soft_bounds = param.Number(0.5,bounds=(0,None),softbounds=(0,2))\n", 34 | " unbounded_float = param.Number(30.01,precedence=0)\n", 35 | " hidden_parameter = param.Number(2.718,precedence=-1)\n", 36 | " integer_range = param.Range(default=(3,7),bounds=(0, 10))\n", 37 | " float_range = param.Range(default=(0,1.57),bounds=(0, 3.145))\n", 38 | " dictionary = param.Dict(default={\"a\":2, \"b\":9})\n", 39 | " \n", 40 | "class Example(BaseClass):\n", 41 | " \"\"\"An example Parameterized class\"\"\"\n", 42 | " boolean = param.Boolean(True, doc=\"A sample Boolean parameter\")\n", 43 | " color = param.Color(default='#FFFFFF')\n", 44 | " date = param.Date(dt.datetime(2017, 1, 1),\n", 45 | " bounds=(dt.datetime(2017, 1, 1), dt.datetime(2017, 2, 1)))\n", 46 | " select_string = param.ObjectSelector(default=\"yellow\",objects=[\"red\",\"yellow\",\"green\"])\n", 47 | " select_fn = param.ObjectSelector(default=list,objects=[list,set,dict])\n", 48 | " int_list = param.ListSelector(default=[3,5], objects=[1,3,5,7,9],precedence=0.5)\n", 49 | " single_file = param.FileSelector(path='../*/*.py*',precedence=0.5)\n", 50 | " multiple_files = param.MultiFileSelector(path='../*/*.py?',precedence=0.5)\n", 51 | " msg = param.Action(hello, doc=\"\"\"Print a message.\"\"\",precedence=0.7)\n", 52 | " \n", 53 | "Example.num_int" 54 | ] 55 | }, 56 | { 57 | "cell_type": "markdown", 58 | "metadata": {}, 59 | "source": [ 60 | "As you can see, declaring Parameters depends only on the separate Param library. Parameters are a simple idea with some properties that are crucial for helping you create clean, usable code:\n", 61 | "\n", 62 | "- The Param library is pure Python with no dependencies, which makes it easy to include in any code without tying it to a particular GUI or widgets library, or even to the Jupyter notebook. \n", 63 | "- Parameter declarations focus on semantic information relevant to your domain, allowing you to avoid polluting your domain-specific code with anything that ties it to a particular way of displaying or interacting with it. \n", 64 | "- Parameters can be defined wherever they make sense in your inheritance hierarchy, allowing you to document, type, and range-limit them once, with all of those properties inherited by any base class. E.g. parameters work the same here whether they were declared in `BaseClass` or `Example`, which makes it easy to provide this metadata once, and avoiding duplicating it throughout the code wherever ranges or types need checking or documentation needs to be stored.\n", 65 | "\n", 66 | "If you then decide to use these Parameterized classes in a notebook environment, you can import ParamNB and easily display and edit the parameter values as an optional additional step:" 67 | ] 68 | }, 69 | { 70 | "cell_type": "code", 71 | "execution_count": null, 72 | "metadata": {}, 73 | "outputs": [], 74 | "source": [ 75 | "import paramnb\n", 76 | "paramnb.Widgets(Example)" 77 | ] 78 | }, 79 | { 80 | "cell_type": "markdown", 81 | "metadata": {}, 82 | "source": [ 83 | "As you can see, `paramnb.Widgets()` 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 ParamNB 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", 84 | "\n", 85 | "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", 86 | "\n", 87 | "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:" 88 | ] 89 | }, 90 | { 91 | "cell_type": "code", 92 | "execution_count": null, 93 | "metadata": {}, 94 | "outputs": [], 95 | "source": [ 96 | "Example.unbounded_int" 97 | ] 98 | }, 99 | { 100 | "cell_type": "code", 101 | "execution_count": null, 102 | "metadata": {}, 103 | "outputs": [], 104 | "source": [ 105 | "Example.num_int" 106 | ] 107 | }, 108 | { 109 | "cell_type": "code", 110 | "execution_count": null, 111 | "metadata": {}, 112 | "outputs": [], 113 | "source": [ 114 | "#Example.print_param_defaults() # see all parameter values" 115 | ] 116 | }, 117 | { 118 | "cell_type": "markdown", 119 | "metadata": {}, 120 | "source": [ 121 | "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 ParamNB; it will simply see new values for whatever attributes were changed interactively. ParamNB thus allows you to provide notebook-specific, domain-specific interactive functionality without ever tying your domain-specific code to the notebook environment.\n", 122 | "\n", 123 | "\n", 124 | "# Controlling code execution\n", 125 | "\n", 126 | "If you do `Run All` in the notebook instead of running cell by cell, you won't get any opportunity to interact with the widgets until the notebook has completed, and so any values you change will only take effect if you then do a separate `Run All Below` command to update the results of subsequent cells. \n", 127 | "\n", 128 | "Having to work cell by cell or re-run the notebook manually can be awkward, especially when building dashboards that hide the notebook user interface (such as with [Jupyter Dashboards](https://github.com/jupyter/dashboards)). In order to provide \"live\" or dynamic updating, ParamNB also allows you to control code (re-)execution automatically in various ways. First, you can define *what* code will be executed:\n", 129 | "\n", 130 | "* `callback`=*callable*: User-defined function to call, if any\n", 131 | "* `next_n`=*n*: zero by default, but if set to e.g. 2, will execute the subsequent 2 cells of the notebook\n", 132 | "\n", 133 | "You can also define *when* the code will be executed:\n", 134 | "\n", 135 | "* `button=False`: the default; the specified code will be executed whenever a widget value is changed\n", 136 | "* `button=True`: Provide a button to control code execution, so that multiple widgets can be adjusted and code is updated only when the button is pushed.\n", 137 | "* `continuous_update=True`: the specified code is executed for every movement of a slider\n", 138 | "* `continuous_update=False`: the default; the specified code is executed only once a widget has been released\n", 139 | "\n", 140 | "These options allow you to choose between various levels of dynamic interactivity, as appropriate for the computational and semantic requirements of the code you are executing. Rough guidelines:\n", 141 | "\n", 142 | "* `button=False,continuous_update=True`: Provides a smooth, dynamic user experience, with text or plots updating immediately as a slider is dragged. Appropriate only for inexpensive operations, where rexecuting the code multiple times on the fly is not an issue.\n", 143 | "* `button=False,continuous_update=False`: The default; a good middle ground appropriate for most interactive use, with relatively responsive interactivity, updating each time a widget is released. Suitable for relatively expensive operations, but not so expensive that it is problematic to have them run once for each widget adjusted.\n", 144 | "* `button=True`: Suitable for very expensive or transactional operations, where you want to adjust multiple widgets before committing to executing the code.\n", 145 | "\n", 146 | "Example of dynamic updating:" 147 | ] 148 | }, 149 | { 150 | "cell_type": "code", 151 | "execution_count": null, 152 | "metadata": {}, 153 | "outputs": [], 154 | "source": [ 155 | "class Example2(param.Parameterized):\n", 156 | " num1 = param.Number(3.14,bounds=(0.0,10.0))\n", 157 | " number2 = param.Integer(2,bounds=(0,5))\n", 158 | "\n", 159 | "paramnb.Widgets(Example2,next_n=1)" 160 | ] 161 | }, 162 | { 163 | "cell_type": "code", 164 | "execution_count": null, 165 | "metadata": {}, 166 | "outputs": [], 167 | "source": [ 168 | "Example2.num1, Example2.number2" 169 | ] 170 | }, 171 | { 172 | "cell_type": "markdown", 173 | "metadata": {}, 174 | "source": [ 175 | "Notice that in a live notebook, the `In` and `Out` numbers of the above cell increase every time you release a slider after dragging it, because that cell is being re-executed. \n", 176 | "\n", 177 | "Example of updating on the \"Run\" button press:" 178 | ] 179 | }, 180 | { 181 | "cell_type": "code", 182 | "execution_count": null, 183 | "metadata": {}, 184 | "outputs": [], 185 | "source": [ 186 | "paramnb.Widgets(Example2,button=True,callback=hello,next_n=1)" 187 | ] 188 | }, 189 | { 190 | "cell_type": "code", 191 | "execution_count": null, 192 | "metadata": {}, 193 | "outputs": [], 194 | "source": [ 195 | "Example2.num1, Example2.number2" 196 | ] 197 | }, 198 | { 199 | "cell_type": "markdown", 200 | "metadata": {}, 201 | "source": [ 202 | "Here, the cell above changes its number (and output value) only when the \"Run 1\" button is pressed in the previous cell. The supplied callback is also executed at that time.\n", 203 | "\n", 204 | "Note that `paramnb.Widgets()` displays all the parameters that have a `precedence` that's above the `Widgets.display_threshold` value, which is zero by default. You can thus hide values that are not useful in the notebook by giving the parameters a negative precedence when they are declared. If you later want to display the hidden parameters, e.g. for debugging, you can change the `display_threshold` parameter, e.g. by supplying it to the `Widgets()` call. Parameters with the same precedence are sorted alphanumerically, in groups sorted by the precedence value. Values with no declared precedence are given a very low precedence by default (`Widgets(...,default_precedence=1e-8)`), allowing you to force parameters to appear at the top of the list by giving them a precedence of zero (or another very small number).\n", 205 | "\n", 206 | "Together, all these features make it simple to add interactive controls in Jupyter notebooks: just declare your parameters wherever their values will need to be used, using the Param library (pure Python, zero dependencies), then add an optional `Widgets()` declaration in your notebook wherever you want to be able to modify those values interactively. That way your main code can be fully independent of any GUI or notebook display, while your notebooks can easily expose the parameters declared in your main code, without duplicating their names or definitions and without relying on any specific details of that code. So you can now have full interactivity without tying yourself to any particular user interface or GUI library, and without tying your user interface code to details of your domain-specific code.\n", 207 | "\n", 208 | "You can install ParamNB as described at [github.com/ioam/paramnb](https://github.com/ioam/paramnb). Have fun widgeting!" 209 | ] 210 | } 211 | ], 212 | "metadata": { 213 | "language_info": { 214 | "name": "python", 215 | "pygments_lexer": "ipython3" 216 | } 217 | }, 218 | "nbformat": 4, 219 | "nbformat_minor": 0 220 | } 221 | -------------------------------------------------------------------------------- /paramnb/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Jupyter notebook interface for Param (https://github.com/ioam/param). 3 | 4 | Given a Parameterized object, displays a box with an ipywidget for each 5 | Parameter, allowing users to view and and manipulate Parameter values 6 | from within a Jupyter/IPython notebook. 7 | """ 8 | from __future__ import absolute_import 9 | 10 | import os 11 | import ast 12 | import uuid 13 | import itertools 14 | import json 15 | import functools 16 | from collections import OrderedDict 17 | 18 | import param 19 | import ipywidgets 20 | from IPython.display import display, Javascript, HTML, clear_output 21 | 22 | from . import widgets 23 | from .widgets import wtype, apply_error_style, literal_params, Output 24 | from .util import named_objs, get_method_owner 25 | from .view import View, HTML as HTMLView 26 | 27 | from param.version import Version 28 | __version__ = str(param.Version(fpath=__file__,archive_commit="bdf1856",reponame="paramnb")) 29 | del Version 30 | 31 | 32 | def run_next_cells(n): 33 | if n=='all': 34 | n = 'NaN' 35 | elif n<1: 36 | return 37 | 38 | js_code = """ 39 | var num = {0}; 40 | var run = false; 41 | var current = $(this)[0]; 42 | $.each(IPython.notebook.get_cells(), function (idx, cell) {{ 43 | if ((cell.output_area === current) && !run) {{ 44 | run = true; 45 | }} else if ((cell.cell_type == 'code') && !(num < 1) && run) {{ 46 | cell.execute(); 47 | num = num - 1; 48 | }} 49 | }}); 50 | """.format(n) 51 | 52 | display(Javascript(js_code)) 53 | 54 | 55 | def estimate_label_width(labels): 56 | """ 57 | Given a list of labels, estimate the width in pixels 58 | and return in a format accepted by CSS. 59 | Necessarily an approximation, since the font is unknown 60 | and is usually proportionally spaced. 61 | """ 62 | max_length = max([len(l) for l in labels]) 63 | return "{0}px".format(max(60,int(max_length*7.5))) 64 | 65 | 66 | class Widgets(param.ParameterizedFunction): 67 | 68 | callback = param.Callable(default=None, doc=""" 69 | Custom callable to execute on button press 70 | (if `button`) else whenever a widget is changed, 71 | Should accept a Parameterized object argument.""") 72 | 73 | view_position = param.ObjectSelector(default='below', 74 | objects=['below', 'right', 'left', 'above'], 75 | doc=""" 76 | Layout position of any View parameter widgets.""") 77 | 78 | next_n = param.Parameter(default=0, doc=""" 79 | When executing cells, integer number to execute (or 'all'). 80 | A value of zero means not to control cell execution.""") 81 | 82 | on_init = param.Boolean(default=False, doc=""" 83 | Whether to do the action normally taken (executing cells 84 | and/or calling a callable) when first instantiating this 85 | object.""") 86 | 87 | close_button = param.Boolean(default=False, doc=""" 88 | Whether to show a button allowing the Widgets to be closed.""") 89 | 90 | button = param.Boolean(default=False, doc=""" 91 | Whether to show a button to control cell execution. 92 | If false, will execute `next` cells on any widget 93 | value change.""") 94 | 95 | label_width = param.Parameter(default=estimate_label_width, doc=""" 96 | Width of the description for parameters in the list, using any 97 | string specification accepted by CSS (e.g. "100px" or "50%"). 98 | If set to a callable, will call that function using the list of 99 | all labels to get the value.""") 100 | 101 | tooltips = param.Boolean(default=True, doc=""" 102 | Whether to add tooltips to the parameter names to show their 103 | docstrings.""") 104 | 105 | show_labels = param.Boolean(default=True) 106 | 107 | display_threshold = param.Number(default=0,precedence=-10,doc=""" 108 | Parameters with precedence below this value are not displayed.""") 109 | 110 | default_precedence = param.Number(default=1e-8,precedence=-10,doc=""" 111 | Precedence value to use for parameters with no declared precedence. 112 | By default, zero predecence is available for forcing some parameters 113 | to the top of the list, and other values above the default_precedence 114 | values can be used to sort or group parameters arbitrarily.""") 115 | 116 | initializer = param.Callable(default=None, doc=""" 117 | User-supplied function that will be called on initialization, 118 | usually to update the default Parameter values of the 119 | underlying parameterized object.""") 120 | 121 | layout = param.ObjectSelector(default='column', 122 | objects=['row','column'],doc=""" 123 | Whether to lay out the buttons as a row or a column.""") 124 | 125 | continuous_update = param.Boolean(default=False, doc=""" 126 | If true, will continuously update the next_n and/or callback, 127 | if any, as a slider widget is dragged.""") 128 | 129 | def __call__(self, parameterized, plots=[], **params): 130 | self.p = param.ParamOverrides(self, params) 131 | if self.p.initializer: 132 | self.p.initializer(parameterized) 133 | 134 | self._id = uuid.uuid4().hex 135 | self._widgets = {} 136 | self.parameterized = parameterized 137 | 138 | widgets, views = self.widgets() 139 | layout = ipywidgets.Layout(display='flex', flex_flow=self.p.layout) 140 | if self.p.close_button: 141 | layout.border = 'solid 1px' 142 | 143 | widget_box = ipywidgets.VBox(children=widgets, layout=layout) 144 | plot_outputs = tuple(Output() for p in plots) 145 | if views or plots: 146 | outputs = tuple(views.values()) + plot_outputs 147 | view_box = ipywidgets.VBox(children=outputs, layout=layout) 148 | layout = self.p.view_position 149 | if layout in ['below', 'right']: 150 | children = [widget_box, view_box] 151 | else: 152 | children = [view_box, widget_box] 153 | box = ipywidgets.VBox if layout in ['below', 'above'] else ipywidgets.HBox 154 | widget_box = box(children=children) 155 | 156 | display(widget_box) 157 | self._widget_box = widget_box 158 | 159 | self._display_handles = {} 160 | # Render defined View parameters 161 | for pname, view in views.items(): 162 | p_obj = self.parameterized.params(pname) 163 | value = getattr(self.parameterized, pname) 164 | if value is None: 165 | continue 166 | handle = self._update_trait(pname, p_obj.renderer(value)) 167 | if handle: 168 | self._display_handles[pname] = handle 169 | 170 | # Render supplied plots 171 | for p, o in zip(plots, plot_outputs): 172 | with o: 173 | display(p) 174 | 175 | # Keeps track of changes between button presses 176 | self._changed = {} 177 | 178 | if self.p.on_init: 179 | self.execute() 180 | 181 | 182 | def _update_trait(self, p_name, p_value, widget=None): 183 | p_obj = self.parameterized.params(p_name) 184 | widget = self._widgets[p_name] if widget is None else widget 185 | if isinstance(p_value, tuple): 186 | p_value, size = p_value 187 | 188 | if isinstance(size, tuple) and len(size) == 2: 189 | if isinstance(widget, ipywidgets.Image): 190 | widget.width = size[0] 191 | widget.height = size[1] 192 | else: 193 | widget.layout.min_width = '%dpx' % size[0] 194 | widget.layout.min_height = '%dpx' % size[1] 195 | 196 | if isinstance(widget, Output): 197 | if isinstance(p_obj, HTMLView) and p_value: 198 | p_value = HTML(p_value) 199 | with widget: 200 | # clear_output required for JLab support 201 | # in future handle.update(p_value) should be sufficient 202 | handle = self._display_handles.get(p_name) 203 | if handle: 204 | clear_output(wait=True) 205 | handle.display(p_value) 206 | else: 207 | handle = display(p_value, display_id=p_name+self._id) 208 | self._display_handles[p_name] = handle 209 | else: 210 | widget.value = p_value 211 | 212 | 213 | def _make_widget(self, p_name): 214 | p_obj = self.parameterized.params(p_name) 215 | widget_class = wtype(p_obj) 216 | 217 | value = getattr(self.parameterized, p_name) 218 | 219 | # For ObjectSelector, pick first from objects if no default; 220 | # see https://github.com/ioam/param/issues/164 221 | if hasattr(p_obj,'objects') and len(p_obj.objects)>0 and value is None: 222 | value = p_obj.objects[0] 223 | if isinstance(p_obj,param.ListSelector): 224 | value = [value] 225 | setattr(self.parameterized, p_name, value) 226 | 227 | kw = dict(value=value) 228 | if p_obj.doc: 229 | kw['tooltip'] = p_obj.doc 230 | 231 | if isinstance(p_obj, param.Action): 232 | def action_cb(button): 233 | getattr(self.parameterized, p_name)(self.parameterized) 234 | kw['value'] = action_cb 235 | 236 | kw['name'] = p_name 237 | 238 | kw['continuous_update']=self.p.continuous_update 239 | 240 | if hasattr(p_obj, 'callbacks'): 241 | kw.pop('value', None) 242 | 243 | if hasattr(p_obj, 'get_range'): 244 | kw['options'] = named_objs(p_obj.get_range().items()) 245 | 246 | if hasattr(p_obj, 'get_soft_bounds'): 247 | kw['min'], kw['max'] = p_obj.get_soft_bounds() 248 | 249 | if hasattr(p_obj,'is_instance') and p_obj.is_instance: 250 | kw['options'][kw['value'].__class__.__name__]=kw['value'] 251 | 252 | w = widget_class(**kw) 253 | 254 | if hasattr(p_obj, 'callbacks') and value is not None: 255 | self._update_trait(p_name, p_obj.renderer(value), w) 256 | 257 | def change_event(event): 258 | new_values = event['new'] 259 | error = False 260 | # Apply literal evaluation to values 261 | if (isinstance(w, ipywidgets.Text) and isinstance(p_obj, literal_params)): 262 | try: 263 | new_values = ast.literal_eval(new_values) 264 | except: 265 | error = 'eval' 266 | elif hasattr(p_obj,'is_instance') and p_obj.is_instance and isinstance(new_values,type): 267 | # results in new instance each time non-default option 268 | # is selected; could consider caching. 269 | try: 270 | # awkward: support ParameterizedFunction 271 | new_values = new_values.instance() if hasattr(new_values,'instance') else new_values() 272 | except: 273 | error = 'instantiate' 274 | 275 | # If no error during evaluation try to set parameter 276 | if not error: 277 | try: 278 | setattr(self.parameterized, p_name, new_values) 279 | except ValueError: 280 | error = 'validation' 281 | 282 | # Style widget to denote error state 283 | apply_error_style(w, error) 284 | 285 | if not error and not self.p.button: 286 | self.execute({p_name: new_values}) 287 | else: 288 | self._changed[p_name] = new_values 289 | 290 | if hasattr(p_obj, 'callbacks'): 291 | p_obj.callbacks[id(self.parameterized)] = functools.partial(self._update_trait, p_name) 292 | else: 293 | w.observe(change_event, 'value') 294 | 295 | # Hack ; should be part of Widget classes 296 | if hasattr(p_obj,"path"): 297 | def path_change_event(event): 298 | new_values = event['new'] 299 | p_obj = self.parameterized.params(p_name) 300 | p_obj.path = new_values 301 | p_obj.update() 302 | 303 | # Update default value in widget, ensuring it's always a legal option 304 | selector = self._widgets[p_name].children[1] 305 | defaults = p_obj.default 306 | if not issubclass(type(defaults),list): 307 | defaults = [defaults] 308 | selector.options.update(named_objs(zip(defaults,defaults))) 309 | selector.value=p_obj.default 310 | selector.options=named_objs(p_obj.get_range().items()) 311 | 312 | if p_obj.objects and not self.p.button: 313 | self.execute({p_name:selector.value}) 314 | 315 | path_w = ipywidgets.Text(value=p_obj.path) 316 | path_w.observe(path_change_event, 'value') 317 | w = ipywidgets.VBox(children=[path_w,w], 318 | layout=ipywidgets.Layout(margin='0')) 319 | 320 | return w 321 | 322 | 323 | def widget(self, param_name): 324 | """Get widget for param_name""" 325 | if param_name not in self._widgets: 326 | self._widgets[param_name] = self._make_widget(param_name) 327 | return self._widgets[param_name] 328 | 329 | 330 | def execute(self, changed={}): 331 | run_next_cells(self.p.next_n) 332 | if self.p.callback is not None: 333 | if get_method_owner(self.p.callback) is self.parameterized: 334 | self.p.callback(**changed) 335 | else: 336 | self.p.callback(self.parameterized, **changed) 337 | 338 | 339 | # Define some settings :) 340 | preamble = """ 341 | 345 | """ 346 | 347 | label_format = """
{1}
""" 349 | 350 | def helptip(self,obj): 351 | """Return HTML code formatting a tooltip if help is available""" 352 | helptext = obj.__doc__ 353 | return "" if (not self.p.tooltips or not helptext) else helptext 354 | 355 | 356 | def widgets(self): 357 | """Return name,widget boxes for all parameters (i.e., a property sheet)""" 358 | 359 | params = self.parameterized.params().items() 360 | key_fn = lambda x: x[1].precedence if x[1].precedence is not None else self.p.default_precedence 361 | sorted_precedence = sorted(params, key=key_fn) 362 | outputs = [k for k, p in sorted_precedence if isinstance(p, View)] 363 | filtered = [(k,p) for (k,p) in sorted_precedence 364 | if ((p.precedence is None) or (p.precedence >= self.p.display_threshold)) 365 | and k not in outputs] 366 | groups = itertools.groupby(filtered, key=key_fn) 367 | sorted_groups = [sorted(grp) for (k,grp) in groups] 368 | ordered_params = [el[0] for group in sorted_groups for el in group] 369 | 370 | # Format name specially 371 | widgets = [ipywidgets.HTML(self.preamble + 372 | '
{0}'.format(self.parameterized.name)+"
")] 373 | 374 | label_width=self.p.label_width 375 | if callable(label_width): 376 | label_width = label_width(self.parameterized.params().keys()) 377 | 378 | def format_name(pname): 379 | p = self.parameterized.params(pname) 380 | # omit name for buttons, which already show the name on the button 381 | name = "" if issubclass(type(p),param.Action) else pname 382 | return ipywidgets.HTML(self.label_format.format(label_width, name, self.helptip(p))) 383 | 384 | if self.p.show_labels: 385 | widgets += [ipywidgets.HBox(children=[format_name(pname),self.widget(pname)]) 386 | for pname in ordered_params] 387 | else: 388 | widgets += [self.widget(pname) for pname in ordered_params] 389 | 390 | if self.p.close_button: 391 | close_button = ipywidgets.Button(description="Close") 392 | # TODO: what other cleanup should be done? 393 | close_button.on_click(lambda _: self._widget_box.close()) 394 | widgets.append(close_button) 395 | 396 | 397 | if self.p.button and not (self.p.callback is None and self.p.next_n==0): 398 | label = 'Run %s' % self.p.next_n if self.p.next_n != 'all' else "Run" 399 | display_button = ipywidgets.Button(description=label) 400 | def click_cb(button): 401 | # Execute and clear changes since last button press 402 | try: 403 | self.execute(self._changed) 404 | except Exception as e: 405 | self._changed.clear() 406 | raise e 407 | self._changed.clear() 408 | display_button.on_click(click_cb) 409 | widgets.append(display_button) 410 | 411 | outputs = OrderedDict([(pname, self.widget(pname)) for pname in outputs]) 412 | return widgets, outputs 413 | 414 | 415 | # TODO: this is awkward. An alternative would be to import Widgets in 416 | # widgets.py only at the point(s) where Widgets is needed rather than 417 | # at the top level (to avoid circular imports). Probably some 418 | # reorganization would be better, though. 419 | widgets.editor = functools.partial(Widgets,close_button=True) 420 | 421 | 422 | class JSONInit(param.Parameterized): 423 | """ 424 | Callable that can be passed to Widgets.initializer to set Parameter 425 | values using JSON. There are three approaches that may be used: 426 | 427 | 1. If the json_file argument is specified, this takes precedence. 428 | 2. The JSON file path can be specified via an environment variable. 429 | 3. The JSON can be read directly from an environment variable. 430 | 431 | Here is an easy example of setting such an environment variable on 432 | the commandline: 433 | 434 | PARAMNB_INIT='{"p1":5}' jupyter notebook 435 | 436 | This addresses any JSONInit instances that are inspecting the 437 | default environment variable called PARAMNB_INIT, instructing it to set 438 | the 'p1' parameter to 5. 439 | """ 440 | 441 | varname = param.String(default='PARAMNB_INIT', doc=""" 442 | The name of the environment variable containing the JSON 443 | specification.""") 444 | 445 | target = param.String(default=None, doc=""" 446 | Optional key in the JSON specification dictionary containing the 447 | desired parameter values.""") 448 | 449 | json_file = param.String(default=None, doc=""" 450 | Optional path to a JSON file containing the parameter settings.""") 451 | 452 | 453 | def __call__(self, parameterized): 454 | 455 | warnobj = param.main if isinstance(parameterized, type) else parameterized 456 | param_class = (parameterized if isinstance(parameterized, type) 457 | else parameterized.__class__) 458 | 459 | 460 | target = self.target if self.target is not None else param_class.__name__ 461 | 462 | env_var = os.environ.get(self.varname, None) 463 | if env_var is None and self.json_file is None: return 464 | 465 | if self.json_file or env_var.endswith('.json'): 466 | try: 467 | fname = self.json_file if self.json_file else env_var 468 | spec = json.load(open(os.path.abspath(fname), 'r')) 469 | except: 470 | warnobj.warning('Could not load JSON file %r' % spec) 471 | else: 472 | spec = json.loads(env_var) 473 | 474 | if not isinstance(spec, dict): 475 | warnobj.warning('JSON parameter specification must be a dictionary.') 476 | return 477 | 478 | if target in spec: 479 | params = spec[target] 480 | else: 481 | params = spec 482 | 483 | for name, value in params.items(): 484 | try: 485 | parameterized.set_param(**{name:value}) 486 | except ValueError as e: 487 | warnobj.warning(str(e)) 488 | 489 | 490 | ## 491 | # make pyct's example/data commands available if possible 492 | from functools import partial 493 | try: 494 | from pyct.cmd import copy_examples as _copy, fetch_data as _fetch, examples as _examples 495 | copy_examples = partial(_copy, 'paramnb') 496 | fetch_data = partial(_fetch, 'paramnb') 497 | examples = partial(_examples, 'paramnb') 498 | except ImportError: 499 | def _missing_cmd(*args,**kw): return("install pyct to enable this command (e.g. `conda install pyct` or `pip install pyct[cmd]`)") 500 | _copy = _fetch = _examples = _missing_cmd 501 | def _err(): raise ValueError(_missing_cmd()) 502 | fetch_data = copy_examples = examples = _err 503 | del partial, _examples, _copy, _fetch 504 | ## 505 | -------------------------------------------------------------------------------- /paramnb/__main__.py: -------------------------------------------------------------------------------- 1 | def main(args=None): 2 | try: 3 | import pyct.cmd 4 | except ImportError: 5 | import sys 6 | from . import _missing_cmd 7 | print(_missing_cmd()) 8 | sys.exit(1) 9 | return pyct.cmd.substitute_main('paramnb',args=args) 10 | 11 | if __name__ == "__main__": 12 | main() 13 | -------------------------------------------------------------------------------- /paramnb/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ioam/paramnb/bdf18566d6279d6612537fadaa5460e1961aa359/paramnb/tests/__init__.py -------------------------------------------------------------------------------- /paramnb/tests/test_dummy.py: -------------------------------------------------------------------------------- 1 | def test_dummy(): 2 | print(1) 3 | pass 4 | -------------------------------------------------------------------------------- /paramnb/util.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import inspect 3 | from collections import OrderedDict 4 | 5 | if sys.version_info.major == 3: 6 | unicode = str 7 | basestring = str 8 | 9 | 10 | def as_unicode(obj): 11 | """ 12 | Safely casts any object to unicode including regular string 13 | (i.e. bytes) types in python 2. 14 | """ 15 | if sys.version_info.major < 3 and isinstance(obj, str): 16 | obj = obj.decode('utf-8') 17 | return unicode(obj) 18 | 19 | 20 | def named_objs(objlist): 21 | """ 22 | Given a list of objects, returns a dictionary mapping from 23 | string name for the object to the object itself. 24 | """ 25 | objs = OrderedDict() 26 | for k, obj in objlist: 27 | if hasattr(k, '__name__'): 28 | k = k.__name__ 29 | else: 30 | k = as_unicode(k) 31 | objs[k] = obj 32 | return objs 33 | 34 | 35 | def get_method_owner(meth): 36 | """ 37 | Returns the instance owning the supplied instancemethod or 38 | the class owning the supplied classmethod. 39 | """ 40 | if inspect.ismethod(meth): 41 | if sys.version_info < (3,0): 42 | return meth.im_class if meth.im_self is None else meth.im_self 43 | else: 44 | return meth.__self__ 45 | -------------------------------------------------------------------------------- /paramnb/view.py: -------------------------------------------------------------------------------- 1 | import param 2 | 3 | class View(param.Parameter): 4 | """ 5 | View parameters hold displayable output, they may have a callback, 6 | which is called when a new value is set on the parameter. 7 | Additionally they allow supplying a renderer function which renders 8 | the display output. The renderer function should return the 9 | appropriate output for the View parameter (e.g. HTML or PNG data), 10 | and may optionally supply the desired size of the viewport. 11 | """ 12 | 13 | __slots__ = ['callbacks', 'renderer'] 14 | 15 | def __init__(self, default=None, callback=None, renderer=None, **kwargs): 16 | self.callbacks = {} 17 | self.renderer = (lambda x: x) if renderer is None else renderer 18 | super(View, self).__init__(default, **kwargs) 19 | 20 | def __set__(self, obj, val): 21 | super(View, self).__set__(obj, val) 22 | obj_id = id(obj) 23 | if obj_id in self.callbacks: 24 | self.callbacks[obj_id](self.renderer(val)) 25 | 26 | 27 | class HTML(View): 28 | """ 29 | HTML is a View parameter that allows displaying HTML output. 30 | """ 31 | 32 | 33 | class Image(View): 34 | """ 35 | Image is a View parameter that allows displaying PNG bytestrings. 36 | """ 37 | -------------------------------------------------------------------------------- /paramnb/widgets.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | import param 4 | from param.parameterized import classlist 5 | 6 | import ipywidgets 7 | from ipywidgets import ( 8 | SelectMultiple, Button, HBox, VBox, Layout, Text, HTML, 9 | FloatSlider, FloatText, IntText, IntSlider, 10 | Image, ColorPicker, FloatRangeSlider, IntRangeSlider, Dropdown, 11 | Output 12 | ) 13 | import traitlets 14 | 15 | from .util import named_objs 16 | from .view import View, HTML as HTMLView, Image as ImageView 17 | 18 | 19 | # What to use for editing parameters of an object, 20 | # e.g. paramnb.Widgets class. 21 | editor = None 22 | 23 | 24 | def FloatWidget(*args, **kw): 25 | """Returns appropriate slider or text boxes depending on bounds""" 26 | has_bounds = not (kw['min'] is None or kw['max'] is None) 27 | return (FloatSlider if has_bounds else FloatText)(*args,**kw) 28 | 29 | 30 | def IntegerWidget(*args, **kw): 31 | """Returns appropriate slider or text boxes depending on bounds""" 32 | has_bounds = not (kw['min'] is None or kw['max'] is None) 33 | return (IntSlider if has_bounds else IntText)(*args,**kw) 34 | 35 | 36 | def TextWidget(*args, **kw): 37 | """Forces a parameter value to be text""" 38 | kw['value'] = str(kw['value']) 39 | return Text(*args,**kw) 40 | 41 | 42 | def HTMLWidget(*args, **kw): 43 | """Forces a parameter value to be text, displayed as HTML""" 44 | kw['value'] = str(kw['value']) 45 | return HTML(*args,**kw) 46 | 47 | 48 | def DateWidget(*args, **kw): 49 | """ 50 | DateWidget to pick datetime type, if bounds but no default is 51 | defined default to min. 52 | """ 53 | if kw.get('value') is None and 'min' in kw: 54 | kw['value'] = kw['min'] 55 | return DatePicker(*args, **kw) 56 | 57 | 58 | def ColorWidget(*args, **kw): 59 | """Color widget to pick hex color (defaults to black)""" 60 | if kw.get('value') is None: 61 | kw['value'] = '#000000' 62 | return ColorPicker(*args, **kw) 63 | 64 | 65 | class RangeWidget(param.ParameterizedFunction): 66 | """ 67 | Range widget switches between integer and float slider dynamically 68 | and computes the step size based based on the steps parameter. 69 | """ 70 | 71 | steps = param.Integer(default=50, doc=""" 72 | Number of steps used to compute step size for float range.""") 73 | 74 | def __call__(self, *args, **kw): 75 | has_bounds = not (kw['min'] is None or kw['max'] is None) 76 | if not has_bounds: 77 | return TextWidget(*args,**kw) 78 | if all(kw[k] is None or isinstance(kw[k], int) 79 | for k in ['min', 'max']): 80 | widget = IntRangeSlider 81 | kw['step'] = 1 82 | else: 83 | widget = FloatRangeSlider 84 | if not 'step' in kw: 85 | kw['step'] = float((kw['max'] - kw['min']))/self.steps 86 | return widget(*args, **kw) 87 | 88 | 89 | class ListSelectorWidget(param.ParameterizedFunction): 90 | """ 91 | Selects the appropriate ListSelector widget depending on the number 92 | of items. 93 | """ 94 | 95 | item_limit = param.Integer(default=20, allow_None=True, doc=""" 96 | The number of items in the ListSelector before it switches from 97 | a regular SelectMultiple widget to a two-pane CrossSelect widget. 98 | Setting the limit to None will disable the CrossSelect widget 99 | completely while a negative value will force it to be enabled. 100 | """) 101 | 102 | def __call__(self, *args, **kw): 103 | item_limit = kw.pop('item_limit', self.item_limit) 104 | if item_limit is not None and len(kw['options']) > item_limit: 105 | return CrossSelect(*args, **kw) 106 | else: 107 | return SelectMultiple(*args, **kw) 108 | 109 | 110 | def ActionButton(*args, **kw): 111 | """Returns a ipywidgets.Button executing a paramnb.Action.""" 112 | kw['description'] = str(kw['name']) 113 | value = kw["value"] 114 | w = ipywidgets.Button(*args,**kw) 115 | if value: w.on_click(value) 116 | return w 117 | 118 | 119 | class CrossSelect(SelectMultiple): 120 | """ 121 | CrossSelect provides a two-tab multi-selection widget with regex 122 | text filtering. Items can be transferred with buttons between the 123 | selected and unselected options. 124 | """ 125 | 126 | def __init__(self, *args, **kwargs): 127 | # Compute selected and unselected values 128 | options = kwargs.get('options', {}) 129 | if isinstance(options, list): 130 | options = named_objs([(opt, opt) for opt in options]) 131 | self._reverse_lookup = {v: k for k, v in options.items()} 132 | selected = [self._reverse_lookup[v] for v in kwargs.get('value', [])] 133 | unselected = [k for k in options if k not in selected] 134 | 135 | # Define whitelist and blacklist 136 | self._lists = {False: SelectMultiple(options=unselected), 137 | True: SelectMultiple(options=selected)} 138 | 139 | self._lists[False].observe(self._update_selection, 'value') 140 | self._lists[True].observe(self._update_selection, 'value') 141 | 142 | # Define buttons 143 | button_layout = Layout(width='50px') 144 | self._buttons = {False: Button(description='<<', layout=button_layout), 145 | True: Button(description='>>', layout=button_layout)} 146 | self._buttons[False].on_click(self._apply_selection) 147 | self._buttons[True].on_click(self._apply_selection) 148 | 149 | # Define search 150 | self._search = {False: Text(placeholder='Filter available options'), 151 | True: Text(placeholder='Filter selected options')} 152 | self._search[False].observe(self._filter_options, 'value') 153 | self._search[True].observe(self._filter_options, 'value') 154 | 155 | # Define Layout 156 | no_margin = Layout(margin='0') 157 | row_layout = Layout(margin='0', display='flex', justify_content='space-between') 158 | 159 | search_row = HBox([self._search[False], self._search[True]]) 160 | search_row.layout = row_layout 161 | button_box = VBox([self._buttons[True], self._buttons[False]], 162 | layout=Layout(margin='auto 0')) 163 | tab_row = HBox([self._lists[False], button_box, self._lists[True]]) 164 | tab_row.layout = row_layout 165 | self._composite = VBox([search_row, tab_row], layout=no_margin) 166 | 167 | self.observe(self._update_options, 'options') 168 | self.observe(self._update_value, 'value') 169 | 170 | self._selected = {False: [], True: []} 171 | self._query = {False: '', True: ''} 172 | super(CrossSelect, self).__init__(*args, **dict(kwargs, options=options)) 173 | 174 | 175 | def _update_value(self, event): 176 | selected = [self._reverse_lookup.get(v, v) for v in event['new']] 177 | self._lists[True].options = selected 178 | self._lists[True].value = [] 179 | self._lists[False].options = [o for o in self.options 180 | if o not in selected] 181 | 182 | def _update_options(self, event): 183 | """ 184 | Updates the options of each of the sublists after the options 185 | for the whole widget are updated. 186 | """ 187 | self._reverse_lookup = {v: k for k, v in event['new'].items()} 188 | options = list(event['new'].keys()) if isinstance(event, dict) else event['new'] 189 | self._selected[False] = [] 190 | self._selected[True] = [] 191 | self._lists[True].options = [''] 192 | self._lists[True].value = [] 193 | self._lists[False].options = options 194 | self._lists[False].value = [] 195 | self._apply_filters() 196 | 197 | def _apply_filters(self): 198 | self._filter_options({'owner': self._search[False]}) 199 | self._filter_options({'owner': self._search[True]}) 200 | 201 | def _filter_options(self, event): 202 | """ 203 | Filters unselected options based on a text query event. 204 | """ 205 | selected = event['owner'] is self._search[True] 206 | query = self._query[selected] if 'new' not in event else event['new'] 207 | self._query[selected] = query 208 | other = self._lists[not selected].options 209 | options = [o for o in self.options if o not in other] 210 | if not query: 211 | self._lists[selected].options = options 212 | self._lists[selected].value = [] 213 | else: 214 | try: 215 | match = re.compile(query) 216 | matches = list(filter(match.search, options)) 217 | options = matches + [opt for opt in options if opt not in matches] 218 | except: 219 | matches = options 220 | self._lists[selected].options = options if options else [''] 221 | self._lists[selected].value = matches 222 | 223 | def _update_selection(self, event): 224 | """ 225 | Updates the current selection in each list. 226 | """ 227 | selected = event['owner'] is self._lists[True] 228 | self._selected[selected] = [v for v in event['new'] if v != ''] 229 | 230 | def _apply_selection(self, event): 231 | """ 232 | Applies the current selection depending on which button was 233 | pressed. 234 | """ 235 | selected = event is self._buttons[True] 236 | new = self._selected[not selected] 237 | old = self._lists[selected].options 238 | other = self._lists[not selected].options 239 | 240 | merged = sorted([v for v in list(old) + list(new) if v != '']) 241 | leftovers = sorted([o for o in other if o not in new and o != '']) 242 | self._lists[selected].options = merged if merged else [''] 243 | self._lists[not selected].options = leftovers if leftovers else [''] 244 | self.value = [self._options_dict[o] for o in self._lists[True].options if o != ''] 245 | self._apply_filters() 246 | 247 | def _ipython_display_(self, **kwargs): 248 | """ 249 | Displays the composite widget. 250 | """ 251 | self._composite._ipython_display_(**kwargs) 252 | 253 | def get_state(self, key=None, drop_defaults=False): 254 | # HACK: Lets this composite widget pretend to be a regular widget 255 | # when included into a layout. 256 | if key in ['value', '_options_labels']: 257 | return super(CrossSelect, self).get_state(key) 258 | return self._composite.get_state(key) 259 | 260 | 261 | # Composite widget containing a Dropdown and a Button in an HBox. 262 | # Some things are routed to/from the Dropdown, others to/from the 263 | # composite HBox. 264 | class DropdownWithEdit(ipywidgets.Widget): 265 | """ 266 | Dropdown, but displays an edit button if the current selection is 267 | a parameterized object. 268 | """ 269 | 270 | # I couldn't figure out which widget class actually declares the 271 | # value trait. Might not be needed in ipywidgets>6 because I 272 | # can see value trait declared in ValueWidget... 273 | value = traitlets.Any() 274 | 275 | def __init__(self, *args, **kwargs): 276 | self._select = Dropdown(*args,**kwargs) 277 | self._edit = Button(description='...', 278 | layout=Layout(width='15px')) 279 | self._composite = HBox([self._select,self._edit]) 280 | super(DropdownWithEdit, self).__init__() 281 | self.layout = self._composite.layout 282 | # so that others looking at this widget's value get the 283 | # dropdown's value 284 | traitlets.link((self._select,'value'),(self,'value')) 285 | self._edit.on_click(lambda _: editor(self._select.value)) 286 | self._select.observe(lambda e: self._set_editable(e['new']),'value') 287 | self._set_editable(self._select.value) 288 | 289 | def _set_editable(self,v): 290 | if hasattr(v,'params'): 291 | self._edit.layout.display = None # i.e. make it visible 292 | else: 293 | self._edit.layout.display = 'none' 294 | 295 | def _ipython_display_(self, **kwargs): 296 | self._composite._ipython_display_(**kwargs) 297 | 298 | def get_state(self, *args, **kw): 299 | # support layouts; see CrossSelect.get_state 300 | return self._composite.get_state(*args,**kw) 301 | 302 | 303 | def apply_error_style(w, error): 304 | "Applies error styling to the supplied widget based on the error code" 305 | if error: 306 | color = '#FFCC00' if error == 'eval' else '#cc0000' 307 | w.layout.border = '5px solid %s' % color 308 | else: 309 | w.layout.border = '0px' 310 | 311 | 312 | 313 | # Define parameters which should be evaluated using ast.literal_eval 314 | literal_params = (param.Dict, param.List, param.Tuple) 315 | 316 | # Maps from Parameter type to ipython widget types with any options desired 317 | ptype2wtype = { 318 | param.Parameter: TextWidget, 319 | param.Dict: TextWidget, 320 | param.Selector: DropdownWithEdit, 321 | param.Boolean: ipywidgets.Checkbox, 322 | param.Number: FloatWidget, 323 | param.Integer: IntegerWidget, 324 | param.ListSelector: ListSelectorWidget, 325 | param.Action: ActionButton, 326 | HTMLView: Output, 327 | ImageView: Image, 328 | View: Output 329 | } 330 | 331 | # Handle new parameters introduced in param 1.5 332 | try: 333 | from param import Color, Range 334 | ptype2wtype.update({ 335 | Color: ColorWidget, 336 | Range: RangeWidget 337 | }) 338 | except: 339 | pass 340 | 341 | try: 342 | from param import Date 343 | ptype2wtype[Date] = TextWidget 344 | 345 | from ipywidgets import DatePicker 346 | ptype2wtype[Date] = DateWidget 347 | except: 348 | pass 349 | 350 | 351 | def wtype(pobj): 352 | if pobj.constant: # Ensure constant parameters cannot be edited 353 | return HTMLWidget 354 | for t in classlist(type(pobj))[::-1]: 355 | if t in ptype2wtype: 356 | return ptype2wtype[t] 357 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = [ 3 | "param >=1.7.0", 4 | "pyct >=0.4.4", 5 | "setuptools >=30.3.0" 6 | ] 7 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = paramnb 3 | version = attr: param.version.get_setupcfg_version 4 | description = Generate ipywidgets from Parameterized objects in the notebook 5 | long_description = file: README.md 6 | long_description_content_type = text/markdown 7 | license = BSD 3-Clause License 8 | license_file = LICENSE.txt 9 | classifiers = 10 | License :: OSI Approved :: BSD License 11 | Operating System :: OS Independent 12 | Programming Language :: Python 13 | Programming Language :: Python :: 2.7 14 | Programming Language :: Python :: 3.5 15 | Programming Language :: Python :: 3.6 16 | Development Status :: 4 - Beta 17 | author = PyViz 18 | author_email = holoviews@gmail.com 19 | maintainer = PyViz 20 | maintainer_email = holoviews@gmail.com 21 | url = https://paramnb.pyviz.org 22 | project_urls = 23 | Bug Tracker = https://github.com/ioam/paramnb/issues 24 | Documentation = https://paramnb.pyviz.org 25 | Source Code = https://github.com/ioam/paramnb 26 | 27 | 28 | [options] 29 | include_package_data = True 30 | packages = find: 31 | python_requires = >=2.7 32 | install_requires = 33 | param >=1.7.0 34 | ipywidgets >=5.2.2 35 | 36 | [options.extras_require] 37 | tests = 38 | nbsmoke >=0.2.6 39 | flake8 40 | pytest >=2.8.5 41 | 42 | examples = 43 | pyct[cmd] 44 | holoviews >=1.9.0 45 | pandas 46 | jupyter 47 | numpy 48 | matplotlib 49 | bokeh 50 | 51 | 52 | [options.entry_points] 53 | console_scripts = 54 | paramnb = paramnb.__main__:main 55 | 56 | [wheel] 57 | universal = 1 58 | 59 | [tool:autover.configparser_workaround.archive_commit=bdf1856] 60 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | import os, sys, shutil 3 | 4 | import pyct.build 5 | 6 | if __name__=="__main__": 7 | # TODO: hope to eliminate the examples handling from here 8 | # (i.e. all lines except setup()), moving it to pyct 9 | example_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 10 | 'paramnb','examples') 11 | if 'develop' not in sys.argv: 12 | pyct.build.examples(example_path, __file__, force=True) 13 | 14 | setup() 15 | 16 | if os.path.isdir(example_path): 17 | shutil.rmtree(example_path) 18 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | # For use with pyct (https://github.com/pyviz/pyct), but just standard 2 | # tox config (works with tox alone). 3 | 4 | [tox] 5 | # python version test group extra envs extra commands 6 | envlist = {py27,py36}-{lint,unit,examples,all}-{default,examples}-{dev,pkg} 7 | build = wheel 8 | 9 | [_lint] 10 | description = Flake check python and notebooks, and verify notebooks 11 | deps = .[tests] 12 | # verify takes quite a long time - maybe split into flakes and lint? 13 | commands = flake8 14 | pytest --nbsmoke-lint -k ".ipynb" 15 | # requires hv, pandas etc unless missing modules turned into warnings 16 | # pytest --nbsmoke-verify -k ".ipynb" 17 | 18 | [_unit] 19 | description = Run unit tests 20 | deps = .[tests] 21 | commands = pytest paramnb 22 | 23 | [_examples] 24 | description = Test that examples run 25 | deps = .[examples, tests] 26 | commands = pytest --nbsmoke-run -k ".ipynb" 27 | # could add more, to test types of example other than nbs 28 | 29 | [_all] 30 | description = Run all tests 31 | deps = .[examples, tests] 32 | commands = {[_lint]commands} 33 | {[_unit]commands} 34 | {[_examples]commands} 35 | 36 | [_pkg] 37 | commands = paramnb copy-examples --path=. --force 38 | 39 | [testenv] 40 | changedir = {envtmpdir} 41 | 42 | commands = examples-pkg: {[_pkg]commands} 43 | unit: {[_unit]commands} 44 | lint: {[_lint]commands} 45 | examples: {[_examples]commands} 46 | all: {[_all]commands} 47 | 48 | deps = unit: {[_unit]deps} 49 | lint: {[_lint]deps} 50 | examples: {[_examples]deps} 51 | all: {[_all]deps} 52 | 53 | [pytest] 54 | addopts = -v --pyargs --doctest-modules --doctest-ignore-import-errors 55 | norecursedirs = doc .git dist build _build .ipynb_checkpoints 56 | # notebooks to skip running; one case insensitive re to match per line 57 | nbsmoke_skip_run = ^.*JSONInit\.ipynb$ 58 | 59 | [flake8] 60 | include = *.py 61 | # run_tests.py is generated by conda build, which appears to have a 62 | # bug resulting in code being duplicated a couple of times. 63 | exclude = .git,__pycache__,.tox,.eggs,*.egg,doc,dist,build,_build,.ipynb_checkpoints,run_test.py 64 | ignore = E, 65 | W 66 | --------------------------------------------------------------------------------