diff --git a/.travis.yml b/.travis.yml index 70240934e..cef338630 100644 --- a/.travis.yml +++ b/.travis.yml @@ -132,8 +132,8 @@ jobs: - pip install -e . script: # TODO: nbsite commands will be simplified eventually... - - nbsite generate-rst --org pyviz --repo param --project-name param - - mkdir doc/Reference_Manual && nbsite_generate_modules.py param -d ./doc/Reference_Manual -n param -e tests + - nbsite generate-rst --org holoviz --repo param --project-name param + - mkdir -p doc/Reference_Manual && nbsite_generate_modules.py param -d ./doc/Reference_Manual -n param -e tests - nbsite build --examples-assets='' deploy: - provider: pages diff --git a/doc/Reference_Manual/index.rst b/doc/Reference_Manual/index.rst new file mode 100644 index 000000000..f0a1520e9 --- /dev/null +++ b/doc/Reference_Manual/index.rst @@ -0,0 +1,97 @@ +******************** +API Reference Manual +******************** + +The Param API Reference Manual provides a comprehensive reference for +all methods and objects available from Param. For human-readable guides to +how to use these capabilities, see the `User Guide <../user_guide>`_. + +Module Structure +________________ + + +`param package <#param-package>`_ + Collection of Parameter objects to use in your code +`param.parameterized module <#parameterized-module>`_ + Definition for core Param components: Parameter and Parameterized, plus utilities +`param.serializer module <#serializer-module>`_ + Optional support for serializing to JSON or other formats +`param.ipython module <#ipython-module>`_ + Optional ipython support +`numbergen package <#numbergen-numbergen-package>`_ + Separate package with generators for streams of numeric values + + +--------- + +:mod:`param` Package +-------------------- + +.. inheritance-diagram:: param.__init__ + + +.. automodule:: param.__init__ + :members: + :inherited-members: + :show-inheritance: + +------- + +:mod:`parameterized` Module +--------------------------- + +.. inheritance-diagram:: param.parameterized + + +.. automodule:: param.parameterized + :members: + :inherited-members: + :show-inheritance: + +------- + +:mod:`serializer` Module +------------------------ + +.. inheritance-diagram:: param.serializer + + +.. automodule:: param.serializer + :members: + :inherited-members: + :show-inheritance: + +------- + +:mod:`ipython` Module +--------------------- + +.. inheritance-diagram:: param.ipython + + +.. automodule:: param.ipython + :members: + :inherited-members: + :show-inheritance: + +------- + + + +:mod:`numbergen` Package +------------------------ + +.. inheritance-diagram:: numbergen.__init__ + + +.. automodule:: numbergen.__init__ + :members: + :inherited-members: + :show-inheritance: + +------- + +.. toctree:: + :maxdepth: 2 + :hidden: + diff --git a/doc/_static/favicon.ico b/doc/_static/favicon.ico old mode 100755 new mode 100644 index a53ea1798..ee0844c3d Binary files a/doc/_static/favicon.ico and b/doc/_static/favicon.ico differ diff --git a/doc/_static/icon.png b/doc/_static/icon.png new file mode 100644 index 000000000..b9727d55d Binary files /dev/null and b/doc/_static/icon.png differ diff --git a/doc/_static/icon.svg b/doc/_static/icon.svg new file mode 100644 index 000000000..ed61ab3d0 --- /dev/null +++ b/doc/_static/icon.svg @@ -0,0 +1,296 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/doc/_static/logo_horizontal.png b/doc/_static/logo_horizontal.png new file mode 100644 index 000000000..e4972d6ed Binary files /dev/null and b/doc/_static/logo_horizontal.png differ diff --git a/doc/_static/logo_horizontal.svg b/doc/_static/logo_horizontal.svg new file mode 100644 index 000000000..be94b635b --- /dev/null +++ b/doc/_static/logo_horizontal.svg @@ -0,0 +1,324 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Param + + diff --git a/doc/_static/logo_horizontal_white.png b/doc/_static/logo_horizontal_white.png new file mode 100644 index 000000000..a4c58d38d Binary files /dev/null and b/doc/_static/logo_horizontal_white.png differ diff --git a/doc/_static/logo_horizontal_white.svg b/doc/_static/logo_horizontal_white.svg new file mode 100644 index 000000000..ad353acad --- /dev/null +++ b/doc/_static/logo_horizontal_white.svg @@ -0,0 +1,309 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Param + + diff --git a/doc/_static/logo_stacked.png b/doc/_static/logo_stacked.png new file mode 100644 index 000000000..05a9425fb Binary files /dev/null and b/doc/_static/logo_stacked.png differ diff --git a/doc/_static/logo_stacked.svg b/doc/_static/logo_stacked.svg new file mode 100644 index 000000000..8cb73224e --- /dev/null +++ b/doc/_static/logo_stacked.svg @@ -0,0 +1,321 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Param + + diff --git a/doc/_static/wordmark.png b/doc/_static/wordmark.png new file mode 100644 index 000000000..2e11baed6 Binary files /dev/null and b/doc/_static/wordmark.png differ diff --git a/doc/_static/wordmark.svg b/doc/_static/wordmark.svg new file mode 100644 index 000000000..72aa762e0 --- /dev/null +++ b/doc/_static/wordmark.svg @@ -0,0 +1,230 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Param + + diff --git a/doc/about.rst b/doc/about.rst new file mode 100644 index 000000000..860ea2b59 --- /dev/null +++ b/doc/about.rst @@ -0,0 +1,10 @@ +About +===== + +Param is completely open source, available under a `BSD license `_ freely for both commercial and non-commercial use. Param was originally developed at the University of Texas at Austin and the University of Edinburgh with funding from the US National Institutes of Health grant 1R01-MH66991. Param is now maintained by `Anaconda Inc. `_ and by community contributors. + +Param is maintained as part of the `HoloViz `_ family of tools. The `holoviz.org `_ website shows how to use Param together with other libraries to solve complex problems, with detailed tutorials and examples. Each of the HoloViz tools builds on Param, as do many of the example projects at `examples.pyviz.org `_. + +If you have any questions or usage issues visit the `Param Discourse site `_, and if you want to report bugs or request new features, first see if it's already in our list of `open issues `_ and then add to the issue or open a new one if needed. + +If you like Param and have built something you want to share, tweet a link or screenshot of your latest creation at `@HoloViz_org `_. Thanks! diff --git a/doc/comparisons.rst b/doc/comparisons.rst new file mode 100644 index 000000000..6eec0eb28 --- /dev/null +++ b/doc/comparisons.rst @@ -0,0 +1,16 @@ +.. + Originally generated by nbsite (0.6.8a29): + /Users/jbednar/miniconda3/envs/test-environment/bin/nbsite generate-rst --org holoviz --project-name param --skip ^.*homepage.*$ + Will not subsequently be overwritten by nbsite, so can be edited. + +*********** +Comparisons +*********** + +.. notebook:: param ../examples/comparisons.ipynb + :offset: 1 + + +------- + +`Right click to download this notebook from GitHub. `_ diff --git a/doc/conf.py b/doc/conf.py index 7811bf433..a11d924da 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -2,39 +2,52 @@ from nbsite.shared_conf import * -project = u'Param' -authors = u'HoloViz authors' -copyright = u'\u00a9 2005-2018, ' + authors -description = 'Declarative Python programming using Parameters.' +project = u'param' +authors = u'HoloViz developers' +copyright = u'2003-2020 ' + authors +description = 'Declarative Python programming using Parameters' import param -version = release = param.__version__ +version = release = str(param.__version__) + +param.parameterized.docstring_signature = False +param.parameterized.docstring_describe_params = False + +nbbuild_cell_timeout = 600 html_static_path += ['_static'] html_theme = 'sphinx_holoviz_theme' html_theme_options = { - 'logo':'logo.png', - 'favicon':'favicon.ico', + 'favicon': 'favicon.ico', + 'logo': 'logo_horizontal_white.svg', + 'include_logo_text': False, + 'primary_color': '#266498', + 'primary_color_dark': '#1b486e', + 'secondary_color': '#5f9df0', # 'css':'site.css' + 'second_nav': True, + 'footer': False, } _NAV = ( - ('API', 'Reference_Manual/param'), - ('About', 'About'), + ('Getting Started', 'getting_started'), + ('User Guide', 'user_guide/index'), + ('API', 'Reference_Manual/index'), + ('About', 'about') ) html_context.update({ 'PROJECT': project, 'DESCRIPTION': description, 'AUTHOR': authors, - # canonical URL (for search engines); can ignore for local builds - 'WEBSITE_SERVER': 'https://param.holoviz.org', 'VERSION': version, 'GOOGLE_ANALYTICS_UA': 'UA-154795830-6', + 'WEBSITE_URL': 'https://param.holoviz.org', + 'WEBSITE_SERVER': 'https://param.holoviz.org', 'NAV': _NAV, 'LINKS': _NAV, 'SOCIAL': ( - ('Gitter', '//gitter.im/pyviz/pyviz'), - ('Github', '//github.com/ioam/param'), + ('Discourse', '//discourse.holoviz.org'), + ('Github', '//github.com/holoviz/param'), ) }) diff --git a/doc/getting_started.rst b/doc/getting_started.rst new file mode 100644 index 000000000..11a7e37b7 --- /dev/null +++ b/doc/getting_started.rst @@ -0,0 +1,16 @@ +.. + Originally generated by nbsite (0.6.8a29): + /Users/jbednar/miniconda3/envs/test-environment/bin/nbsite generate-rst --org holoviz --project-name param --skip ^.*homepage.*$ + Will not subsequently be overwritten by nbsite, so can be edited. + +*************** +Getting Started +*************** + +.. notebook:: param ../examples/getting_started.ipynb + :offset: 0 + + +------- + +`Right click to download this notebook from GitHub. `_ diff --git a/doc/index.rst b/doc/index.rst index 002594201..fc8449f80 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -1,20 +1,19 @@ -.. - Originally generated by nbsite (0.4.4a13+gdbf7de7-dirty): - nbsite generate-rst --org ioam --project param --repo param --examples-path examples --doc-path doc - Will not subsequently be overwritten by nbsite, so can be edited. +.. raw:: html -***** -Param -***** +

.. notebook:: param ../examples/index.ipynb - :offset: 0 + :offset: 0 .. toctree:: - :titlesonly: - :maxdepth: 2 - - Introduction - API - About + :hidden: + :maxdepth: 2 + Introduction + Getting Started + User Guide + Comparisons + Roadmap + API + Github Source + About diff --git a/doc/roadmap.rst b/doc/roadmap.rst new file mode 100644 index 000000000..3b98efed3 --- /dev/null +++ b/doc/roadmap.rst @@ -0,0 +1,11 @@ +.. + Originally generated by nbsite (0.6.8a29): + /Users/jbednar/miniconda3/envs/test-environment/bin/nbsite generate-rst --org holoviz --project-name param --skip ^.*homepage.*$ + Will not subsequently be overwritten by nbsite, so can be edited. + +******* +Roadmap +******* + +.. notebook:: param ../examples/roadmap.ipynb + :offset: 0 diff --git a/doc/user_guide/Simplifying_Codebases.rst b/doc/user_guide/Simplifying_Codebases.rst new file mode 100644 index 000000000..2423776a4 --- /dev/null +++ b/doc/user_guide/Simplifying_Codebases.rst @@ -0,0 +1,16 @@ +.. + Originally generated by nbsite (0.6.8a29): + /Users/jbednar/miniconda3/envs/test-environment/bin/nbsite generate-rst --org holoviz --project-name param --skip ^.*homepage.*$ + Will not subsequently be overwritten by nbsite, so can be edited. + +********************* +Simplifying Codebases +********************* + +.. notebook:: param ../../examples/user_guide/Simplifying_Codebases.ipynb + :offset: 1 + + +------- + +`Right click to download this notebook from GitHub. `_ diff --git a/doc/user_guide/index.rst b/doc/user_guide/index.rst new file mode 100644 index 000000000..f4f00bb51 --- /dev/null +++ b/doc/user_guide/index.rst @@ -0,0 +1,22 @@ +.. + Originally generated by nbsite (0.6.8a29): + /Users/jbednar/miniconda3/envs/test-environment/bin/nbsite generate-rst --org holoviz --project-name param --skip ^.*homepage.*$ + Will not subsequently be overwritten by nbsite, so can be edited. + +********** +User Guide +********** + +.. notebook:: param ../../examples/user_guide/index.ipynb + :offset: 1 + +.. toctree:: + :titlesonly: + :maxdepth: 2 + + Simplifying Codebases + + +------- + +`Right click to download this notebook from GitHub. `_ diff --git a/examples/About.ipynb b/examples/About.ipynb deleted file mode 100644 index 8340ecbd7..000000000 --- a/examples/About.ipynb +++ /dev/null @@ -1,34 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Param is part of the [HoloViz](https://holoviz.org) family of tools. The\n", - "[holoviz.org](https://holoviz.org) website shows how to use Param\n", - "together with other libraries to solve complex problems, with detailed\n", - "tutorials and examples.\n", - "\n", - "Param is completely open source, available under a [BSD license](https://github.com/holoviz/param/blob/master/LICENSE.txt) freely for both commercial and non-commercial use. Param was originally developed by the Neural Networks Research Group at the University of Texas at Austin and the Institute for Adaptive and Neural Computation at the University of Edinburgh with support from the United States National Institutes of Health (grant 1R01-MH66991), and is now maintained by developers at [Anaconda Inc.](https://anaconda.com) and community contributors.\n", - "\n", - "If you have any questions or usage issues visit the [Param Discourse](https://discourse.holoviz.org/c/param/) site. If you are interested in contributing to Param development to help address some of the [open issues](https://github.com/holoviz/param/issues), see our [developer instructions](https://param.org/getting_started/#developer-instructions) to set up your development environment.\n", - "\n", - "Param is part of [HoloViz](http://holoviz.org), a collaborative project\n", - "to produce a coherent solution to a wide range of Python visualization\n", - "problems.\n", - "\n", - "If you like Param and have built something you want to share, tweet a link or screenshot of your latest creation at @HoloViz_org. Thanks!\n", - "\n", - "\n" - ] - } - ], - "metadata": { - "language_info": { - "name": "python", - "pygments_lexer": "ipython3" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/examples/assets/logo_stacked.png b/examples/assets/logo_stacked.png new file mode 100644 index 000000000..05a9425fb Binary files /dev/null and b/examples/assets/logo_stacked.png differ diff --git a/examples/comparisons.ipynb b/examples/comparisons.ipynb new file mode 100644 index 000000000..0c2f316bb --- /dev/null +++ b/examples/comparisons.ipynb @@ -0,0 +1,129 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Comparison to other approaches" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Param was first developed in 2003 for Python 2.1 as part of a long-running brain simulation [project](https://topographica.org), and was made into a separate package on [Github](https://github.com/holoviz/param/graphs/contributors) in 2012. In the interim a variety of other libraries solving some of the same problems have been developed, including:\n", + "\n", + "- [Traits](http://code.enthought.com/projects/traits)\n", + "- [Traitlets](https://github.com/ipython/traitlets/)\n", + "- [attrs](https://github.com/python-attrs/attrs) (with optional [attrs-strict](https://github.com/bloomberg/attrs-strict))\n", + "- [Django models](https://docs.djangoproject.com/en/3.1/topics/db/models/)\n", + "\n", + "Plus, Python itself has incorporated mechanisms addressing some of the same issues:\n", + "\n", + "- [Python 3.6+ type annotations](https://www.python.org/dev/peps/pep-0526/)\n", + "- [Python 3.7+ data classes](https://docs.python.org/3/library/dataclasses.html)\n", + "- [Python 2.6+ namedtuples](https://docs.python.org/3/library/collections.html#namedtuple-factory-function-for-tuples-with-named-fields)\n", + "- [Python 2.2+ properties](https://docs.python.org/3/library/functions.html#property)\n", + "\n", + "Each of these approaches overlaps with some but by no means all of the functionality provided by Param, as described below. Also see the comparisons provided with [attrs](https://www.attrs.org/en/stable/why.html) and by an [attr user](https://glyph.twistedmatrix.com/2016/08/attrs.html), which were written about `attrs` but also apply just as well to Param (with Param differing in also providing e.g. GUI support as listed below). \n", + "\n", + "Here we will use the word \"parameter\" as a generic term for a Python attribute, a Param Parameter, a Traitlets/HasTraits trait, or an attr `attr.ib`.\n", + "\n", + "\n", + "## Brevity of code\n", + "\n", + "Python properties can be used to express nearly anything Param or Traitlets can do, but they require at least an order of magnitude more code to do it. You can think of Param and Traitlets as a pre-written implementation of a Python property that implements a configurable parameter. Avoiding having to write that code each time is a big win, because configurable parameters are all over any Python codebase, and Parameter/attr.ib/Traits-based approaches lead to much simpler and more maintainable codebases.\n", + "\n", + "Specifically, where Param or Traitlets can express an automatically validated type and bounds on an attribute in a simple and localized one-line declaration like `a = param.Integer(5, bounds=(1,10))`, implementing the same functionality using properties requires changes to the constructor plus separate explicit `get` and `set` methods, each with at least a half-dozen lines of validation code. Though this get/set/validate code may seem easy to write, it is difficult to read, difficult to maintain, and difficult to make comprehensive or exhaustive.\n", + "In practice, most programmers simply skip validation or implement it only partially, leaving their code behaving in undefined ways for unexpected inputs. With Param or Traitlets, you don't have to choose between short/readable/maintainable code and heavily validated code; you can have both for far less work!\n", + "\n", + "`attrs` provides many of these same benefits, though with type and bounds validation treated as an extra step that is more general but also typically much more verbose. \n", + "\n", + "## Runtime checking\n", + "\n", + "Python 3 type annotations allow users to specify types for attributes and function returns, but these types are not normally checked at runtime, and so they do not have the same role of validating user input or programmer error as the type declarations in Params, Traits, Traitlets, and attr. They also are limited to the type, so they cannot enforce constraints on range ('state' must be in the list ['Alabama', 'Alaska',...]). Thus even if type hinting is used, programmers still need to write code to actually validate the inputs to functions and methods, which is the role of packages like Param and Traitlets.\n", + "\n", + "## Generality and ease of integration with your project\n", + "\n", + "The various Python features listed above are part of the standard library with the versions indicated above, and so do not add any dependencies at all to your build process, as long as you restrict yourself to the Python versions where that support was added. \n", + "\n", + "Param, Traitlets, and attrs are all pure Python projects, with minimal dependencies, and so adding them to any project is generally straightforward. They also support a wide range of Python versions, making them usable in cases where the more recent Python-language features are not available.\n", + "\n", + "Django models offer some of the same ways to declare parameters and generate web-based GUIs (below), but require the extensive Django web framework and normally rely on a database and web server, which in practice limit their usage to users building dedicated web sites, unlike the no-dependency Param and attrs libraries that can be added to Python projects of any type.\n", + "\n", + "Traits is a very heavyweight solution, requiring installation and C compilation of a large suite of tools, which makes it difficult to include in separate projects.\n", + "\n", + "## GUI toolkits\n", + "\n", + "Several of these packages support automatically mapping parameters/traits/attributes into GUI widgets. Although any of them could in principle be supported for any GUI toolkit, only certain GUI interfaces are currently available:\n", + "\n", + "- Panel: Jupyter and Bokeh-server support for Param\n", + "- ParamTk: (unsupported) TKinter support for Param\n", + "- IPywidgets: Jupyter support for Traitlets\n", + "- TraitsUI: wxWidgets and Qt support for Traits\n", + "\n", + "## Dynamic values\n", + "\n", + "Param, Traits, Traitlets, and attrs all allow any Python expression to be supplied for initializing parameters, allowing parameter default values to be computed at the time a module is first loaded. Traits and Traitlets also allow a class author to add code for a given parameter to compute a default value on first access. \n", + "\n", + " ```python\n", + " >>> from time import time\n", + " >>> import traitlets as tr\n", + " >>> class A(tr.HasTraits):\n", + " ... instantiation_time = tr.Float()\n", + " ... @tr.default('instantiation_time')\n", + " ... def _look_up_time(self):\n", + " ... return time()\n", + " ... \n", + " >>> a=A()\n", + " >>> a.instantiation_time\n", + " 1475587151.967874\n", + " >>> a.instantiation_time\n", + " 1475587151.967874\n", + " >>> b=A()\n", + " >>> b.instantiation_time\n", + " 1475587164.750875\n", + " ```\n", + "\n", + "Param does not currently provide any special support for programmatic default values, which would need to be set explicitly in the Parameterized object's constructor. On the other hand, Param does allow fully dynamic values for *any* numeric Parameter instance:\n", + "\n", + " ```python\n", + " >>> from time import time\n", + " >>> import param\n", + " >>> class A(param.Parameterized):\n", + " ... val=param.Number(0)\n", + " ... \n", + " >>> a=A()\n", + " >>> a.val\n", + " 0\n", + " >>> a.val=lambda:time()\n", + " >>> a.val\n", + " 1475587455.437027\n", + " >>> a.val\n", + " 1475587456.501314\n", + " ```" + ] + } + ], + "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.7.7" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/examples/getting_started.ipynb b/examples/getting_started.ipynb new file mode 100644 index 000000000..0ff056cdf --- /dev/null +++ b/examples/getting_started.ipynb @@ -0,0 +1,259 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This Getting Started guide helps you install Param and introduces the main ways to use it." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Installation\n", + "\n", + "Param has no required dependencies outside of Python's standard library, and so it is very easy to install.\n", + "\n", + "Official releases of Param are available from [conda](https://anaconda.org/ioam/param) and [PyPI](http://pypi.python.org/pypi/param), and can be installed via `conda install -c pyviz param`, `pip install --user param`, or `pip install param`.\n", + "\n", + "The very latest changes can be obtained via `conda install -c pyviz/label/dev param` or `pip install https://github.com/ioam/param/archive/master.zip`." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Using Param to get simple, robust code\n", + "\n", + "The `param` library gives you Parameters and Parameterized classes. \n", + "\n", + "A Parameter is a special type of Python attribute extended to have various optional features such as type and range checking, dynamically generated values, documentation strings, default values, etc., each of which is inherited from parent classes if not specified in a subclass:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import param\n", + " \n", + "class A(param.Parameterized):\n", + " title = param.String(default=\"sum\", doc=\"Title for the result\")\n", + " \n", + "class B(A):\n", + " a = param.Integer(2, bounds=(0,10), doc=\"First addend\")\n", + " b = param.Integer(3, bounds=(0,10), doc=\"Second addend\")\n", + " \n", + " def __call__(self):\n", + " return self.title + \": \" + str(self.a + self.b)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "o1 = B(b=4, title=\"Sum\")\n", + "o1.a=5\n", + "o1()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "o1.b" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As you can see, the Parameters defined here work precisely like any other Python attributes in your code, so it's generally quite straightforward to migrate an existing class to use Param. Just inherit from `param.Parameterized`, then provide an optional `Parameter` declaration for each parameter the object accepts, including ranges and allowed values if appropriate. You only need to declare and document each parameter _once_, at the highest class where it applies, and all the metadata will be inherited by subclasses.\n", + "\n", + "Once you've declared your parameters, a whole wealth of features and better behavior is now unlocked! For instance, what happens if a user tries to supply some inappropriate data? With Param, such errors will be caught immediately:\n", + "\n", + "```\n", + ">>> ParamClass(a=\"four\")\n", + "ValueError: Parameter 'a' must be an integer.\n", + "\n", + ">>> o2 = ParamClass()\n", + ">>> o2.b = -5\n", + "ValueError: Parameter 'b' must be at least 0\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Of course, you could always add more code to an ordinary Python class to check for errors like that, but as described in the [User Guide](user_guide/Simplifying_Codebases.ipynb), that quickly gets unwieldy, with dozens of lines of exceptions, assertions, property definitions, and decorators that obscure what you actually wrote your code to do. Param lets you focus on the code you're writing, while letting your users know exactly what inputs they can supply. \n", + "\n", + "The types in Param may remind you of the static types found in some languages, but here the validation is done at runtime and is checking not just types but also numeric ranges or for specific allowed values. Param thus helps you not just with programming correctness, as for static types, but also for validating user inputs, which is generally a large fraction of a program's code.\n", + "\n", + "The [User Guide](user_guide/index.ipynb) explains all the other Param features for simplifying your codebase, improving input validation, allowing flexible configuration, and supporting serialization." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Using Param for configuration\n", + "\n", + "Once you have declared your Parameters, they are now fully accessible from Python in a way that helps users of your code configure it and control it if they wish. Without any extra work by the author of the class, a user can use Python to reconfigure any of the defaults that will be used when they use these objects:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "A.title = \"The sum is\"\n", + "B.a=6\n", + "\n", + "o3 = B()\n", + "o3()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Because this configuration is all declarative, the underlying values can come from a YAML file, a JSON blob, URL parameters, CLI arguments, or just about any source, letting you provide users full control over configuration with very little effort. Once you write a Parameterized class, it's up to a user to choose how they want to work with it; your job is done!" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Using Param to explore parameter spaces\n", + "\n", + "Param is valuable for _any_ Python codebase, but it offers features that are particularly well suited for running models, simulations, machine learning pipelines, or other programs where the same code needs to be evaluated multiple times to see how it behaves with different parameter values. To facilitate such usage, numeric parameters in Param can be set to a callable value, which will be evaluated every time the parameter is accessed:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import random\n", + "o2 = B(a = lambda: random.randint(0,5))\n", + "\n", + "o2(), o2(), o2(), o2()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The code for `B` doesn't have to have any special knowledge or processing of dynamic values, because accessing `a` always simply returns an integer, not the callable function:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "o2.a" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Thus the author of a Parameterized class does not have to take such dynamic values into account; their code simply works with whatever value is returned by the attribute lookup, whether it's dynamic or not. This approach makes Parameterized code immediately ready for exploration across parameter values, whether or not the code's author specifically provided for such usage.\n", + "\n", + "Param includes a separate and optional module `numbergen` that makes it simple to generate streams of numeric values for use as Parameter values. `numbergen` objects are picklable (unlike a `lambda` as above) and can be combined into expressions to build up parameter sweeps or Monte Carlo simulations:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import numbergen as ng\n", + "\n", + "o3 = B(a = ng.Choice(choices=[2,4,6]),\n", + " b = 1+2*ng.UniformRandomInt(ubound=3))\n", + "\n", + "o3(), o3(), o3(), o3()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Numbergen objects support the usual arithmetic operations like +, -, *, /, //, %, **, and `abs()`, and so they can be freely combined with each other or with mathematical constants. They also optionally respect a global \"time\" (e.g. a simulation time or a logical counter), which lets you synchronize changes to dynamic values without any speciall coordination code." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Using Param to build GUIs\n", + "\n", + "Param is useful for any sort of programming, but if you need a GUI with widgets, it turns out that the information captured by a Parameter is very often already what is needed to build such a GUI. For instance, we can use the separate [Panel](https::/panel.holoviz.org) library to create widgets in a web browser and display the output from the above class automatically:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import panel as pn\n", + "pn.extension()\n", + "\n", + "pn.Row(o1, o1.__call__).embed(max_opts=10)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Here Panel created a widget for each of the parameters of this object and displayed the result of calling it with the indicated parameters, all without the programmer having to give Panel any information beyond the object itself. \n", + "\n", + "Panel and other GUI libraries can of course explicitly instantiate widgets, so why use Param in this way? Simply put, this approach lets you cleanly separate your domain-specific code, clearly declaring the parameters it requires and respects, from your GUI code. The GUI code controls GUI issues like layout and font size, but the fundamental declaration of what parameters are available is done at the level of the code that actually uses it (classes A and B in this case). With Param, you can _separately_ declare all your Parameters right where they are used, achieving robustness, type checking, and clear documentation, while avoiding having your GUI code be tightly bound up with your domain-specific details. This approach helps you build maintainable, general-purpose codebases that can easily be used with or without GUI interfaces, with unattended batch operation not needing any GUI support and GUIs not needing to be updated every time someone adds a new option or parameter to the underlying code." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Learning more\n", + "\n", + "The [User Guide](user_guide/index.ipynb) goes through the major features of Param and how to use them. If you are interested in GUI programming, also see the [Param guide](https://panel.holoviz.org/user_guide/Param.html) in Panel, and the rest of the [Panel](https://panel.holoviz.org) docs. Have fun making your life better with Param!" + ] + } + ], + "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.7.7" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/examples/index.ipynb b/examples/index.ipynb index 9f2c62204..bf4079d92 100644 --- a/examples/index.ipynb +++ b/examples/index.ipynb @@ -4,283 +4,17 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Param is a library providing Parameters: Python attributes extended to\n", - "have features such as type and range checking, dynamically generated\n", - "values, documentation strings, default values, etc., each of which is\n", - "inherited from parent classes if not specified in a subclass. Param\n", - "lets you program declaratively in Python, by just stating facts about\n", - "each of your parameters, and then using them throughout your code.\n", - "With Parameters, error checking will be automatic, which eliminates\n", - "huge amounts of boilerplate code that would otherwise be required to\n", - "verify or test user-supplied values.\n", + "Are you a Python programmer? If so, you need Param!\n", "\n", - "Param-based programs tend to contain much less code than other Python\n", - "programs, instead just having easily readable and maintainable\n", - "manifests of Parameters for each object or function. This way your\n", - "remaining code can be much simpler and clearer, while users can also\n", - "easily see how to use it properly." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# What is a Parameter?\n", - "\n", - "A Parameter is a special type of Python attribute extended to have features such as type and range checking, dynamically generated values, documentation strings, default values, etc., each of which is inherited from parent classes if not specified in a subclass.\n", - "\n", - "```python\n", - ">>> import param,random\n", - ">>> class A(param.Parameterized):\n", - "... a = param.Number(0.5,bounds=(0,1),doc=\"Probability that...\")\n", - "... b = param.Boolean(False,doc=\"Enable feature...\")\n", - "\n", - ">>> class B(A):\n", - "... b = param.Boolean(True)\n", - "\n", - ">>> x = B(a=lambda: random.uniform(0,1))\n", - "\n", - ">>> x.a\n", - "0.37053399325641945\n", - "\n", - ">>> x.a\n", - "0.64907392300071842\n", - "```\n", - "\n", - "## Parameters provide optional range and type checking\n", - "\n", - "```python\n", - ">>> x.a=5\n", - "[...]\n", - "ValueError: Parameter 'a' must be at most 1\n", - "\n", - ">>> x.a=\"0.5\"\n", - "[...]\n", - "ValueError: Parameter 'a' only takes numeric values\n", - "```\n", - "\n", - "## Parameters have docstrings\n", - "\n", - "```python\n", - ">>> help(x)\n", - "[...]\n", - "class B(A)\n", - "[...]\n", - " Data descriptors defined here:\n", - " b\n", - " Enable feature...\n", - "[...]\n", - " Data descriptors inherited from A:\n", - " a\n", - " Probability that...\n", - "```\n", - "\n", - "## Param is lightweight\n", - "\n", - "Param consists of two required BSD-licensed Python files, with no\n", - "dependencies outside of the standard library, and so it can easily be\n", - "included as part of larger projects without adding external dependencies.\n", - "\n", - "\n", - "## Parameters make GUI programming simpler\n", - "\n", - "Parameters make it simple to generate GUIs by separating your semantic\n", - "information (what is this parameter? what type can it have? does it\n", - "have bounds?) from anything to do with a particular GUI library. To\n", - "use Parameters in a particular GUI toolkit, you just need to write a\n", - "simple set of interfaces that indicate how a given Parameter type\n", - "should be displayed, and what widgets to generate for it. Currently,\n", - "interfaces are provided for use in Jupyter Notebooks ([ParamNB]\n", - "(https://github.com/ioam/paramnb)) \n", - "or in Tk ([ParamTk](http://ioam.github.com/paramtk)), both of which\n", - "make it simple to provide a property sheet that automatically\n", - "generates a set of widgets for viewing and editing an object's\n", - "Parameters.\n", - "\n", - "\n", - "## Optional dynamic parameter values using `numbergen`\n", - "\n", - "Providing random or other types of varying values for parameters can\n", - "be tricky, because unnamed (\"lambda\") functions as used above cannot\n", - "easily be pickled, causing problems for people who wish to store\n", - "Parameterized objects containing random state. To avoid users having\n", - "to write a separate function for each random value, Param includes an\n", - "optional set of value-generating objects that are easily configured\n", - "and support pickling. These objects are available if you import the\n", - "optional `numbergen` module. If you wish to use numbergen, the above\n", - "example can be rewritten as:\n", - "\n", - "```python\n", - ">>> import param,numbergen\n", - ">>> class A(param.Parameterized):\n", - "... a = param.Number(0.5,bounds=(0,1),doc=\"Probability that...\")\n", - "... b = param.Boolean(False,doc=\"Enable feature...\")\n", - "\n", - ">>> class B(A):\n", - "... b = param.Boolean(True)\n", - "\n", - ">>> x = B(a=numbergen.UniformRandom())\n", - "``` \n", - "\n", - "Numbergen objects support the usual arithmetic operations like `+`, `-`,\n", - "`*`, `/`, `//`, `%`, `**`, and `abs()`, and so they can be freely combined with\n", - "each other or with mathematical constants:\n", + "Param is a library for handling all the user-modifiable parameters, arguments, and attributes that control your code. It provides automatic, robust error-checking while dramatically reducing boilerplate code, letting you focus on what you want your code to do rather than on checking for all the possible ways users could supply inappropriate values to a function or class.\n", "\n", - "```python\n", - ">>> y = B(a=2.0*numbergen.UniformRandom()/(numbergen.NormalRandom()+1.5))\n", - "```\n", + "Param lets you program declaratively in Python, stating facts about each of your parameters up front. Once you have done that, Param can handle the rest (type checking, range validation, documentation, serialization, and more!). \n", "\n", - "Note that unlike the lambda-function approach, all varying numbergen\n", - "objects respect `param.Dynamic.time_fn`, e.g. to ensure that new\n", - "values will be generated only when Param's time has changed. \n", - "Parameterized programs can define a time function to maintain a\n", - "logical/simulated time, such as the state of a simulator, which\n", - "allows all Parameter values to be kept synchronized without\n", - "any special coordination code.\n", - "\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Installation\n", - "\n", - "Param has no required dependencies outside of Python's standard\n", - "library.\n", - "\n", - "Official releases of Param are available on\n", - "[Anaconda](https://anaconda.org/ioam/param) and\n", - "[PyPI](http://pypi.python.org/pypi/param), and can be installed via\n", - "`conda install -c ioam param`, `pip install --user param`, or \n", - "`pip install param`.\n", - "\n", - "The very latest changes can be obtained via `conda install -c pyviz/label/dev\n", - "param` or `pip install\n", - "https://github.com/ioam/param/archive/master.zip`.\n", - "\n", - "For development, the [git repository](http://github.com/ioam/param)\n", - "can be cloned and then 'develop installed' (`pip install -e .` or\n", - "`python setup.py develop`). Tests can be run via [tox]\n", - "(https://tox.readthedocs.io/en/latest/): `tox` for all tests, or\n", - "e.g. `tox -e coverage` to run unit tests with coverage for the\n", - "currently active python. Alternatively, unit tests can be run via\n", - "`nosetests` (after installing [nose](http://nose.readthedocs.io/en/latest)." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Comparison to other packages\n", - "\n", - "Param was first developed in 2003, in the context of the Topographica brain simulator project, and\n", - "was made into a separate package in 2012. In the interim other parameter libraries were\n", - "developed, including [Traits](http://code.enthought.com/projects/traits) and \n", - "[Traitlets](https://github.com/ipython/traitlets/). These libraries have broadly similar goals,\n", - "but each differs in important ways:\n", - "\n", - "**Dependencies**: \n", - " Traits is a much more heavyweight solution, requiring \n", - " installation of a large suite of tools, including C code, which makes it difficult to include in \n", - " separate projects. In contrast, Param and Traitlets are both pure Python projects, with minimal dependencies. \n", - "\n", - "**GUI toolkits**: \n", - " Although any of the packages could in principle add support for any\n", - " GUI toolkit, the toolkits actually provided differ: Traits (via the\n", - " separate TraitsUI package) supports wxWidgets and QT, while Param\n", - " supports Tkinter (via the separate ParamTk package) and\n", - " browser-based IPython widgets (via the separate ParamNB package),\n", - " while Traitlets only supports IPython widgets.\n", - "\n", - " ```python\n", - " >>> from time import time\n", - " >>> import traitlets as tr\n", - " >>> class A(tr.HasTraits):\n", - " ... instantiation_time = tr.Float()\n", - " ... @tr.default('instantiation_time')\n", - " ... def _look_up_time(self):\n", - " ... return time()\n", - " ... \n", - " >>> a=A()\n", - " >>> a.instantiation_time\n", - " 1475587151.967874\n", - " >>> a.instantiation_time\n", - " 1475587151.967874\n", - " >>> b=A()\n", - " >>> b.instantiation_time\n", - " 1475587164.750875\n", - " ```\n", - "\n", - "**Dynamic values**:\n", - " Param, Traits, and Traitlets all allow any Python expression to be\n", - " supplied for initializing parameters, allowing parameter default\n", - " values to be computed at the time a module is first loaded. Traits\n", - " and Traitlets also allow a class author to add code for a given\n", - " parameter to compute a default value on first access. Param does\n", - " not provide any special support for programmatic default values,\n", - " instead allowing fully dynamic values for *any* numeric Parameter\n", - " instance:\n", - "\n", - " ```python\n", - " >>> from time import time\n", - " >>> import param\n", - " >>> class A(param.Parameterized):\n", - " ... val=param.Number(0)\n", - " ... \n", - " >>> a=A()\n", - " >>> a.val\n", - " 0\n", - " >>> a.val=lambda:time()\n", - " >>> a.val\n", - " 1475587455.437027\n", - " >>> a.val\n", - " 1475587456.501314\n", - " ```\n", - " \n", - " Note that here it is the *user* of a Parameterized class, not the\n", - " author of the class, that decides whether any particular value is\n", - " dynamic, without writing any new methods or other code. All the\n", - " usual type checking, etc. is done on dynamic values when they are\n", - " computed, and so the rest of the code does not need to know or care\n", - " whether the user has set a particular parameter to a dynamic value.\n", - " This approach provides an enormous amount of power to the user,\n", - " without making the code more complex.\n", - "\n", - "**On_change callbacks**\n", - " Traitlets and Traits allow the author of a HasTraits-derived class\n", - " to specify code to run when a specific parameter used in that class\n", - " instance is modified. Param supports similar capabilities, but not\n", - " at the Parameterized class level, only at the Parameter class level\n", - " or as part of ParamNB. I.e., a class author needs to first write a\n", - " new Parameter class, adding methods to implement checking on\n", - " changes, and then add it to a Parameterized class, or else such\n", - " functionality can be added as callbacks at the whole-object level,\n", - " using ParamNB. Each approach has advantages and disadvantages, and\n", - " per-parameter on_change callbacks could be added in the future if\n", - " there are clear use cases.\n", - "\n", - "All of these packages also overlap in functionality with Python\n", - "properties, which were added to the language after Traits and Param\n", - "were developed. Like parameters and traits, properties act like\n", - "attributes with possible method-like actions, and so they can all be\n", - "used to provide the same user-visible functionality. However,\n", - "implementing Param/Traits-like functionality using properties would\n", - "require vastly more code (multiple method definitions for *every*\n", - "parameter in a class), and so in practice Parameters and Traits are\n", - "much more practical for the use cases that they cover.\n", - " " - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Release notes\n", + "Param-based programs tend to contain much less code than other Python programs, instead just having easily readable and maintainable manifests of Parameters for each object or function. This way your remaining code can be much simpler and clearer, while users can also easily see how to use it properly. Plus, Param doesn't require any code outside of the Python standard library, making it simple to add to any project. \n", "\n", - "Recent release notes are available on [GitHub](https://github.com/ioam/param/releases).\n", + "Param is also useful as a way to keep your domain-specific code independent of any GUI or other user-interface code, letting you maintain a single codebase to support both GUI and non-GUI usage, with the GUI maintainable by UI experts and the domain-specific code maintained by domain experts.\n", "\n", - "For older releases, see our [historical release notes](historical_release_notes.html).\n" + "To quickly see how Param works and can be used, jump straight into the [Getting Started Guide](getting_started.ipynb), then check out the full functionality in the [User Guide.](user_guide/index.ipynb)" ] } ], diff --git a/examples/roadmap.ipynb b/examples/roadmap.ipynb new file mode 100644 index 000000000..1360034e9 --- /dev/null +++ b/examples/roadmap.ipynb @@ -0,0 +1,45 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Param is a mature library (originally from 2003) that changes very slowly and very rarely; it is fully ready for use in production applications. Major changes are undertaken only after significant discussions and with attention to how existing Param-based applications will be affected. Thus Param users should not expect only slow progress on these roadmap items, but they are listed here in the hopes that they will be useful.\n", + "\n", + "Currently scheduled plans:\n", + "\n", + "- Make there be documentation. We have funding for this and will do it in 2020. This includes both user guide/tutorial material and reference material, covering both how it's currently used in HoloViz and on the underlying design and general functionality independent of HoloViz (as indeed it predates all of HoloViz). This will be a major step forward in how suitable Param is for general usage.\n", + "\n", + "- Improve the Param website (same time as docs; just make it look decent!)\n", + "\n", + "- Clean up the Parameterized namespace (remove nearly all methods not on .param) and other API warts, scheduled for Param 2.0 since those are breaking changes. See https://github.com/holoviz/param/issues/154 and https://github.com/holoviz/param/issues/233.\n", + "\n", + "- More powerful serialization (to JSON, YAML, and URLs) to make it simpler to persist the state of a Parameterized object. Some support already merged as https://github.com/holoviz/param/pull/414 , but still to be further developed as support for using Parameterized objects to build REST APIS (see https://github.com/holoviz/monitor for example usage).\n", + "\n", + "Other items that are not yet scheduled but would be great to have:\n", + "\n", + "- Integrate more fully with Python 3 language features like [type annotations](https://www.python.org/dev/peps/pep-0526) and/or [data classes], e.g. to respect and validate against declared types without requiring an explicit `param.Parameter` declaration, and to better support IDE type-checking features.\n", + "\n", + "- Integrate and interoperate more fully with other frameworks like Django models, Traitlets, attrs, Django models, or swagger/OpenAPI, each of which capture or can use similar information about parameter names, values, and constraints and so in many cases can easily be converted from one to the other.\n", + "\n", + "- Improve support for Param in editors, automatic formatting tools, linters, document generators, and other tools that process Python code and could be made to have special-purpose optimizations specifically for Parameterized objects.\n", + "\n", + "- Follow PEP8 more strictly: PEP8 definitely wasn't written with Parameters in mind, and it typically results in badly formatted files when applied to Parameterized code. But PEP8 could be applied to Param's own code.\n", + "\n", + "- Triaging open issues: The Param developer team consists of volunteers typically using Param on their projects but not explicity tasked with or funded to work on Param itself. It would thus be great if the more experienced Param users could help address some of the issues that have been raised but not yet solved.\n", + "\n", + "- Improve test coverage\n", + "\n", + "Other [issues](https://github.com/holoviz/param/issues) are collected on Github and will be addressed on various time scales as indicated by the issue's milestone (typically next minor release, next major release, or \"wishlist\" (not scheduled or assigned to any person but agreed to be desirable). Any contributor is encouraged to attempt to implement a \"wishlist\" item, though if it is particularly complex or time consuming it is useful to discuss it first with one of the core maintainers (e.g. by stating your intentions on the issue)." + ] + } + ], + "metadata": { + "language_info": { + "name": "python", + "pygments_lexer": "ipython3" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/examples/user_guide/Simplifying_Codebases.ipynb b/examples/user_guide/Simplifying_Codebases.ipynb new file mode 100644 index 000000000..511dd7409 --- /dev/null +++ b/examples/user_guide/Simplifying_Codebases.ipynb @@ -0,0 +1,381 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from contextlib import contextmanager\n", + "@contextmanager\n", + "def exceptions_summarized():\n", + " try:\n", + " yield\n", + " except Exception:\n", + " import sys\n", + " etype, value, tb = sys.exc_info()\n", + " print(f\"{etype.__name__}: {value}\", file=sys.stderr)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Param's just a Python library, and so anything you can do with Param you can do \"manually\". So, why use Param?\n", + "\n", + "The most immediate benefit to using Param is that it allows you to greatly simplify your codebases, making them much more clear, readable, and maintainable, while simultaneously providing robust handling against error conditions.\n", + "\n", + "Param does this by letting a programmer explicitly declare the types and values of parameters accepted by the code. Param then ensures that only suitable values of those parameters ever make it through to the underlying code, removing the need to handle any of those conditions explicitly.\n", + "\n", + "To see how this works, let's create a Python class with some attributes without using Param:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "class OrdinaryClass(object):\n", + " def __init__(self, a=2, b=3, title=\"sum\"):\n", + " self.a = a\n", + " self.b = b\n", + " self.title = title\n", + " \n", + " def __call__(self):\n", + " return self.title + \": \" + str(self.a + self.b)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As this is just standard Python, we can of course instantiate this class, modify its variables, and call it:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "o1 = OrdinaryClass(b=4, title=\"Sum\")\n", + "o1.a=4\n", + "o1()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The same code written using Param would look like:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import param\n", + " \n", + "class ParamClass(param.Parameterized):\n", + " a = param.Integer(2, bounds=(0,1000), doc=\"First addend\")\n", + " b = param.Integer(3, bounds=(0,1000), doc=\"Second addend\")\n", + " title = param.String(default=\"sum\", doc=\"Title for the result\")\n", + " \n", + " def __call__(self):\n", + " return self.title + \": \" + str(self.a + self.b)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "o2 = ParamClass(b=4, title=\"Sum\")\n", + "o2()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As you can see, the Parameters here are used precisely like normal attributes once they are defined, so the code for `__call__` and for invoking the constructor are the same in both cases. It's thus generally quite straightforward to migrate an existing class into Param. So, why do that?\n", + "\n", + "Well, with fewer lines of code than the ordinary class, you've now unlocked a whole wealth of features and better behavior! For instance, what happens if a user tries to supply some inappropriate data? With Param, such errors will be caught immediately:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "with exceptions_summarized():\n", + " ParamClass(a=\"four\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "with exceptions_summarized(): \n", + " o3 = ParamClass()\n", + " o3.b = -5" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Of course, you could always add more code to an ordinary Python class to check for errors like that, but it quickly gets unwieldy:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "class OrdinaryClass2(object):\n", + " def __init__(self, a=2, b=3, title=\"sum\"):\n", + " if type(a) is not int:\n", + " raise ValueError(\"'a' must be an integer\")\n", + " if type(b) is not int:\n", + " raise ValueError(\"'b' must be an integer\")\n", + " if a<0:\n", + " raise ValueError(\"'a' must be at least `0`\")\n", + " if b<0:\n", + " raise ValueError(\"'b' must be at least `0`\")\n", + " if type(title) is not str:\n", + " raise ValueError(\"'title' must be a string\") \n", + " \n", + " self.a = a\n", + " self.b = b\n", + " self.title = title\n", + " \n", + " def __call__(self):\n", + " return self.title + \": \" + str(self.a + self.b)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "with exceptions_summarized(): \n", + " OrdinaryClass2(a=\"f\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Unfortunately, catching errors in the constructor like that won't help if someone modifies the attribute directly, which won't be detected as an error:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "o4 = OrdinaryClass2()\n", + "o4.a = \"four\"" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Python will happily accept this incorrect value and will continue processing. It may only be much later, in a very different part of your code, that you see a mysterious error message that's then very difficult to relate back to the actual problem you need to fix:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "with exceptions_summarized(): \n", + " o4()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Here there's no problem with the code in the cell above; `o4()` is fully valid Python; the real problem is in the preceding cell, which could have been in a completely different file or library. The error message is also obscure and confusing at this level, because the user of `o4` may have no idea why strings and integers are getting concatenated.\n", + "\n", + "To get a better error message, you _could_ move those checks into the `__call__` method, which would make sure that errors are always eventually detected:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "class OrdinaryClass3(object):\n", + " def __init__(self, a=2, b=3, title=\"sum\"): \n", + " self.a = a\n", + " self.b = b\n", + " self.title = title\n", + " \n", + " def __call__(self):\n", + " if type(self.a) is not int:\n", + " raise ValueError(\"'a' must be an integer\")\n", + " if type(self.b) is not int:\n", + " raise ValueError(\"'b' must be an integer\")\n", + " if self.a<0:\n", + " raise ValueError(\"'a' must be at least `0`\")\n", + " if self.b<0:\n", + " raise ValueError(\"'b' must be at least `0`\")\n", + " if type(self.title) is not str:\n", + " raise ValueError(\"'title' must be a string\") \n", + "\n", + " return self.title + \": \" + str(self.a + self.b)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "o5 = OrdinaryClass3()\n", + "o5.a = \"four\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "with exceptions_summarized(): \n", + " o5()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "But you'd now have to check for errors in _every_ _single_ _method_ that might use those parameters. Worse, you still only detect the problem very late, far from where it was first introduced. Any distance between the error and the error report makes it much more difficult to address, as the user then has to track down where in the code `a` might have gotten set to a non-integer.\n", + "\n", + "With Param you can catch such problems at their start, as soon as an incorrect value is provided, when it is still simple to detect and correct it. To get those same features in hand-written Python code, you would need to provide explicit getters and setters, which is made easier with Python properties and decorators, but is still quite unwieldy:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "class OrdinaryClass4(object):\n", + " def __init__(self, a=2, b=3, title=\"sum\"):\n", + " self.a = a\n", + " self.b = b\n", + " self.title = title\n", + " \n", + " @property\n", + " def a(self): return self.__a\n", + " @a.setter\n", + " def a(self, a):\n", + " if type(a) is not int:\n", + " raise ValueError(\"'a' must be an integer\")\n", + " if a < 0:\n", + " raise ValueError(\"'a' must be at least `0`\")\n", + " self.__a = a\n", + " \n", + " @property\n", + " def b(self): return self.__b\n", + " @b.setter\n", + " def b(self, b):\n", + " if type(b) is not int:\n", + " raise ValueError(\"'a' must be an integer\")\n", + " if b < 0:\n", + " raise ValueError(\"'a' must be at least `0`\")\n", + " self.__b = b\n", + "\n", + " @property\n", + " def title(self): return self.__title\n", + " def title(self, b):\n", + " if type(title) is not string:\n", + " raise ValueError(\"'title' must be a string\")\n", + " self.__title = title\n", + "\n", + " def __call__(self):\n", + " return self.title + \": \" + str(self.a + self.b)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "o5=OrdinaryClass4()\n", + "o5()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "with exceptions_summarized(): \n", + " o5=OrdinaryClass4()\n", + " o5.b=-6" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Note that this code has an easily overlooked mistake in it, reporting `a` rather than `b` as the problem. This sort of error is extremely common in copy-pasted validation code of this type, because tests rarely exercise all of the error conditions involved.\n", + "\n", + "As you can see, even getting close to the automatic validation already provided by Param requires 8 methods and >30 highly repetitive lines of code, even when using relatively esoteric Python features like properties and decorators, and still doesn't yet implement other Param features like automatic documentation, attribute inheritance, or dynamic values. With Param, the corresponding `ParamClass` code only requires 6 lines and no fancy techniques beyond Python classes. Most importantly, the Param version lets readers and program authors focus directly on what this code actually does, which is to compute a function from three provided parameters:\n", + "\n", + "```\n", + "class ParamClass(param.Parameterized):\n", + " a = param.Integer(2, bounds=(0,1000), doc=\"First addend\")\n", + " b = param.Integer(3, bounds=(0,1000), doc=\"Second addend\")\n", + " title = param.String(default=\"sum\", doc=\"Title for the result\")\n", + " \n", + " def __call__(self):\n", + " return self.title + \": \" + str(self.a + self.b)\n", + "```\n", + "\n", + "Even a quick skim of this code reveals what parameters are available, what values they will accept, what the default values are, and how those parameters will be used in the method. Plus the actual code of the method stands out immediately, as all the code is either parameters or actual functionality. In contrast, users of OrdinaryClass3 will have to read through dozens of lines of code to discern even basic information about usage, or else authors of the code will need to create and maintain docstrings that may or may not match the actual code over time and will further increase the amount of text to write and maintain." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Programming contracts\n", + "\n", + "If you think about the examples above, you can see how Param makes it simple for programmers to make a contract with their users, being explicit and clear what will be accepted and rejected, while also allowing programmers to make safe assumptions about what inputs the code may ever receive. There is no need for `__call__` _ever_ to check for the type of one of its parameters, whether it's in the range allowed, or any other property that can be enforced by Param. Your custom code can then be much more linear and straightforward, getting right to work with the actual task at hand, without having to have reams of `if` statements and `asserts()` that disrupt the flow of the source file and make the reader get sidetracked in error-handling code. Param lets you once and for all declare what this code accepts, which is both clear documentation to the user and a guarantee that the programmer can forget about any other possible value a user might someday supply.\n", + "\n", + "Crucially, these contracts apply not just between the user and a given piece of code, but also between components of the system itself. When validation code is expensive, as in ordinary Python, programmers will typically do it only at the edges of the system, where input from the user is accepted. But expressing types and ranges is so easy in Param, it can be done for any major component in the system. The Parameter list declares very clearly what that component accepts, which lets the code for that component ignore all potential inputs that are disallowed by the Parameter specifications, while correctly advertising to the rest of the codebase what inputs are allowed. Programmers can thus focus on their particular components of interest, knowing precisely what inputs will ever be let through, without having to reason about the flow of configuration and data throughout the whole system.\n", + "\n", + "Without Param, you should expect Python code to be full of confusing error checking and handling of different input types, while still only catching a small fraction of the possible incorrect inputs that could be provided. But Param-based code should be dramatically easier to read, easier to maintain, easier to develop, and nearly bulletproof against mistaken or even malicious usage. " + ] + } + ], + "metadata": { + "language_info": { + "name": "python", + "pygments_lexer": "ipython3" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/examples/user_guide/index.ipynb b/examples/user_guide/index.ipynb new file mode 100644 index 000000000..908cf6d8a --- /dev/null +++ b/examples/user_guide/index.ipynb @@ -0,0 +1,36 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# User Guide" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This user guide provides a detailed introduction to the API and features\n", + "of Param. In the [Introduction](Introduction.ipynb) you will learn how to \n", + "get started with Param and start using it. Next you will learn... \n", + "\n", + "- Introduction: Introduction to Param and how to start using it.\n", + "- [Simplifying_Codebases](Simplifying_Codebases.ipynb): How Param allows you to eliminate boilerplate and unsafe code \n", + "- Predefined Parameter types: \n", + "- Serialization:\n", + "- Dependencies and watchers: \n", + "- Dynamic Parameter values: Using dynamic values with and without Numbergen\n", + "- How Param works" + ] + } + ], + "metadata": { + "language_info": { + "name": "python", + "pygments_lexer": "ipython3" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/setup.py b/setup.py index 768885699..46050f545 100644 --- a/setup.py +++ b/setup.py @@ -22,6 +22,12 @@ def get_setup_version(reponame): 'nose', 'flake8', 'jsonschema', + ], + 'doc': [ + 'pygraphviz', + 'nbsite >=0.6.1', + 'sphinx_holoviz_theme', + 'tornado <6.0' ] }