pax_global_header00006660000000000000000000000064143444156400014517gustar00rootroot0000000000000052 comment=897687f71ac4ca7da6d35361ffe832f93a64a128 param-1.12.3/000077500000000000000000000000001434441564000127035ustar00rootroot00000000000000param-1.12.3/.coveragerc000066400000000000000000000000411434441564000150170ustar00rootroot00000000000000[report] omit = param/version.py param-1.12.3/.gitattributes000066400000000000000000000000321434441564000155710ustar00rootroot00000000000000 __init__.py export-subst param-1.12.3/.github/000077500000000000000000000000001434441564000142435ustar00rootroot00000000000000param-1.12.3/.github/workflows/000077500000000000000000000000001434441564000163005ustar00rootroot00000000000000param-1.12.3/.github/workflows/build.yaml000066400000000000000000000056151434441564000202720ustar00rootroot00000000000000name: packages on: push: tags: - 'v[0-9]+.[0-9]+.[0-9]+' - 'v[0-9]+.[0-9]+.[0-9]+a[0-9]+' - 'v[0-9]+.[0-9]+.[0-9]+b[0-9]+' - 'v[0-9]+.[0-9]+.[0-9]+rc[0-9]+' # Dry-run only workflow_dispatch: schedule: - cron: '0 13 * * SUN' jobs: conda_build: name: Build Conda Packages runs-on: 'ubuntu-latest' defaults: run: shell: bash -l {0} env: CHANS_DEV: "-c pyviz/label/dev" PKG_TEST_PYTHON: "--test-python=py37" PYTHON_VERSION: "3.7" CHANS: "-c pyviz" CONDA_UPLOAD_TOKEN: ${{ secrets.CONDA_UPLOAD_TOKEN }} steps: - uses: actions/checkout@v3 - name: Fetch unshallow run: git fetch --prune --tags --unshallow -f - uses: actions/setup-python@v4 with: python-version: "3.7" - uses: conda-incubator/setup-miniconda@v2 with: miniconda-version: "latest" - name: Set output id: vars run: echo "tag=${GITHUB_REF#refs/*/}" >> $GITHUB_OUTPUT - name: conda setup run: | eval "$(conda shell.bash hook)" conda config --set always_yes yes --set changeps1 no conda update conda conda install anaconda-client conda-build - name: conda build run: | eval "$(conda shell.bash hook)" conda build conda.recipe/ - name: conda dev upload if: (github.event_name == 'push' && (contains(steps.vars.outputs.tag, 'a') || contains(steps.vars.outputs.tag, 'b') || contains(steps.vars.outputs.tag, 'rc'))) run: | eval "$(conda shell.bash hook)" anaconda --token $CONDA_UPLOAD_TOKEN upload --user pyviz --label=dev $(conda build --output conda.recipe) - name: conda main upload if: (github.event_name == 'push' && !(contains(steps.vars.outputs.tag, 'a') || contains(steps.vars.outputs.tag, 'b') || contains(steps.vars.outputs.tag, 'rc'))) run: | eval "$(conda shell.bash hook)" anaconda --token $CONDA_UPLOAD_TOKEN upload --user pyviz --label=dev --label=main $(conda build --output conda.recipe) pip_build: name: Build PyPI Packages runs-on: 'ubuntu-latest' defaults: run: shell: bash -l {0} env: TOX_ENV: "py3.7" steps: - uses: actions/checkout@v3 - name: Fetch unshallow run: git fetch --prune --tags --unshallow -f - uses: actions/setup-python@v4 with: python-version: "3.7" - name: env setup run: | python -m pip install --upgrade pip python -m pip install setuptools wheel twine tox - name: pip build run: | python setup.py sdist bdist_wheel - name: Publish package to PyPI if: github.event_name == 'push' uses: pypa/gh-action-pypi-publish@master with: user: ${{ secrets.PPU }} password: ${{ secrets.PPP }} packages_dir: dist/ param-1.12.3/.github/workflows/docs.yaml000066400000000000000000000045531434441564000201230ustar00rootroot00000000000000name: docs on: push: tags: - 'v[0-9]+.[0-9]+.[0-9]+' - 'v[0-9]+.[0-9]+.[0-9]+a[0-9]+' - 'v[0-9]+.[0-9]+.[0-9]+b[0-9]+' - 'v[0-9]+.[0-9]+.[0-9]+rc[0-9]+' workflow_dispatch: inputs: target: description: 'Site to build and deploy' type: choice options: - dev - main - dryrun required: true default: dryrun schedule: - cron: '0 13 * * SUN' jobs: build_docs: name: Documentation runs-on: 'ubuntu-latest' timeout-minutes: 120 defaults: run: shell: bash -l {0} env: DESC: "Documentation build" steps: - uses: actions/checkout@v3 - name: Fetch unshallow run: git fetch --prune --tags --unshallow -f - uses: actions/setup-python@v4 with: python-version: '3.9' - name: Set output id: vars run: echo "tag=${GITHUB_REF#refs/*/}" >> $GITHUB_OUTPUT - name: graphviz run: sudo apt install graphviz graphviz-dev - name: env setup run: | python -m pip install --upgrade pip python -m pip install nbsite==0.8.0rc2 python -m pip install -e .[doc] - name: build docs run: | cp examples/user_guide/*.ipynb doc/user_guide/ python -m nbsite build --org holoviz --project-name param - name: Deploy dev uses: peaceiris/actions-gh-pages@v3 if: | (github.event_name == 'workflow_dispatch' && github.event.inputs.target == 'dev') || (github.event_name == 'push' && (contains(steps.vars.outputs.tag, 'a') || contains(steps.vars.outputs.tag, 'b') || contains(steps.vars.outputs.tag, 'rc'))) with: personal_token: ${{ secrets.ACCESS_TOKEN }} external_repository: pyviz-dev/param publish_dir: ./builtdocs force_orphan: true - name: Deploy main if: | (github.event_name == 'workflow_dispatch' && github.event.inputs.target == 'main') || (github.event_name == 'push' && !(contains(steps.vars.outputs.tag, 'a') || contains(steps.vars.outputs.tag, 'b') || contains(steps.vars.outputs.tag, 'rc'))) uses: peaceiris/actions-gh-pages@v3 with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: ./builtdocs cname: param.holoviz.org force_orphan: true param-1.12.3/.github/workflows/downstream_tests.yaml000066400000000000000000000012251434441564000225710ustar00rootroot00000000000000name: downstream_tests on: # Run this workflow after the build workflow has completed. workflow_run: workflows: [packages] types: [completed] # Or by triggering it manually via Github's UI workflow_dispatch: inputs: manual: description: don't change me! type: boolean required: true default: true jobs: downstream_tests: uses: pyviz-dev/holoviz_tasks/.github/workflows/run_downstream_tests.yaml@main with: downstream_repos_as_json: "{\"downstream_repo\":[\"panel\", \"lumen\", \"holoviews\", \"datashader\", \"colorcet\"]}" secrets: ACCESS_TOKEN: ${{ secrets.ACCESS_TOKEN }} param-1.12.3/.github/workflows/test.yaml000066400000000000000000000072671434441564000201570ustar00rootroot00000000000000name: tests on: push: branches: - master pull_request: branches: - '*' workflow_dispatch: schedule: - cron: '0 13 * * SUN' jobs: test_suite: name: Tox on ${{ matrix.python-version }}, ${{ matrix.platform }} runs-on: ${{ matrix.platform }} strategy: fail-fast: false matrix: platform: ['ubuntu-latest', 'windows-latest', 'macos-latest'] python-version: ['3.6', '3.7', '3.8', '3.9', '3.10', '3.11', 'pypy-3.7'] exclude: # Started failing with `Error: The version '3.6' with architecture 'x64' was not found for Ubuntu 22.04.`` - platform: ubuntu-latest python-version: '3.6' timeout-minutes: 30 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - uses: actions/checkout@v3 with: fetch-depth: "100" - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - name: env setup run: | python -m pip install --upgrade setuptools pip wheel python -m pip install "tox<4" tox-gh-actions - name: lint run: tox -e flakes - name: unit run: tox - name: unit with_ipython run: tox -e with_ipython - name: unit with_numpy if: (!startsWith(matrix.python-version, 'py')) run: tox -e with_numpy - name: unit with_pandas if: (!startsWith(matrix.python-version, 'py')) run: tox -e with_pandas - name: unit with_jsonschema run: tox -e with_jsonschema - name: unit with_gmpy if: contains(matrix.platform, 'ubuntu') && !startsWith(matrix.python-version, 'py') && matrix.python-version != '3.11' run: tox -e with_gmpy - name: unit all_deps if: contains(matrix.platform, 'ubuntu') && !startsWith(matrix.python-version, 'py') && matrix.python-version != '3.11' run: tox -e with_all - uses: codecov/codecov-action@v3 if: github.event_name == 'push' test_suite27: name: Tox on ${{ matrix.python-version }}, ${{ matrix.platform }} runs-on: ${{ matrix.platform }} strategy: fail-fast: false matrix: platform: ['ubuntu-latest', 'windows-latest', 'macos-latest'] python-version: ['2.7'] timeout-minutes: 30 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - uses: actions/checkout@v3 with: fetch-depth: "100" - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - name: env setup run: | python -m pip install --upgrade setuptools pip wheel python -m pip install "tox<4" tox-gh-actions - name: lint run: tox -e flakes - name: unit run: tox -c tox27.ini - name: unit with_ipython run: tox -c tox27.ini -e with_ipython - name: unit with_numpy if: (!startsWith(matrix.python-version, 'py')) run: tox -c tox27.ini -e with_numpy - name: unit with_pandas if: (!startsWith(matrix.python-version, 'py')) run: tox -c tox27.ini -e with_pandas - name: unit with_jsonschema run: tox -c tox27.ini -e with_jsonschema - name: unit with_gmpy if: contains(matrix.platform, 'ubuntu') && !startsWith(matrix.python-version, 'py') run: tox -c tox27.ini -e with_gmpy - name: unit all_deps if: contains(matrix.platform, 'ubuntu') && !startsWith(matrix.python-version, 'py') run: tox -c tox27.ini -e with_all - uses: codecov/codecov-action@v3 if: github.event_name == 'push' param-1.12.3/.gitignore000066400000000000000000000004261434441564000146750ustar00rootroot00000000000000*.py[cod] #*# *~ *.egg *.egg-info *.swp *.DS_Store *.so *.o *.out *.lock .ipynb_checkpoints .vscode .tox data.pickle # Docs builtdocs/ jupyter_execute/ doc/user_guide/*.ipynb doc/Reference_Manual/ # Unit test / Coverage report .coverage coverage.xml # autover param/.version param-1.12.3/.gitmodules000066400000000000000000000000001434441564000150460ustar00rootroot00000000000000param-1.12.3/LICENSE.txt000066400000000000000000000027421434441564000145330ustar00rootroot00000000000000Copyright (c) 2005-2022, HoloViz team. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the copyright holder nor the names of any contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. param-1.12.3/MANIFEST.in000066400000000000000000000002151434441564000144370ustar00rootroot00000000000000include README.rst include LICENSE.txt include setup.py include param/.version recursive-include param *.py recursive-include numbergen *.py param-1.12.3/README.md000066400000000000000000000053071434441564000141670ustar00rootroot00000000000000 | | | | --- | --- | | Build Status | [![Linux/MacOS/Windows Build Status](https://github.com/holoviz/param/workflows/pytest/badge.svg)](https://github.com/holoviz/param/actions/workflows/test.yml) | Coverage | [![codecov](https://codecov.io/gh/holoviz/param/branch/master/graph/badge.svg)](https://codecov.io/gh/holoviz/param) || | Latest dev release | [![Github tag](https://img.shields.io/github/v/tag/holoviz/param.svg?label=tag&colorB=11ccbb)](https://github.com/holoviz/param/tags) [![dev-site](https://img.shields.io/website-up-down-green-red/https/pyviz-dev.github.io/param.svg?label=dev%20website)](https://pyviz-dev.github.io/param/) | | Latest release | [![Github release](https://img.shields.io/github/release/holoviz/param.svg?label=tag&colorB=11ccbb)](https://github.com/holoviz/param/releases) [![PyPI version](https://img.shields.io/pypi/v/param.svg?colorB=cc77dd)](https://pypi.python.org/pypi/param) [![param version](https://img.shields.io/conda/v/pyviz/param.svg?colorB=4488ff&style=flat)](https://anaconda.org/pyviz/param) [![conda-forge version](https://img.shields.io/conda/v/conda-forge/param.svg?label=conda%7Cconda-forge&colorB=4488ff)](https://anaconda.org/conda-forge/param) [![defaults version](https://img.shields.io/conda/v/anaconda/param.svg?label=conda%7Cdefaults&style=flat&colorB=4488ff)](https://anaconda.org/anaconda/param) | | Python | [![Python support](https://img.shields.io/pypi/pyversions/param.svg)](https://pypi.org/project/param/) | Docs | [![gh-pages](https://img.shields.io/github/last-commit/holoviz/param/gh-pages.svg)](https://github.com/holoviz/param/tree/gh-pages) [![site](https://img.shields.io/website-up-down-green-red/https/param.holoviz.org.svg)](https://param.holoviz.org) | | Binder | [![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/holoviz/param/master?labpath=examples) | | Support | [![Discourse](https://img.shields.io/discourse/status?server=https%3A%2F%2Fdiscourse.holoviz.org)](https://discourse.holoviz.org/) | Param is a library providing Parameters: Python attributes 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. Param contains only two required Python files, with no external dependencies, and is provided freely for both non-commercial and commercial use under a BSD license, so that it can easily be included as part of other projects. Please see [param's website](https://param.holoviz.org) for official releases, installation instructions, documentation, and examples. param-1.12.3/binder/000077500000000000000000000000001434441564000141465ustar00rootroot00000000000000param-1.12.3/binder/environment.yml000066400000000000000000000001671434441564000172410ustar00rootroot00000000000000name: param channels: - pyviz - defaults dependencies: - python=3.9.7 - aiohttp=3.7.4 - panel=0.12.4 - pip param-1.12.3/binder/postBuild000066400000000000000000000000501434441564000160310ustar00rootroot00000000000000pip uninstall param --yes pip install . param-1.12.3/conda.recipe/000077500000000000000000000000001434441564000152355ustar00rootroot00000000000000param-1.12.3/conda.recipe/meta.yaml000066400000000000000000000013441434441564000170510ustar00rootroot00000000000000{% set sdata = load_setup_py_data() %} package: name: param version: {{ sdata['version'] }} source: path: .. build: noarch: python script: python setup.py install --single-version-externally-managed --record=record.txt requirements: build: - python - setuptools run: - python {{ sdata['python_requires'] }} test: requires: {% for dep in sdata['extras_require']['tests'] %} - {{ dep }} {% endfor %} source_files: # for nose config - setup.cfg - tests imports: - param - numbergen commands: # https://github.com/holoviz/param/issues/219 - pytest tests about: home: {{ sdata['url'] }} summary: {{ sdata['description'] }} license: {{ sdata['license'] }} param-1.12.3/doc/000077500000000000000000000000001434441564000134505ustar00rootroot00000000000000param-1.12.3/doc/_static/000077500000000000000000000000001434441564000150765ustar00rootroot00000000000000param-1.12.3/doc/_static/favicon.ico000066400000000000000000000353561434441564000172330ustar00rootroot00000000000000 h6  00 %F(  v4i{0?Yqtrrrrrrj'J^:Cwww|PLmwwwwwwwwwvwwwwwwwwwKww̸wjwwww*wwݸw۸w۸w۸w۸w۸w۸wwwwwݸwԸwbw/wɸwwwwwwwwwwwwwwmwww)w(w(w(w(w(w'wPww˸wmw(w#wwwxd)U>[P;^tODxwwwwwwww     J ^    .'x7,q     J `     d a a a a a a Y  >                 w              9     v  5         ( @  0>)s,0/83/0000000000000%"s f5^0Zua ]B wwwwttttwwwwwwwwwwwwwwwwwwwwwwww xwwwuuuwwwwwwwwwwwwwwwwwwwwwwwwŸwwNwwwwwwwt wwwwwwwwwwwwwwwwww1wwwwwwwbwwwwwwwwww̸wҸwѸwѸwѸwѸwѸwѸwѸwѸwѸwѸwѸwѸwиw۸wwwwwwwwѸwѸwҸwwWwwwZwwwwwwwwwwwwwwwwwwwwwwwwwwwww̸w-wwbwwwwwwwwwwwwwwwwwwwwwwwwwwwwwոw2wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwٸwtw wxww0w5w4w4w4w4w4w4w4w4w4w4w4w4w3wBwwwwwwwkw3w4w5w(wwwwwwwwwwwwwwwwwwwwwwwwwwŸwwNwwwwwwywwww        wwwwwwww xwwwwww           wwwvwwwwwwww                wwwwvvvv            ]    B                           r      ]                     g                            D   U                              *  k                              8  -                                  @ F E K        F E F F F F F F F F F F F F F 8               t                             0 > )                    8 ! (0` $Zb!9>hh..ww7UUTTSwSTTTTTTTTTTTTTTTTTTTTUV7mbm -?4wwwwwrtvvwwww wxwwwww~w.wwwƸww}w(wwwwv wwwxxxxxxxxxxxxxxxxxxxxwwwuwwwwwwwڸwQwxxxwwwn uwwwwwwxxxxxxxxxxxxxxxxxxxxxwswwwwwwwwwwExxxwwwwwwwxw>wwոwӸwӸwӸwӸwӸwӸwӸwӸwӸwӸwӸwӸwӸwӸwӸwӸwӸwӸwӸwӸwӸwӸwwwwwwwwwwwwҸwӸwӸwӸwԸww>{www=wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww=wvwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwxwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwxwwUwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwTwwwwxwܸwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwܸwxwwwwvww7w7w6w6w6w6w6w6w6w6w6w6w6w6w6w6w6w6w6w6w6w6w4w}wwwwwwwwwwXw4w6w6w7w7wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwvwwwwwwwڸwQvwwwwwwwwww w.wwwǸww~w(wwwvvwwww wwwwwwwwwwvv              - ? 4        a      l             m                                        * * * * ( _           ) * * * * * * * * * * * * * * * * * * * * + +       v                                          v    a                                            a                                                {                                              y  D                                            D    @                                          A                      B                                    "        9                               [    b                 37 param-1.12.3/doc/_static/icon.png000066400000000000000000000122441434441564000165370ustar00rootroot00000000000000PNG  IHDRȁY pHYs篶tEXtSoftwarewww.inkscape.org<1IDATxy\ekuw:]]NX$,#茇0dNY[9Nt1YT<= d\Fh$!AT@= DtwuWt?B4`jU.ߓ~w֭DDDDDDDDDDv|.^iuJ($"jDqBKK{}`'qlDdDop>'h"v"LDSm1j-;yI1;3YBaVNŞE+(_8Č?ֶE'(_iM 8g[, "JGN,i.K䔨H,ⲁ*N&"yv@wq 9B>A7m2tbdx;-A@l&"oQPXŸAt~ >RyPvJꋔ cJJrE湈MII6h  l]%H́ꏪUT%z*PVr`۵e`Ğb* } |CK!C?hNsGz*Pms [!ĶԜrbs)0Lϛcb]02vJLŪl (:^wt0 r^HoLt;KD {q;凂|-ۼ(t9Ҧx%sd`qwd# >>v/[D9Rm.IAS-z ΒEOAC:+$~搼 BF@~!Tvq;寂"A,oʧ ns;巢jENn}ڑA1%dՌxA-Fe3Kr<(C_wMh} .P!R#͋Ã/`,*"E]ijtB?(v*̪=?u(YW\zoQ:ϒ ~|Ru~I"( 4\c!g>OM7t@D)p2|I)1[^:} KDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD캦5Kꇅ164?dMGΘXDrL]?:I׿hs !6}ED~lWjr8O*}3ҹPLP ^䙗eF|,¬ j 5=RX*r/`NF*Q]Z3\\J nta"h(2Zo0~VUԺCՖ5v3XD ߰^%nG9GHY3nq H+ r;yD M} NBj l ,"W` 渝#RU<{7KS+uw`9@GݵvI3"Iݲ1jǶΒ.u[|=@+ݎ^6f!ҕipŎcM5ƺZ`.ŀ\ @d؉] \.q;G*=VB$h@Fx eW,szWhͬLFH+wOT<Dn*Oe;tNrx>1T͡3rXJE ܵI!06pD2SH7k+vޮ'%R裁?I͈uw}Pv4"UES혰_xB-ݏK|>\Z5D^/%$JWжv;#~j5~QZ\ӽ`ψ,XBLFC"*pyIY,b]Ry;1f+țl.-fE0!;qI\5fUO9HX]ىCMk["Z> @DqRb|e@";u@d%n|4Ƹ_vy=n-c}gVT&0>oE r";t]ϚO_N"~g&,bmre-IF>RIYӽ頨|Qb=mS)V C [-!UM/ۧ^n0r>6FRB Ǯ9E3f2Q?(d;v{G9#wkxz(SA گIIUe.n'ɄsO Ԩ(9 t(IXց𪦗3Y݊wCd92SК)1W|-Y?oΑ.UKt9B|@vt&,u;G*q|Nދ`鯄ۚ>s$i@Eftq A 1*Й=mv$S)pe |$1 PB3"%2v"y˲df2vCpE ,R {hѠ;mc-_;w9/XDJ'UO=.R߽M{r<"RfZ=U3RkBg(G!UN85PI\q pXWT^7 yEӼ+PRaƋJ mFD=}}44'7,Zz5.bݺȎpJѭ>n|.ݓ*bKK, #^Z=~}Ѱg'XBTiV޸1ὔEO2W9s䝣ݒpx6kjE=%>"WqgQo` 2Um#/M|8DǼ4 \ ۇ"c{FԒ9Eb>^8D%q:j9@{Fӗ4D%jzcmSDJ"RShg% G{W40 ca&Qn5<Է3E<יWYS2 Ly U ? u \2"U:_Yŕ(K0ŠU ua񈈈GSMIENDB`param-1.12.3/doc/_static/icon.svg000066400000000000000000000304431434441564000165530ustar00rootroot00000000000000 image/svg+xml param-1.12.3/doc/_static/logo.png000066400000000000000000000353701434441564000165540ustar00rootroot00000000000000PNG  IHDR4LE@3 iCCPICC ProfileHTǿ퍶aw"ʲ K]`bG(""PcAD$(1X zr{Ϲ7R4+)) ug2p Ā2dQ;ګ}JF8N@av?bFj*!L#"nkz/-b9_e" bs:#EkqbxEؚB#JHH\e["ևdڳ|(31>`:0>+5?[r$/oun$$x$<#$3 ŀ pR#3SW0cѩ ;d#<@Oobm7/+AՒ2Cܿj,E:@]i A@@("k`,-pn  A4H@n pJpԃ,hep `<` `,C( eH2 k|(B<( " N2tC,`2Le`X6`wsd8 ΁åp5| n/÷x~/Q(3 EE>>|}Q65-? P H  l\ r*  |+D2$&#Zq SaaaU7fnIrS 76 DŽ7dyY l&ǖS̙,*ZqqgmKb1e1b]b+cV[ <^7Q613q(I3)7i<"p<ߝ_lLH!?4&ҭefeoزotSַ[[[{o۽mbЎ=;w弫~7qw ӕ#+g+篚rrc{-V~:}ɻ_]poJYФށу6닄&yj+f=##iGK=J;*=pcYtH}yKtžccm7WTW~8s^sU[JuIjk5V6Sn޷Q nJk=vjfzKp&¿=~ٹhAm[ۣ;B:::{,Z  //\\ui;{2d枇W~5kW.]~ōΛf7oj7oۦ; ]|εwo 766~so~W,?('ORed„DS'ٓ/~NT3ʳiN7~⳾XpT aUkw^u_d@-@7[ pHYs%%IR$iTXtXML:com.adobe.xmp 332 308 1 0ɾ.vIDATxUյםޘ a1 {K%bM, 0҆:La)G)}-{sv}w}^;e#@R*A и -4DE@R ꪫ@>PJ5唔L9ٱ#wH~] jSi#m 6{\0ʶ~K̬4jWPH]GFQ3΢u=NmA?7A M3/v舔,iOPH#ArUB"*YW߮7 wFUG>nAdrrEЁ͛L}'LOsR}=ZA#_^5>q[P^eO> /ǎ~'Ѧg8 _kop#i DlL/=(m`:DIzV;l۳{HyfѾiYp8s4]oy4;<,Y'O=G9ݎy5 :Kѐ\^>j/ʃBY4\8~JCnRN< KT0O‘ 3)#,yQcZܙ&u6Bi &Asv.ZH3nJ7nRb>l:G)CNQ c4:AZ_hԿR]U+%lӽ|Qy>/@м`۶ҢGҊ矦j0.2A_E)-OA ` hpX5O]tZ4L2;ƖG}Ϟ@\B=Og,@QD0Mf'TVҎsi:*ݼXmPSQAmS@ #N0I[!wE h@# `"4E  h3F @F6 9("ASـO>c Mhd >"4E  h3F @F6 9("ASـO>c Mhd >"4E  h3F (ـTҎUTqk)35rғ[^ekX8@G"@}}fmG_lKy2SQs_SQ\:O;ۯ WX\vjf?/EͧkF9iG?D& ))fWP4jHIˇw;OA C_3㳷P_fe)t'H$V6H4 " n=@׽TeE57CY9^8dWҡ:zeUrq4{^syY'6]6gd%ӳ?Bvt0U$-[X$2LM7F#{kP|V (n}b&8XSOc!UaR8q4Ǒ"A ;H2Gd8QFY}p Ή䐆 h@DIJ_ZJkF+wo>Xc#$4nxN&σ[H`MHou{;KTJ[VRY5.'b)}%1U8? />F̣I;kMCiS>3w(>[/eʾpG"\ry%AQ~4 n ,jW[ЖM8Z*Shf֪uXS ?^" 5l7]3)rۄ/v}⋗z=v+37 > /Rgo[똝FnEy-.z M'v˥ɧup13xGrV! rGt |R4IC{ +v [HfNQO[^Mv8m嫏TR<|3ݟ6g[~0Mw9=ŏ̭~.f-;ȥE虩`~A0>\KII2SFP2Ff y[]oߺפAG}S/D:כ/y6$ d[vԙiѷ]_C'n(& m TnOӖleLMn<ȼt~,` 5A`MI9޴V( +i{Y5UTRum=et6)Ӿ8q.اXp @Ђ;@ [w(9@Z.Zp}[w(9@Z.Zp}[w(9@Z.Zp}[w(9@Z.Zp}[w(9@Z.%J*Siu%UTQr$2CF.e`"@]ɦE4{*Zcmܿ6U[*lߍF =(#%-@EQA<d3jhYOTNZ&M(N{ o) ~'ADȞ^=4o:m+h)wOwchH <&AMeer'=M}&Ntp,M$^yI?.Zi(TX^z6=xƍtN *C *xOxc?;ϧ)'_NHĹD(&AS E5{mcedhzɔ,OdNZ'iZHk2_3?f8Akky,K+?z1Ox !AUw|'ȮMg7 yϧ'|Qg;yT 0:X[Mw~9'循r?G.gdg[>w׾Gϧ-PyN#z9k\ht1E̵Đc~>zoo1g0ւ&bv[WG?Xr͜<`~"pf3󞅨8E3S)fQ 51M/B@ڮaa lU$o K[5RnPoyt,`cqBҖMÈ4̦W~XQCy'mY4LSAyf-!2.ݔPd Iv"6Բ&f[$'[r I!X-MV(hc=4COڴ&VQS' x$؈íNWMZ#kk)h٩Yh8+JֶFhԴ4Yܢʳt6CDI&sGuD}3u98JC?~=w'(d\?FI|#a AFSMVϤCuD蔕GM|E ZXR+Zs%иC'Ăj: A A bʸz*Zc-߷6P)_VTQrRefP{۶ F# RFJZL*x $A _Q+ВFt;* $A _Q+ВMK Np- @дt;* $A _Q+ВMK Np- @дt;* $A _Q+ВMK Np- @дt;* $A _Q+ВMK Np- @дt;* $A _Q+ВMK Np- @дt;* $A _Q+ВMK Np- @дt;* $A _Q+ВMK Np- @дt;* $A _Q+ВMK Np- @дt;* $A _Q+ВMK Np- @дt;* $A _Q+ВMK Np- @дt;* $A _Q+ВMK Np- @дt;* $A _Q+ВMK Np- @дt;* $A _Q+ВMK Np- @дt;* $A _Q+ВMK Np- @дt;* $A _Q+ВMK Np- @дt;* $ZhEGҒ(;=R v 4}i<-=8DeUTV]KU͖559Bi)▝&:M)>*Mͦ@@ =h"^YE⵵;;Z?Kt%Љ& :l$ 3|( Mx(]P -ḥ)g)b 5ltA{ mgM{GU/6nJhGT@ N4ᱹo\@ ZA^FgƳD-A@o4qSC i4 zAUM.&jkTBO@ A/UAd} 4q|V(ߨ;VV[\jr 3[7[.]:4eR^F e&SzJJzhN`+h8&XVs짒jn+'wO%&Ă+We[S3N|U_;G=|`EDVX5=tq 'Q7l;@sؑ˶e]njq0>S딓f|Nߑ.1GkDf-rwWXjSvJL{38]_/7%[/c*Q/=w6eI*چFnKW Nj˹}K;b͕97Ҍׁ.ҕvn# R&qpwqܶi]vÔD۸jDw녃+Rncζޔ3 nnHE}WuNNIמ؃e\϶63\E̸Άx%Zt:ß[LOk1f>wtGyLoWCRZc'\b;k]Ģv۩}H{}6zfxSX[vttVNo@kr, _OjrYe&{|AIwy/1Nc?_X_YBf{q2DҒ3)ӿ6cQMku/~r>}W ^VhrxwR#ERj3?NOg$) 0.Ý~Sk!sm2`沨"[H.~ZeS6s*S$%2 j2~-(.qM;iɥ>4vqʃ7qEb+W=c86c joo?D{hK6hdDN iCs-n^\~䙅cuQ\_GOui Q2i׿^fF_cy+hrĎ*KBc=W." M1LvIoFz˯Aa4CMbQu1ar^GIBaq؎VdB3%h>CSM@cyZ[4D/.J2ͪVоڰjI| NV s? ~ޛ[Nq0C@|1?)h-Nm̍ dbgQ/9p9:'""0g ۑ } 7lg=49Se@MYGú9^^ VXKq\grGwdr t1n%A%qB7䪜uN +Md '[ HVi\GRLtr5fBTY*r|k ,}뚬?ĀV؁獌oc2 Ld;M|LcXq iDGu턁GZ\=e܁l4Ce|S[%<=fǁ*2cAlH'I&7gV\t)??nitL}rzaV[_FY?X݉żLYQl-ue~:ҷ= k.+&Y,?|m v#ovbg4.<&61Ys(*%r@͈KzՈ__Aoژ)-=>Ղv1Gџ/$>-kb,_͗hs9ȦH\:!qb冄ztᏺ/,NWI 7uAG,?sy =7jRG]kLZvZ qɽlwWtn.f&~m&pc/hNG=D'!p/;ZL=:C;a0{]$z^fC[BVo VҢR1Ü(+ҰO@/l/xP@ןؓN0E[KoǸZ;Ld_dfv-og1Q>$-)ڌ1}m+bߊ˘Zvbin-/nj֗ߙVOyL`ncHQI;Y1YFr*Ή!`'#{@bXu+A$ TbUD&X&AlDkuA'`,9d h1Od)߿\/}Ƌt{0 @b/xeSQZ9dc@_vM;_iKvĥf `D0} @|o ukEXN9֧\Ȟ?ʰǂ)a:ͣypCsIFs mγCwC˧uF+BqNN5O}]l-d pUC)@5VF./\9~stV.8'hHӯ #0%Mڋs4ApT;c]2+Oܵ1␁]Л 7fleB|Eـe1P `bfЖC<3?rj_]ךݼvD*/.9$;{>9o,5DZ+ ݡ>XVlw7txie@/sO/@$"4j_SRAx(1J`aqs#S#FO/džy[;KYӭ-?I=y5O@6-5k<5]9M-Dl,Q]c|2o,{Ɨ#AKTQSGUHM/%/RDQ0%dlG]k/+B`#A OQ#Ж~u=*#A OQ#ЖM[ף >5m @дu=*#A OQ#Ж0 IENDB`param-1.12.3/doc/_static/logo_horizontal.png000066400000000000000000000215441434441564000210230ustar00rootroot00000000000000PNG  IHDRuk pHYs篶tEXtSoftwarewww.inkscape.org< IDATx{|uyfIsI+-U@b./VUU' kH mz+UX]W]WC#ܱ mI>=?RiyfIrޯW t:sy{c1c1c~vY[QxTN"MzUkk2܌1ƌN9Qo;KEBCgҵ=1ƌM^+/Ս.rDV1ƌUY/*/ZPD [){x]|ccCz["="2gq_gp c1P X_ %wexc1 ||ٞz0IT/}8ݎH`Q4"e@wƃ"PBd`49kCW$ag1#a *㲕 TwFKښNcrـ1^p We9M{1gʋ[1MX}w҄ʰ31Ƙ\uPA|ׇɨ{SzkIcL:Q#{]%v=cЯ TT#\V2&㼔&$1&+qPR.& Dy9cL.W#*Z'h"C*b1cB*=; D`ܤʊfJDwdTtlz9N94K[ t_33fULAhV՗tlbaTL a0{@=YNYz[ńP?KD\leMQIk[ہ[WUc+kqmYYYK=dt vqc4 ;}ae'Sƌ C̄kZZZP a~^nY>ySyY+;t3Wf E'VUi׬v2ƘjAX񔈞<XPគ 7v,1=xu͸6fzAXر#5BT /,nlT)͘qoʫ;cLB)Kߤo hNo?+Mn M:tYa'b VfɿMg}Y3b4ЦK94) o5YG+ჍpcLr g4}{N_hLfŎ)fƘ̳h|#x$<1YA4J,;cv dLzfQD$t]8O4Z 3<"3P=]T^O|mf ;cHyЅE *V*HtAIL(B=& =ةyyїvrѮr 7V)O)Eopu d${ IJe:鉆q{{sG;dTjT~tҍH*;=OwyN_k,J6 R<`I˫:W╋h*oҘj Һ/{c[bVF{V޸iRJ{!Mu8K;#"oڎŁTfMv Q^r+H]WdP!5޲f>k© 3Q/"㘎0<v;jgsfu3"Qw_cc Ldp(;#]/hZ=Pβk87dkp~~B}n>=%+zjZdN@9}SJ`0¾ 8GSԽ{\<{"vwA|,].ӆsfVMtAvzOE/mY1チ+iTR Ep 7c3`C'﷮, hyWU UBs xxTWˊxĆ>X9Py3eLNy3QLx a{@j?*+| L*h{C9cbdLD/A9{{lYOP<1|r ?uiںzk vci8]r!pPqD`wsIU]?@~UƖ~H#O%<6b?? $k3tK2YtpWOHufL+n1D,GrGY/[cU7p#>RVU:V]2i}e0!MzvTncM ?Dd3́b}aU&vciv~Tg-:^U΋bյ=ң 1͵:Qy$`)[|Dn[p |ŒSz-µOq)c]q.r-9a1¹Xu7n'dhᔃ8V]w'CN&Yp]}e<NR\(E"ҡG=ǬCB};<Ʃn$N^#Un\9{X͵DsN]ǪRJWem^1ɐ|{5sX; `baBogy)z& 4kTtbuE*~˿j /s . rB@GR[]*[gԴ%k>69-Uͪ9꺎dN&s4bGt~I^ߴS~$ڥmަY nLUVKAXUM,~ өCS`0~n@[cd))h΁0U'Ș \VVUwVdYqY𧒗 ϱӥ6…1Iqd78\uDǫGG++aCPl2Di~d'sD"dY]pV!V]{ADyJ@ yzlYGgD@w٪h. aj.._sǖ~1,ƈˆ Q솇ʎࡩK, 4; U/Ev~Bn$(z#_z:g/:2^_w8 @a'^]wj;= BpÚ{K/藧$6dwVIހG{V-x ȕ5+kZvcxuo `-\"d~"IϕEģZ9@y q 5CRtpD@@@tWU{Eyg0n1*.m>wKuuT yl"-ۆMR*z:nf-zNA VEwG%*Jxu88 ﹖ |Ex.I-les!XgD`Η _g+̲ڤ|-'uӳc񦍨^?Z|톊&g`rLHThٲڏƪ~"">?xÕ]?g,j\9E|or JN+~]_˹aLl^I/$6ypT1̲fʖ4]ʪ.Uuv'ESdPcsˉhFWZEO(1y{ß="aZn/uNL,y־ |' r\S}]DpDy@0펲EDCݥ}=JAR~_u2 $k<{#Qe46yw~5~2jP9eGbuoݐy^t]o:u>z+'[8IN4 }wS=Va% #?KM-iTqz|Da.]*?eE H QB[K0O sƨ 4A$pZ]S~#,TZV] Ѿt^|Y8uȩiҷ)F1ΡU߾FTWQlgHD?4TN_x8_0CPU_K*,{#76v^M r-{L8ha~:ջ<mxlr7ܹ}$PZj  ںz.q y|O2PZY=ň |eѣNYȰ bcco:mq蠂wō>9FfD{Zjfv#\\ռb䚺x4_ih[A%^'T:p;@g,e"ᨮ_.M+n*#v=,&{5Jۜz [kfvCJ$s |UQ;򪺳RN@^N7|΀NOCvhY9lI ^Aϵ kcY𐨫i^9z :VptE~L@0,\SU7 ޙmO">wN|6(SōuGt|9F<5H#W.m廂d(OT*z>MGhJK@NE׉OYuLZrLO4kKeRȁz& R-sw׼|#6VU7'+ [ږXcP[svNG;U:+Q+mS1N \gT#ȧfU*;~kv'\W_V  +f A˚7^N6`xl):3( {Gv(?T9:Șfhb^vf䰂8D|_ 0t* 599۾uP^OiZ^xٲڏ3ٔĆN^y 0\qc9"@.-o]=b: =ѻO$@ڑVO4-m\SڹT1Cnåؚ~::(  8dLx&ì S-swH\'T105Cy`Īk/0dާ㨾ȫE"ѯƞ2':ݩ QQ|#7|VD~מ nmBqI@q@f`&H\AcjdL  WvWVӁ7"1+<@XA1 ͵:ہۋPD&E٣[vrѮs68hsYVgH-ɚ7Ȍ VwōMEع,KϧSOFThd͜L_ 69f̵x1&S䖭+gD>cSw챂hL@JeabwCl`1#Dc"*l o1ZdlhL@Tn?_6bhL@4ÜO2;m^1 дw9aKع5eߞZדsD-CpW,ܝrz]\ּ+W"Y3;7mK49i`25%o3:{If4IXr7eEMET.$T<4.T*<ιh{YMƥO/36}-* h68%Mdsqoag YX7=pWkgN?;Hw˞U ;1zN/4E#,r* Nmu$; ޓq#Oh]qѫ ŻzsnBvO(c3*[D7A9> Nuz0lK= TB (/"lAe ڒLNʝg o<33._)<2WnXUQyè!ټt2-\)9dFDqx'qBQ @xJ@B7MU[EQDt7iy}u9IDAT 7<81ɉ"'h"=) q٫IsoNv{x6Jb-QoO- T 7-ck1f^QIUTX=c@lH̫H $1&+'b -G5qa9cL.SBdv3~ѩ+*t1~Qm!aDcrQ\&seis0Ƙ\4&غ.i5OicL.: 6m7r1իe. image/svg+xml Param param-1.12.3/doc/_static/logo_horizontal_white.png000066400000000000000000000211131434441564000222130ustar00rootroot00000000000000PNG  IHDRuk pHYs篶tEXtSoftwarewww.inkscape.org< IDATxy|\u?=6ML"b+B e E~¥e)K " E-` mɜ3I&ҤIs̙dUәg9RJ)RJ)RJI 溝j,g[1@asslܹ)* .)5'[9D0O]KÎ-ݿ= ʚRJ +ւx ]("l%__CXRJU yB~p ]E^J)ceM BCìߟh]yq'RlȂ e9vD)*ՐLb8Ȼ [Ӓ-qgRjxcӔC$dTI?q'Rj3D/B8U;)RCت .inv!p\ɨ3 %$RmU'g/cE;D[DRu*.]ER*lUa{\ J $RV1!/)5 ag;YbU$(UAT͖gU(0ApOI(T%vE/OEzĝRJUm ]+)8Q%4$RR yK,kʝ*ՎqN+q'N5 ftF2mILHLc2@@qD0=$tc 5YJ։Jqcs4nl;%-S>*Z=<Ӌ-ڽ GD!9 ]:%EDo(F7iL\qp+0BY 4AiiK8zaL0O|vrσg'lqq0 I> 7}}}hiiǝR*e۳m c=.Xjx5Ok[EoNF)U^C*` qP^+@k﹵㨉I>yS/ YR*^e{__dK8_3$.JD|>yŝRway 1r,ʐ<sܷHN;Tr=O OETpG.li,R`D}}q[#ॳ0%Sw D.1p~GzI5#yL:~2DRъ ~ B>OCt/[&62?5v @Z{t&y&DRщ l<0+@#%}tlV+.8ח~R*S3 "<"ĝR*<]celB)UzZ W:::Rq硔 O RL4iaI({dH"rtzM^cL1f%4!9Y$"'95C-wJTꪱd"5 4;{w4QVJ^2U d2gRRuȩ֕`QTJ!!"p V~@El^T;/^IYpͧ7=."; -jOc$NRc~4\@q<A(< q?QC BJꁞDH@P-6?~7ɔ'iɄy# C< ~ܖեRC 8JD^ %gKKT*`A "sIXwGጯT<@T*bc.;sɏQ."bQLD}HFPT*uck3̚R ~$O@oֆT*5m/\8I1峥[ x<_16^'Q `_r ,Z-D(\~?ߖr}PbSHO\̑QU5wmuD2 ?\<3.(ɧ{oojTX8\.7?\JPN>y"`D䇞}䰓IuiĒhI&"D(W* ΐaDd5YQŃc\{,1JND*ӧO} ;Ǖ0EW]q'S"HBZSb?"jAG6~P7+`$@+EC;]/sU2~hYmmELʾAh\|M}k0JsI\#5\ ,orSUH0a7|]]xQ $(ӧoedy{ƝH5 yyAqR-߭qS\O%OA)eUT\-RtPgbYqi?b1\.D8-cа#)6O2T$Ԏj(fVɠ=k;Da|$D-q5|Ym Y(4,^q\qȢLXg&ٸZP?p @rڴڝu_ +{ԕhw5.^ow(^@D7{x\|ymmm] yL&B ݞ]<;-$8s_ ւ;(VO@띕xBƈ½EAq]7%((ެqqDqP-Hf3(]d/+08 @?^صdIo}zRpQL`uL2$| _ekz6mTÂEOD^1u_}߿(eu$}Y"21tJD\`d]Q-?=UCE:EbB-$"?.8f]Uz࿆- L-N@@:j(:S@ Nc RK?;.ay E$˥ rDWP46~ƘU$WvSl gcv#)c0FD$_dڷD$G28N=Iv_ ^Ě^O*H־E$7' ='9s_Gk4_|ZFlP[[;q]H30LQxUDy؂k< ìcrCG7mDg/?kɱǝLrk# ٖJ^Hm<00L&( N5$o}.@䩾2-S3@g{<!"r1zm~^K̽rEvD3sp >׷?mkkW\Dn'yk:w@ ]]]s1pTDdNnh18gt.x)O~ E^DёNxC"6*l<P.՗=uhrJ+< 'r&D+b/rYR1I>L!R&YCDa °fz׼ͤ9Q&&RLT.v}_FoL\]h* FUJR/HM} Rԗ6p;y'}ϒ|DD>ҴjO&QEƘQ}QS\׽v/ߎDDfU|%X\H(r VLw($bCtBD>C=f5"FХiÄHD2($={c"";ER^(ȹ;Vꆈ0:Z9_FVnUW&Np"qԩ$HӜ1W" 6SAۖ8b b ,$L&r+բ/uaVJ KvΨ&: "Rǁdw&91 LDfnVS@^fI cchyTd-c 䮮X_L HVL;'?a0=lpUqg0AcNkiiɏԱf83d"!<| 2L)A\ic̻D#ɔ6iItE Ƙ6o1uT$C!H1q]wyP~ҤIuQYflnuDEa)Sx_ILpn2s$BaK}¦.PO:8Ah;' 9xn6AWW{Ȼ`p֖bc2'O ^?kT/עow|;qA,J$j>6+"/ DV.;'Hg51ag͡f]<MC _X`ԶzBh.9<| =~>2Hfw}Qz+"$\.e-WC1Ha |6o»I+7+!FM(}K ?ڑJs3HeW)Kb,/1!YE䔸s*aگ=cKv"NsCd ::Wv0t%<ajfQ%:5" ņ%yN.U|5bۤ1/QI t-9ve n Jɹk毉 ք#"mDz@2@G:,g%n= º mjJ9xS__%&Aӂ8FWG4GHY`X@pMֻNZj:+31H <@r:j!xqO3$wp t͸1V 5;]~"p0F7ȩty%Z⤈rOOϲל1cF g*mġ8}$5hAT*"P@Dތ"*v:iAT*""v~' /qйUJ R!ET^0~6V!=JE$"rtT_6hAT*"ڿGf\.w|qY[1Ώ g *t::"hR*"r"HZAS}?78jтTD[w ,S.;ƍn "sy?}(ʧ/;&pm9 "i~TDd ΣAUCĦ7|]N4.znj[gERaR{D䦸f ]$?`sܹCiFѳlЂM7-maLBI&YyTt:;UZ3׶̅-yнifSԱ  R'WܹTT*uEUu.0{mZu9 "ҟJ>-"A?cS}(Uq*רg7lLA$dlp;&*waB4 _t﫩=== b= uHsD i$Ƙ vߔK][ A9}@1`]ƝGY[<S}\ڟ;5'8$Ow^:I֮L;R(7m|S$ppy0o(>o]H$V%ɷbΫ [zoQjH.YʂƥSo6^N,I@@;ɷl7k3T5 K)Ƙx Dl'AD^s]5e j4oj1JH ;mԵqJ)5mfc]-/5/izJ)5 b+ gƔ*'ĝRJU bSiOhC XwJ)U>C1Ft{;Vŕ* zA)*VQ`mLy2pM9(T%ڪ v.|@A)*`sYPe!X>4RmS;x"\Ti8omIIDATf¶g0x 8'h*0~a`н/ǹйhwND)*ِ{oY ;B)ʺeNC\Ԉ`2!zyB&uE(F,hEѝ,1?.jd9RJ)51 y4rtp_?)p..8J)&\mZc=-Y⤎K6Tc(V`=rL}F[>gf/xԱRJM|幇 K?.(dڵwSJ)Ueb)p]by;]Cz3;E^J)Slf-5e.p!;bE_^TJ)Nqٵ}SD0o;5@Q݆L)TT* S-ӂ~Da1.}kes1RJ)RJ)RJ)D?Ìx}rIENDB`param-1.12.3/doc/_static/logo_horizontal_white.svg000066400000000000000000000325051434441564000222350ustar00rootroot00000000000000 image/svg+xml Param param-1.12.3/doc/_static/logo_stacked.png000066400000000000000000000260211434441564000202430ustar00rootroot00000000000000PNG  IHDR,\r_ pHYs篶tEXtSoftwarewww.inkscape.org< IDATx{|\u;i3MS\ \Pw?u -+ 4-t\/(^ mR xCˢEPmIܓzI2g3y>$=o2s{1c1c1c1E|0;嚚d<>r(h (NsL}k3 uuSgn"kP} )+,e1@ᝄ|<l<[a1pܧ3@bta !x~6ա P0EJ#/:B)g1mp"Se*nXaPuCC"z|g1mRu w\ESB> ῀Zy)P5?cZy#2S!,҅5@#EhL iS^30|0ºžs)ͫ(Z30(=' Dp( t <;xEsk2fbw\aT?,TNØH1^+@')r sX7:+ۀjY>%_I|'HaIWՉXY b<"UX XT K` H"UX*R;1DE*}gH([|g0˾3G |'0(;xD2|G0LwTa]= B~/|wTa(EȌ)?a"WX#Cr;12IuW'gS\a]+͍ɸ^NIT"WXaLDw;~,TOb9q^/ӳ%bH<զ㌨D?=V S3yq ;I."]X _.{~ѻc^!^pv&v3fTqUp2P;1 ! Tz~[[@a)Qs}}.J k(EywN.\Y)R4 #Yya(VLS{{|P5پV[3 L|t|0fW"P[ww'p9Xkee Vt}G{sLVV߀1Y|~[k<+5d3 E^wc +n|(U%Tʋ:zoØʓmk PKUʋ{S36!nh=lf5p, )|梎Ę=<[8Fϡ{LT$X̘M|&yƄ k}&/| 䟁oQE :/+,Ons| rtqٖ|1f5ɸN8wW8վ3VXyp}LV ;.|4}֟bXYaAdx,'gm];1c|(:+1n/rToK^d b!SGux6@^>È|PT5;5{jwUPU3AТ?./J3>,ǘ_J &W/kg՗ȏ;1K䞑=;J."]XkG U^1{$.8caGdSFf,W6ƃ|9)U}U1*M6 vK*c ;sіMcBgAH90ҋVڴS <0h2a]"rK­?2 (^0Oyp8GVWN.mmϬ /O#DPU9G5!J ^V/oyd}6c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1Ș2r3mȨk F jD\ uۊdQCŹЎMۦn{1Eo7^?`&?Bވ{^xr(/"<%"o}pliI4䍠F=*ܥ#5]+|w c?hjQ!H~ꭁdWv/=w cRaWDVt60Ƙzw]r0Ƙr{VrLK]c?cɯaOا;Oa![*dϭz Ƙpca!ph)_3+j,Ƙea7;1&ύ&-9+cUNcU;(Z9Җ:w%\9Cݘ*30| a)q/}՜w Q"r00 rhŞFx퀠πla#&=qAff2ٷӃ@=PzH_~E'Fu3 ϣ\|֫O2!BZKTN=2Kd'yTE_TMvRHUAC nU_/kN)D竧?_'vpFwʑ i=Am% u%͊AC8 ${Y}XM( Q84 ss(J6eL&jjݝ xXU^Dܝͧ4aS;ud3^7dꗬ>Xc!t.Srqp_{ wJܑ;{sˀ✕_JTcKI+,E<=$j.[{L) IDATH62zu7?$d<VP= Pq~0Ok~2rzfk9җƏ)SYkҹOOeg:w9U 'vŭ9r,-cCJmOTD Chc򩮊g,GלzɺA"^9^wuuM-%ZorCoBD;z_C9e"32ø<^:U4 9HʑmF}>ό^sͩgt勜e\~ +r$K.nBbq˯efL,w0r̈EÈS澇_N٦ %wNa|'N!!157;b5NAaT(]18%nz˓1I/w\ af*9JS M9z$voi=1pӹ.N)&ؘꊬHU KDviH>W:wJ$hU֫י)MqNLFfRE*e59Lڗx R0Ew^szB;nm[~@O.chܝ(r@.#sUٿ Wwq1rA7-Q\k,$D(rE+.?S{A pZCTy׋a業db*Aj%К#HOF@z(kTIx?)J/Mn L8W4 1鏩^k[.V گ9ѴvWqR|d3r`E".,sٜ[m業CX MGG!FLTO5W^C<#ekΧ^:+%TX<ĎDh܍NSEx!2<U\R)Ĺ% -An,hf…'Yc|sfq@AcD%vDBB!KVhl> !cdL@wi?wٚ| $rN'|oΧ;_LX oKot=]`}])PX7s) k"@Ҟnւ+R6?n.EUX"rQ9LtĒ'ieDy MՒ3ʲ Y)KǢJrݛ氇]?֔POE\:OyԽS".VZnQwb#@{Xy)r=Zza=:pK,lIv x{5Z*'&"9lύtmha <+cU qJ44s5բ8’ڽ|neNoG|J4~ʌI! {`c9I2lUxfy s(’ggT(Apr1iy*K1T1 bƌI8%lU%w&O  鸔w9bA KW]Cmsh E<<9n٨~01R_D6<+d]h̹X-X], ~ #J颐nA':a@FF"R+1KUCU@UILޔ^F @BkR WUܝ׭MducH9"u3Gpn OhcԩNO4ūd XT@]sCY gs%7 A~:b/#)|%8[uE8'zT<\SQVV `Fädua)U"\Uw1 `QۓFs"HOݖ{VREY:1BHR$OeRs.DOIV͆ *ު~,<3HJo׌*'=$O0>SCJ)%UXela8%K/ r*(p:dckl@yLtTa£! tH&عS$@Dɦ++/pޥ9@ a}ibi\Ӣ"Ŝ<.Yya tO]];xL-@01WVQtJef,Kv5`uDB/x,Ѵf"w;G2N2A@x,. A # Kbw8~'p^(4Nb!}Jm8a@HZr*qFG۷EMΤnM, DŮ4VrE*Lޅ&Y{36WX@E(/Qʶ-$u9Lda:Ms\0Kd*\z;G)f^G,&J*}wRֵl"r.,R+jk+y^:r[7SLq_{끏&Sk~~q:S "꣈l6:ؾ=N6*H-ODϊہ{'[FD6 Us&4wO$U5u&<^h#ǃܧS9[RuikC<=LC9ѽ0m`|d/l#^E{Dt*[%%,Oʟ(۬Ԫi}Su.84Pp`JZ =z.m˨< xmM(@Qh׭ kIENDB`param-1.12.3/doc/_static/logo_stacked.svg000066400000000000000000000311661434441564000202640ustar00rootroot00000000000000 image/svg+xml Param param-1.12.3/doc/_static/site.css000066400000000000000000000003111434441564000165470ustar00rootroot00000000000000.prompt { display: none; } .cell_output { padding-left: 0; } @media (min-width: 1200px) { .container, .container-lg, .container-md, .container-sm, .container-xl { max-width: 1600px; } } param-1.12.3/doc/_static/wordmark.png000066400000000000000000000141771434441564000174440ustar00rootroot00000000000000PNG  IHDRuk pHYs篶tEXtSoftwarewww.inkscape.org< IDATxyx\u3lɲfd 8!` H,ވCwI%!mRR(`ɡiI OI,Y dNH# M{ll/=C2{G3|^F{F#ѽ=_cS9,RDM7c `*T Pc0 Tj@PAcqZ |7vs],QlHy$ͦ_\՝D$3? lIȩx!vx_[b3bDwXtDzMs}:_A|N/+--t2""2N VD#|WDDx>C| <-/t.""23WqȢk+t.""2Lp{&CEDDƗ U0z:?&bAH2 k]DDDd|`y#r:)~ sNBDDD/\yױNBDD[)IN]S$DD%0nZ-54zvR^t**ˢ ba8K -[ON$ hxᤓس+0T_.U~}x?ɟN(_[qEDdH Ƶ-d^wI ( WH.wNmwI]J5/irsㄩ[3!=c8#1xü[ѓ?5&ruf(SaT:^sZhT]fL?܎68͏rܫH[/~pv9 Hm "O}o3Ls| 2n"!bc{=`mOUZ̭ԾD,<aM.-ڶlҒ|!{m+/j뛿?$/ϡ|mgv208a&0-T"g"y`uOƹ čeOT}rs ?x+0ûݏVǞQ7)ߴo]ʞw3O$83=!y[g?@Q43Nt ?88(eN!_F<4έIKwexK~\]FH>uASUʱSgfl]1Ʃyk)\s9ބJ/$gl7? M[q~i>ܱ$&+?s8 E2PmVњ['o\r{*swfs|9@&ocqn &8ٞه?*c?즎L(H,]چuܣDjkc}41w7.XxtMg9f\d9ƶl_۳{2 b5X@Va˧N.eKnnw`3}rgybAv@uyю/yvٚd]pBsY밬}G<Ȉ~by璈eC=ELrf뛿U[35nW5W64} lWϋ=]C0>0U>]\^U]=-k>=neg:n7C Xy+e~8[YPmώm6R*K D0+ /5KO.zʓZڑo鰯B'CkǪ#:rvݲf: nWs uج >a;Ҵ8=SB2|km `;ͽ GT{̂n vǙW=I+ܫ v.-8[sfnAV VLn]Z_dq^ bp&N>\"P1pe=֧3݋qTO;:΄Q7 ^pߙUUUֽ2e*f>۱w㼝Z>b#gq/9x6yԿ.[sQpI;RcR+A`/юv3m23n/鮝a^d-gri{0&;͉)]p?Dɳ-نƹ>mU8nsx8rs7?mh>9fi 1Yʅ3{9um= _\ՓNN,5b >\]qC-._#yWg_8~:cd6 \ځVҷv`Kރ{|>zU6f Ov5-p_wvݼU7>پM$) D__; NK3[PًC4X <٣oc7~K5^>pT9&:x bǪEd뛚K}3[@} #!J^%uVVv{@(dSNObiy81"O->r`"!C钩WHЍ:׊|&(ѵx!^ޛ@8,"g >\Qg`{b1^u0fK"HGl.am=~@.HPßw5S38? fS"jW.; |[{;Ծ{whf8b[ZMp麞l2f>c ٗ&ã q܁MO$XRvmQKy85[| J^MԂ}GV}T,s; .u㕝i{;Ͷ7=OsNQ_t/PV;mשtI|ΫW9a/w b k`c> Hz.@Gվmٮ!fjJɫX#w>޸?i3k^a3, x՛襀8ZGFry#K~ÑĜy987FsKzojbPAn}1D5 ~3X9ӮY}L_v G(֧k{OW:h8YNWܞ9ٮ6Edh >nG eF.)i5Kמt0[ۤ'ln5l"gI?\+YKKFK_Ya] BtCYmHw{Y("4 b; m6ڶlӖ5F~'mH ӯjMᰰйL`fЯ;RcA܉ٿvhg@9Qwp醲T+RDdTKAl1 gK¸=;Q1,lf-`*"RpZwwwL6=? dL9<:(XA.c9w=r{q=[߼?2:zyHAt0l'UoMg; fx, 0!ڀVvrRd?*"c/hX[׶&1` 5 ͧ~Frm~R:Z3۸a)^nF2L3\٧lz #37WU<9Rl5 Au2qX!Mh'mF}3_=I>ײ Vt+ɂiX;]D. ^<#V^l6d4f3EDƳ+vaaI{ۮhJN]ɡ;޸ )Yn;F1amo\b(")8;&Nl{HB%=:1~ V%%UAh P :xDTRRA Fv$JIڒ۝T>""Pb1JdGk{.Ƀ*n-6`JDRTR1@At<.[SD>%}E&uk(ITIĠ|;RlL"e=vɭ/u"?%UyC̮ih>/Jt깑KnO%HIAQ764n]WZ-H\&bǙ@2wLdꛞ?uDw+qH3'Qu*K h*kv櫳]=7;O# -p#Q5Hs k "ɐg ܝ4 0|Lz ^oz9`$6Dd(Km+/BQZs9XD39Tm`W9$ bۊYba4^B!"r@ID0Ǣz I)k\qe-оbø}(iX/B""Rq_(tm9\:)m%]ZW.B"" "@ۊcйU6j &AW,wI#]kY:$aqKb7:CЇIH2JSiI#rWn| V?&6D,M8|i]hYf/6ඉ(ܶr۩}KDY@ (b2u'ha;c|ly7` MyzywzĎ8y@FqOL:3;8Ti]Slv|s?ţ m%{@gfs|}ko"#(喚sٚlum,Ǧ^1Jw*::Gt8 }n_ vz amơrYQ0ތ[lYJG؋gm P\ ; DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD^ۑ\u_IENDB`param-1.12.3/doc/_static/wordmark.svg000066400000000000000000000223751434441564000174560ustar00rootroot00000000000000 image/svg+xml Param param-1.12.3/doc/about.md000066400000000000000000000024641434441564000151120ustar00rootroot00000000000000# About 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 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.](https://anaconda.com) and by community contributors. Param is maintained as part of the [HoloViz](https://holoviz.org) family of tools. The [holoviz.org](https://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](https://examples.pyviz.org). If you have any questions or usage issues visit the [Param Discourse site](https://discourse.holoviz.org/c/param), and if you want to report bugs or request new features, first see if it's already in our list of [open issues](https://github.com/holoviz/param/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](https://twitter.com/HoloViz_org). Thanks! param-1.12.3/doc/comparisons.md000066400000000000000000000166761434441564000163470ustar00rootroot00000000000000# Comparison to other approaches 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: - [Traits](http://code.enthought.com/projects/traits) - [Traitlets](https://github.com/ipython/traitlets) - [attrs](https://github.com/python-attrs/attrs) (with optional [attrs-strict](https://github.com/bloomberg/attrs-strict)) - [Django models](https://docs.djangoproject.com/en/3.1/topics/db/models) - [Pydantic](https://pydantic-docs.helpmanual.io) Plus, Python itself has incorporated mechanisms addressing some of the same issues: - [Python 3.6+ type annotations](https://www.python.org/dev/peps/pep-0526/) - [Python 3.7+ data classes](https://docs.python.org/3/library/dataclasses.html) - [Python 2.6+ namedtuples](https://docs.python.org/3/library/collections.html#namedtuple-factory-function-for-tuples-with-named-fields) - [Python 2.2+ properties](https://docs.python.org/3/library/functions.html#property) 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). [Other info](https://threeofwands.com/why-i-use-attrs-instead-of-pydantic/) comparing `attrs` to `pydantic` is also available. 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`. ## Brevity of code 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/pydantic/Traits-based approaches lead to much simpler and more maintainable codebases. 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. 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! `pydantic` and `attrs` provide many of these same benefits, though `attrs` type and bounds validation is treated as an extra step that is more general but also typically much more verbose. ## Runtime checking 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, pydantic, 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. Note that Pydantic focuses on [generating valid outputs at runtime](https://github.com/samuelcolvin/pydantic/issues/578) rather than detecting invalid inputs, so it may or may not be suitable for the same types of applications as the other libraries discussed here. ## Generality and ease of integration with your project 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. Param, Traitlets, Pydantic, 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. 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. 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. ## GUI toolkits 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: - Panel: Jupyter and Bokeh-server support for Param, mapping Parameters to widgets and ParameterizedObjects to sets of widgets - ParamTk: (unsupported) TKinter support for Param - IPywidgets: Jupyter support for Traitlets, but without automatic mapping from trait to widget - TraitsUI: wxWidgets and Qt support for Traits ## Dynamic values Param, Traits, Traitlets, Pydantic, 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. Pydantic, Traits, and Traitlets also allow a class author to add code for a given parameter to compute a default value on first access. ```python >>> from time import time, sleep >>> import traitlets as tr >>> class A(tr.HasTraits): ... instantiation_time = tr.Float() ... @tr.default('instantiation_time') ... def _look_up_time(self): ... return time() ... >>> a=A() >>> time() 1634594159.2040331 >>> sleep(1) >>> time() 1634594165.3485172 >>> a.instantiation_time 1634594167.812151 >>> a.instantiation_time 1634594167.812151 >>> sleep(1) >>> b=A() >>> b.instantiation_time 1634594178.427819 ``` Param's equivalent decorator `@param.depends(on_init=True)` will run a method when the Parameterized class is instantiated, not on first access. On the other hand, Param does allow fully dynamic values for *any* access to a numeric Parameter instance, not just the original instantiation: ```python >>> from time import time >>> import param >>> class A(param.Parameterized): ... val=param.Number(0) ... >>> a=A() >>> a.val 0 >>> a.val=lambda:time() >>> a.val 1475587455.437027 >>> a.val 1475587456.501314 ``` param-1.12.3/doc/conf.py000066400000000000000000000025611434441564000147530ustar00rootroot00000000000000# -*- coding: utf-8 -*- from nbsite.shared_conf import * project = u'param' authors = u'HoloViz developers' copyright = u'2003-2022 ' + authors description = 'Declarative Python programming using Parameters' import param param.parameterized.docstring_signature = False param.parameterized.docstring_describe_params = False version = release = base_version(param.__version__) nbbuild_cell_timeout = 600 html_static_path += ['_static'] html_theme = "pydata_sphinx_theme" html_logo = "_static/logo_horizontal.png" html_favicon = "_static/favicon.ico" html_css_files = ['site.css'] exclude_patterns += ['historical_release_notes.rst'] html_theme_options = { "github_url": "https://github.com/holoviz/param", "icon_links": [ { 'name': 'Twitter', 'url': 'https://twitter.com/holoviz_org', 'icon': 'fab fa-twitter-square', }, { "name": "Discourse", "url": "https://discourse.holoviz.org/", "icon": "fab fa-discourse", }, ], "footer_items": [ "copyright", "last-updated", ], 'google_analytics_id': 'UA-154795830-6', } extensions += ['sphinx_copybutton'] # Override the Sphinx default title that appends `documentation` html_title = f'{project} v{version}' # Format of the last updated section in the footer html_last_updated_fmt = '%Y-%m-%d' param-1.12.3/doc/getting_started.md000066400000000000000000000201471434441564000171650ustar00rootroot00000000000000# Getting Started ## Installation Param has no required dependencies outside of Python's standard library, and so it is very easy to install. 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 ``` or ``` pip install --user param ``` or ``` pip install param ``` ## Using Param to get simple, robust code The `param` library gives you Parameters, which are used in Parameterized classes. A Parameter is a special type of Python class 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 or instance: ```{code-block} python import param class A(param.Parameterized): title = param.String(default="sum", doc="Title for the result") class B(A): a = param.Integer(2, bounds=(0,10), doc="First addend") b = param.Integer(3, bounds=(0,10), doc="Second addend") def __call__(self): return self.title + ": " + str(self.a + self.b) ``` ```{code-block} python >> o1 = B(b=4, title="Sum") >> o1.a = 5 >> o1() 'Sum: 9' ``` ```{code-block} python >> o1.b 4 ``` 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 superclass where it applies, and its default value all the other metadata will be inherited by each subclass. 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: ```{code-block} python >>> ParamClass(a="four") ValueError: Parameter 'a' must be an integer. >>> o2 = ParamClass() >>> o2.b = -5 ValueError: Parameter 'b' must be at least 0 ``` 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), 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. 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. Validating user inputs is generally a large fraction of a program's code, because such inputs are a huge source of vulnerabilities and potential error conditions, and Param lets you avoid ever having to write nearly any of that code. The [User Guide](user_guide/index) explains all the other Param features for simplifying your codebase, improving input validation, allowing flexible configuration, and supporting serialization. ## Using Param for configuration 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: ```{code-block} python >>> A.title = "The sum is" >>> B.a = 6 >>> o3 = B() >>> o3() 'The sum is: 9' ``` 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! ## Using Param to explore parameter spaces 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: ```{code-block} python >>> import random >>> o2 = B(a = lambda: random.randint(0,5)) >>> o2(), o2(), o2(), o2() ('The sum is: 6', 'The sum is: 7', 'The sum is: 3', 'The sum is: 3') ``` 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: ```{code-block} python >>> o2.a 4 ``` 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. 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: ```{code-block} python >>> import numbergen as ng >>> o3 = B(a = ng.Choice(choices=[2,4,6]), >>> b = 1+2*ng.UniformRandomInt(ubound=3)) >>> o3(), o3(), o3(), o3() ('The sum is: 11', 'The sum is: 3', 'The sum is: 13', 'The sum is: 7') ``` 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 special coordination code. ## Using Param to build GUIs 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. 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. ## Learning more The [User Guide](user_guide/index) 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! param-1.12.3/doc/index.md000066400000000000000000000044531434441564000151070ustar00rootroot00000000000000# Welcome to Param!

Are you a Python programmer? If so, you need Param, and check out our 5-minute intro video to see why! 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. 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!). 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. 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. To quickly see how Param works and can be used, jump straight into the [Getting Started Guide](getting_started), then check out the full functionality in the [User Guide.](user_guide/index) ```{toctree} --- hidden: true --- Introduction Getting Started User Guide Comparisons Roadmap Releases API About ``` param-1.12.3/doc/reference.rst000066400000000000000000000034731434441564000161470ustar00rootroot00000000000000******************** API Reference Manual ******************** The Param API Reference Manual provides a comprehensive reference for all modules, functions, classes, and methods provided by Param. See the `User Guide <../user_guide>`_ for a more readable overview and explanations; this material duplicates what is available from `help(obj)` for each object. `parameterized`_ Parameter, Parameterized, and other core classes and methods `param`_ Optional predefined parameter types like Number, Selector, etc. `ipython`_ Optional help functions tailored for Jupyter and IPython `serializer`_ Optional JSON serialization `version`_ Automatic generation of version strings using version control parameterized Module ==================== .. inheritance-diagram:: param.parameterized --------------------------- .. automodule:: param.parameterized :members: :inherited-members: :show-inheritance: param Module =============== .. inheritance-diagram:: param.__init__ ---------------------- .. automodule:: param.__init__ :members: :inherited-members: :show-inheritance: ipython Module ============== .. inheritance-diagram:: param.ipython --------------------- .. automodule:: param.ipython :members: :inherited-members: :show-inheritance: serializer Module ================= .. inheritance-diagram:: param.serializer ------------------------ .. automodule:: param.serializer :members: :inherited-members: :show-inheritance: version Module ============== .. inheritance-diagram:: param.version --------------------- .. automodule:: param.version :members: :inherited-members: :show-inheritance: .. _parameterized: #parameterized-module .. _param: #param-module .. _ipython: #ipython-module .. _serializer: #serializer-module .. _version: #version-module param-1.12.3/doc/releases.md000066400000000000000000000655371434441564000156150ustar00rootroot00000000000000# Releases ## Version 1.12.3 Date: 2022-12-06 The `1.12.3` release adds support for Python 3.11. Many thanks to @musicinmybrain (first contribution!) and @maximlt for contributing to this release. Enhancements: * Preserve existing Random seed behavior in Python 3.11 ([#638](https://github.com/holoviz/param/pull/638)) * Add support for Python 3.11 ([#658](https://github.com/holoviz/param/pull/658)) ## Version 1.12.2 Date: 2022-06-14 The `1.12.2` release fixes a number of bugs and adds support again for Python 2.7, which was unfortunately no longer supported in the last release. Note however that Param 2.0 will still drop support of Python 2.7 as already announced. Many thanks to @Hoxbro and the maintainers @jbednar, @jlstevens, @maximlt and @philippjfr for contributing to this release. Bug fixes: * Match against complete spec name when determining dynamic watchers ([#615](https://github.com/holoviz/param/pull/615)) * Ensure async functionality does not cause python2 syntax errors ([#624](https://github.com/holoviz/param/pull/624)) * Allow (de)serializing `CalendarRange` and `DateRange` `Parameters` ([#625](https://github.com/holoviz/param/pull/625)) * Improve `DateRange` validation ([#627](https://github.com/holoviz/param/pull/627)) * Fix regression in `@param.depends` execution ordering ([#628](https://github.com/holoviz/param/pull/628)) * Ensure `named_objs` does not fail on unhashable objects ([#632](https://github.com/holoviz/param/pull/632)) * Support comparing date-like objects ([#629](https://github.com/holoviz/param/pull/629)) * Fixed `BinaryPower` example in the docs to use the correct name `EvenInteger`([#634](https://github.com/holoviz/param/pull/634)) ## Version 1.12.1 The 1.12.1 release fixes a number of bugs related to async callback handling when using `param.depends` and `.param.watch` and a number of documentation and error messages. Many thanks to @HoxBro and the maintainers @jbednar, @jlstevens, @maximlt and @philippjfr for contributing to this release. Error handling and documentation: - Fixed description of shared_parameters ([#568](https://github.com/holoviz/param/pull/568)) - Improve the error messages of Date and DateRange ([#579](https://github.com/holoviz/param/pull/579)) - Clarified step error messages and other docs and links ([#604](https://github.com/holoviz/param/pull/604)) Bug fixes: - Make iscoroutinefunction more robust ([#572](https://github.com/holoviz/param/pull/572)) - Fix for handling misspelled parameter ([#575](https://github.com/holoviz/param/pull/575)) - Handle None serialization for Date, CalendarDate, Tuple, Array, and DataFrame ([#582](https://github.com/holoviz/param/pull/582)) - Support async coroutines in param.depends ([#591](https://github.com/holoviz/param/pull/591)) - Handle async functions in depends with watch=True ([#611](https://github.com/holoviz/param/pull/611)) - Avoid equality check on Watcher ([#612](https://github.com/holoviz/param/pull/612)) Documentation: - Fix binder ([#564](https://github.com/holoviz/param/pull/564)) - Fixed description of shared_parameters ([#568](https://github.com/holoviz/param/pull/568)) ## Version 1.12.0 Version 1.12.0 introduces a complete user manual and website (for the first time since 2003!) along with extensive API improvements gearing up for the 2.0 release (which will be Python3 only). The pre-2.0 API is still being preserved and no new warnings are added in this release, so the older API can continue to be used with this release, but the next 1.x release is expected to enable warnings for deprecated API. If you have older code using the deprecated Param features below, please update your API calls as described below to be compatible with the 2.0 release when it comes out (or pin to param<2 if you don't need any new Param features). For new users, just use the API documented on the website, and you should be ready to go for either 1.12+ or 2.0+. Thanks to James A. Bednar for the user guide and 2.0 API support, to Philipp Rudiger for improvements and new capabilities for handling dependencies on subobjects, and to Maxime Liquet and Philipp Rudiger for extensive improvements to the website/docs/package-building/testing. New features: - Added future-facing API for certain Parameterized().param methods (see Compatibility below; [#556](https://github.com/holoviz/param/pull/556), [#558](https://github.com/holoviz/param/pull/558), [#559](https://github.com/holoviz/param/pull/559)) - New option `on_init=True` for `@depends` decorator, to run the method in the constructor to ensure initial state is consistent when appropriate ([#540](https://github.com/holoviz/param/pull/540)) - Now resolves subobject dependencies dynamically, allowing dependencies on internal parameters of subobjects to resolve appropriately as those objects are replaced. ([#552](https://github.com/holoviz/param/pull/552)) - Added prettyprinting for numbergen expressions ([#525](https://github.com/holoviz/param/pull/525)) - Improved JSON schema generation ([#458](https://github.com/holoviz/param/pull/458)) - Added more-usable script_repr command, availabie in param namespace, with default values, and showing imports ([#522](https://github.com/holoviz/param/pull/522)) - Added Parameterized.param.pprint(); underlying implementation of script_repr but with defaults suitable for interactive usage rather than generating a .py script. ([#522](https://github.com/holoviz/param/pull/522)) - Watchers can now declare precedence so that events are triggered in the desired order ([#552](https://github.com/holoviz/param/pull/552), [#557](https://github.com/holoviz/param/pull/557)) Bug fixes: - Fix bug setting attributes in some cases before class is initialized ([#544](https://github.com/holoviz/param/pull/544)) - Ensure None is supported on ListSelector ([#511](https://github.com/holoviz/param/pull/511)) - Switched from deprecated `inspect.getargspec` to the py3 version `inspect.getfullargspec`, which is similar but splits `keyword` args into `varkw` (**) and kw-only args. Falls back to getargspec on Python2. ([#521](https://github.com/holoviz/param/pull/521)) Doc improvements (including complete user guide for the first time!): - Misc comments/docstrings/docs cleanup ([#507](https://github.com/holoviz/param/pull/507), [#518](https://github.com/holoviz/param/pull/518), [#528](https://github.com/holoviz/param/pull/528), [#553](https://github.com/holoviz/param/pull/553)) - Added comparison with pydantic ([#523](https://github.com/holoviz/param/pull/523)) - Added new user guide sections: * Dependencies_and_Watchers user guide ([#536](https://github.com/holoviz/param/pull/536)) * Dynamic Parameters ([#525](https://github.com/holoviz/param/pull/525)) * Outputs ([#523](https://github.com/holoviz/param/pull/523)) * Serialization and Persistence ([#523](https://github.com/holoviz/param/pull/523)) Infrastructure: - Added testing on Python 3.10 and on Mac OS X and removed slow PyPy/pandas/numpy tests ([#548](https://github.com/holoviz/param/pull/548), [#549](https://github.com/holoviz/param/pull/549), [#526](https://github.com/holoviz/param/pull/526)) - Docs/tests/build infrastructure improvements ([#509](https://github.com/holoviz/param/pull/509), [#521](https://github.com/holoviz/param/pull/521), [#529](https://github.com/holoviz/param/pull/529), [#530](https://github.com/holoviz/param/pull/530), [#537](https://github.com/holoviz/param/pull/537), [#538](https://github.com/holoviz/param/pull/538), [#539](https://github.com/holoviz/param/pull/539), [#547](https://github.com/holoviz/param/pull/547), [#548](https://github.com/holoviz/param/pull/548), [#555](https://github.com/holoviz/param/pull/555)) Compatibility (see [#543](https://github.com/holoviz/param/pull/543) for the complete list): - Calendardate now accepts date values only ([#517](https://github.com/holoviz/param/pull/517)) - No longer allows modifying name of a Parameter once it is in a Parameterized class, to avoid confusion ([#541](https://github.com/holoviz/param/pull/541)) - Renamed (with old name still accepted for compatibility until 2.0): * `.param._add_parameter()`: Now public `.param.add_parameter()`; too useful to keep private! ([#559](https://github.com/holoviz/param/pull/559)) * `.param.params_depended_on`: Now `.param.method_dependencies` to indicate that it accepts a method name and returns its dependencies ([#559](https://github.com/holoviz/param/pull/559)) * `.pprint`: Now private `._pprint`; use public `.param.pprint` instead ([#559](https://github.com/holoviz/param/pull/559)) * `batch_watch`: Now `batch_call_watchers`, to declare that it does not set up watching, it just invokes it. Removed unused operation argument ([#536](https://github.com/holoviz/param/pull/536)) - Deprecated (but not yet warning unless noted): * `.param.debug()`: Use `.param.log(param.DEBUG, ...)` instead ([#556](https://github.com/holoviz/param/pull/556)) * `.param.verbose()`: Use `.param.log(param.VERBOSE, ...)` instead ([#556](https://github.com/holoviz/param/pull/556)) * `.param.message()`: Use `.param.log(param.MESSAGE, ...)` instead ([#556](https://github.com/holoviz/param/pull/556)) * `.param.defaults()`: Use `{k:v.default for k,v in p.param.objects().items()}` instead ([#559](https://github.com/holoviz/param/pull/559)) * `.param.deprecate()`: To be repurposed or deleted after 2.0 ([#559](https://github.com/holoviz/param/pull/559)) * `.param.params()`: Use `.param.values()` or `.param['param']` instead ([#559](https://github.com/holoviz/param/pull/559)) * `.param.print_param_defaults()`: Use `for k,v in p.param.objects().items(): print(f"{p.__class__.name}.{k}={repr(v.default)}")` instead ([#559](https://github.com/holoviz/param/pull/559)) * `.param.print_param_values()`: Use `for k,v in p.param.values().items(): print(f"{p.name}.{k}={v}")` instead ([#559](https://github.com/holoviz/param/pull/559)) * `.param.set_default()`: Use `p.param.default=` instead ([#559](https://github.com/holoviz/param/pull/559)) * `.param.set_param()`: Had tricky API; use `.param.update` instead ([#558](https://github.com/holoviz/param/pull/558)) * `.param.get_param_values()`: Use `.param.values().items()` instead (or `.param.values()` for the common case of `dict(....param.get_param_values())`) ([#559](https://github.com/holoviz/param/pull/559)) * `.state_pop()`: Will be renamed to `._state_pop` to make private * `.state_push()`: Will be renamed to `._state_push` to make private * `.initialized`: Will be renamed to `._initialized` to make private * Most methods on Parameterized itself have already been deprecated and warning for some time now; see [#543](https://github.com/holoviz/param/pull/543) for the list. Use the corresponding method on the `.param` accessor instead. - Added: * `.param.watchers`: Read-only version of private `_watchers` ([#559](https://github.com/holoviz/param/pull/559)) * `.param.log()`: Subsumes .debug/verbose/message; all are logging calls. ([#556](https://github.com/holoviz/param/pull/556)) * `.param.update()`: Dictionary-style updates to parameter values, as a drop-in replacement for `set_param` except for its optional legacy positional-arg syntax ([#558](https://github.com/holoviz/param/pull/558)) * `.values()`: Dictionary of name:value pairs for parameter values, replacing `get_param_values` but now a dict since python3 preserves order ([#558](https://github.com/holoviz/param/pull/558)) * `.param.log()`: General-purpose interface to the logging module functionailty; replaces .debug, .verbose, .message ([#556](https://github.com/holoviz/param/pull/556)) ## Version 1.11.1 (including changes in 1.11.0; 1.11.1 adds only a minor change to fix `param.List(None)`.) Version 1.11 contains entirely new [documentation](https://param.holoviz.org), plus various enhancements and bugfixess. Thanks to James A. Bednar for the documentation, Philipp Rudiger for the website setup and for many of the other fixes and improvements below, and others as noted below. Documentation: - Brand-new website, with getting started, user manual, reference manual, and more! Some user guide sections are still under construction. ([#428](https://github.com/holoviz/param/pull/428),[#464](https://github.com/holoviz/param/pull/464),[#479](https://github.com/holoviz/param/pull/479),[#483](https://github.com/holoviz/param/pull/483),[#501](https://github.com/holoviz/param/pull/501),[#502](https://github.com/holoviz/param/pull/502),[#503](https://github.com/holoviz/param/pull/503),[#504](https://github.com/holoviz/param/pull/504)) - New intro video with examples/Promo.ipynb notebook, thanks to Marc Skov Madsen and Egbert Ammicht ([#488](https://github.com/holoviz/param/pull/488)) - Sort docstring by definition order and precedence, thanks to Andrew Huang ([#445](https://github.com/holoviz/param/pull/445)) Enhancements: - Allow printing representations for recursively nested Parameterized objects ([#499](https://github.com/holoviz/param/pull/499)) - Allow named colors for param.Color ([#472](https://github.com/holoviz/param/pull/472)) - Allow FileSelector and MultiFileSelector to accept initial values ([#497](https://github.com/holoviz/param/pull/497)) - Add Step slot to Range, thanks to Simon Hansen ([#467](https://github.com/holoviz/param/pull/467)) - Update FileSelector and MultiFileSelector parameters when setting path ([#476](https://github.com/holoviz/param/pull/476)) - Improved error messages ([#475](https://github.com/holoviz/param/pull/475)) Bug Fixes: - Fix Path to allow folders, as documented but previously not supported ([#495](https://github.com/holoviz/param/pull/495)) - Fix previously unimplemented Parameter._on_set ([#484](https://github.com/holoviz/param/pull/484)) - Fix Python2 IPython output parameter precedence ([#477](https://github.com/holoviz/param/pull/477)) - Fix allow_None for param.Series and param.DataFrame ([#473](https://github.com/holoviz/param/pull/473)) - Fix behavior when both `instantiate` and `constant` are `True` ([#474](https://github.com/holoviz/param/pull/474)) - Fix for versioning when param is inside a separate git-controlled repo (port of fix from autover/pull/67) ([#469](https://github.com/holoviz/param/pull/469)) Compatibility: - Swapped ObjectSelector and Selector in the inheritance hierarchy, to allow ObjectSelector to be deprecated. ([#497](https://github.com/holoviz/param/pull/497)) - Now `get_soft_bounds`silently crops softbounds to any hard bounds supplied ; previously soft bounds would be returned even if they were outside the hard bounds ([#498](https://github.com/holoviz/param/pull/498)) - Rename `class_` to `item_type` in List parameter, to avoid clashing semantics with ClassSelector and others with a `class_` slot. `class_` is still accepted as a keyword but is stored in `item_type`. ([#456](https://github.com/holoviz/param/pull/456)) ## Version 1.10.1 Minor release for Panel-related bugfixes and minor features, from @philippjfr. - Fix serialization of Tuple, for use in Panel ([#446](https://github.com/holoviz/param/pull/446)) - Declare asynchronous executor for Panel to use ([#449](https://github.com/holoviz/param/pull/449)) - Switch to GitHub Actions ([#450](https://github.com/holoviz/param/pull/450)) ## Version 1.10.0 ## Version 1.9.3 - Fixed ClassSelector.get_range when a tuple of types is supplied ([#360](https://github.com/holoviz/param/pull/360)) ## Version 1.9.2 - Compatibility with Python 3.8 - Add eager option to watch calls ([#351](https://github.com/holoviz/param/pull/351)) - Add Calendar and CalendarDateRange for real date types ([#348](https://github.com/holoviz/param/pull/348)) ## Version 1.9.1 Enhancements: - Allow param.depends to annotate functions ([#334](https://github.com/holoviz/param/pull/334)) - Add context managers to manage events and edit constant parameters Bug fixes: - Ensure that Select constructor does not rely on truthiness ([#337](https://github.com/holoviz/param/pull/337)) - Ensure that param.depends allows mixed Parameter types ([#338](https://github.com/holoviz/param/pull/338)) - Date and DateRange now allow dt.date type ([#341](https://github.com/holoviz/param/pull/341)) - Ensure events aren't dropped in batched mode ([#343](https://github.com/holoviz/param/pull/343)) ## Version 1.9.0 Full release with new functionality and some fixes. New features: - Added support for instance parameters, allowing parameter metadata to be modified per instance and allowing parameter objects to be passed to Panel objects ([#306](https://github.com/holoviz/param/pull/306)) - Added label slot to Parameter, to allow overriding attribute name for display ([#319](https://github.com/holoviz/param/pull/319)) - Added step slot to Parameter, e.g. to control Panel widget step size ([#326](https://github.com/holoviz/param/pull/326)) - Added keywords_to_params utility for deducing Parameter types and ranges automatically ([#317](https://github.com/holoviz/param/pull/317)) - Added support for multiple outputs from a Parameterized ([#312](https://github.com/holoviz/param/pull/312)) - Added Selector as a more user-friendly version of ObjectSelector, accepting a list of options as a positional argument ([#316](https://github.com/holoviz/param/pull/316)) Changes affecting backwards compatibility: - Changed from root logger to a param-specific logger; no change to API but will change format of error and warning messages ([#330](https://github.com/holoviz/param/pull/330)) - Old abstract class Selector renamed to SelectorBase; should be no change unless user code added custom classes inherited from Selector without providing a constructor ([#316](https://github.com/holoviz/param/pull/316)) Bugfixes and other improvements: - Various bugfixes ([#320](https://github.com/holoviz/param/pull/320), [#323](https://github.com/holoviz/param/pull/323), [#327](https://github.com/holoviz/param/pull/327), [#329](https://github.com/holoviz/param/pull/329)) - Other improvements ([#315](https://github.com/holoviz/param/pull/315), [#325](https://github.com/holoviz/param/pull/325)) For more details, you can see a [full list of changes since the previous release](https://github.com/holoviz/param/compare/v1.8.2...v1.9.0). ## Version 1.8.2 Minor release: - Added output decorator and outputs lookup method ([#299](https://github.com/holoviz/param/pull/299), [#312](https://github.com/holoviz/param/pull/312)) For more details, you can see a [full list of changes since the previous release](https://github.com/holoviz/param/compare/v1.8.2...v1.8.1). ## Version 1.8.1 Minor release: - Added positional default arguments for nearly all Parameter subclasses (apart from ClassSelector) - Minor bugfixes for watching callbacks For more details, you can see a [full list of changes since the previous release](https://github.com/holoviz/param/compare/v1.8.1...v1.8.0). ## Version 1.8.0 Major new feature set: comprehensive support for events, watching, callbacks, and dependencies - Parameterized methods can now declare `@depends(p,q)` to indicate that they depend on parameters `p` and `q` (defaulting to all parameters) - Parameterized methods can depend on subobjects with `@depends(p.param,q.param.r)`, where `p.param` indicates dependencies on all parameters of `p` and `q.param.r` indicates a dependency on parameter `r` of `q`. - Functions and methods can `watch` parameter values, re-running when those values change or when an explicit trigger is issued, and can unwatch them later if needed. - Multiple events can be batched to trigger callbacks only once for a coordinated set of changes Other new features: - Added support in ObjectSelector for selecting lists and dicts ([#268](https://github.com/holoviz/param/pull/268)) - Added pandas DataFrame and Series parameter types ([#285](https://github.com/holoviz/param/pull/285)) - Added support for regular expression validation to String Parameter ([#241](https://github.com/holoviz/param/pull/241), [#245](https://github.com/holoviz/param/pull/245)) For more details, you can see a [full list of changes since the previous release](https://github.com/holoviz/param/compare/v1.8.0...v1.7.0). ## Version 1.7.0 Since the previous release (1.6.1), there should be no changes that affect existing code, only additions: * A new param namespace object, which in future will allow subclasses of Parameterized to have much cleaner namespaces ([#230](https://github.com/holoviz/param/pull/230)). * Started testing on python 3.7-dev ([#223](https://github.com/holoviz/param/pull/223)). * param.version now provides functions to simplify dependants' setup.py/setup.cfg files (see https://github.com/pyviz-dev/autover/pull/49). Although param should still work on python 3.3, we are no longer testing against it (unsupported by our test environment; [#234](https://github.com/holoviz/param/pull/234)). For more details, you can see a [full list of changes since the previous release](https://github.com/holoviz/param/compare/v1.6.1...v1.7.0). ## Version 1.6.1 Restores support for the previous versioning system (pre 1.6; see [#225](https://github.com/holoviz/param/pull/225)), and fixes a number of issues with the new versioning system: * Allow package name to differ from repository name (https://github.com/pyviz-dev/autover/pull/39) * Allow develop install to work when repository is dirty (https://github.com/pyviz-dev/autover/pull/41) * Fixed failure to report dirty when commit count is 0 (https://github.com/pyviz-dev/autover/pull/44) ## Version 1.6.0 Notable changes, fixes, and additions since the previous release (1.5.1) are listed below. You can also see a [full list of changes since the previous release](https://github.com/holoviz/param/compare/v1.5.1...v1.6.0). Changes: * `param.__version__` is now a string * `param.version.Version` now supports a tag-based versioning workflow; if using the `Version` class, you will need to update your workflow (see [autover](https://github.com/holoviz/autover) for more details). * Dropped support for python 2.6 ([#175](https://github.com/holoviz/param/pull/175)). * No longer attempt to cythonize param during installation via pip ([#166](https://github.com/holoviz/param/pull/166), [#194](https://github.com/holoviz/param/pull/194)). Fixes: * Allow `get_param_values()` to work on class ([#162](https://github.com/holoviz/param/pull/162)). * Fixed incorrect default value for `param.Integer` ([#151](https://github.com/holoviz/param/pull/151)). * Allow a `String` to be `None` if its default is `None` ([#104](https://github.com/holoviz/param/pull/104)). * Fixed `ListSelector.compute_default()` ([#212](https://github.com/holoviz/param/pull/212)). * Fixed checks for `None` in various `Parameter` subclasses ([#208](https://github.com/holoviz/param/pull/208)); fixes problems for subclasses of `Parameterized` that define a custom `__nonzero__` or `__len__`. Additions: * Added `DateRange` parameter. Miscellaneous: * No longer tested on python 3.2 (unsupported by our test environment; [#218](https://github.com/holoviz/param/pull/218)). ## Version 1.5.1 * Fixed error messages for ClassSelector with tuple of classes * Added get and contains methods for ParamOverrides A full list of changes since the previous release is available [here](https://github.com/holoviz/param/compare/v1.5.0...v1.5.1). ## Version 1.5.0 - Added Range, Color, and Date parameters - Improved ObjectSelector error messages - Minor bugfixes A full list of changes since the previous release is available [here](https://github.com/holoviz/param/compare/v1.4.2...v1.5.0). ## Version 1.4.2 - Improved version reporting from version module - Minor bugfixes A full list of changes since the previous release is available [here](https://github.com/holoviz/param/compare/v1.4.1...v1.4.2). ## Version 1.4.1 * Selector parameters now respect order of options supplied * Allowed softbounds to be accessed like an attribute A full list of changes since the previous release is available [on GitHub](https://github.com/holoviz/param/compare/v1.4.0...v1.4.1). ## Version 1.4.0 (2016/07) * Added support for new [ParamNB](https://github.com/ioam/paramnb) project * Added new parameter types Action, FileSelector, and ListSelector A full list of changes since the previous release is available [on GitHub](https://github.com/holoviz/param/compare/v1.3.2...v1.4.0). ## Version 1.3.2 (2015/04) * Added Unicode support for param.String. * Minor bugfixes. A full list of changes since the previous release is available [on GitHub](https://github.com/holoviz/param/compare/v1.3.1...v1.3.2). ## Version 1.3.1 (2015/03) * Minor bugfix release to restore pre-1.3.0 script_repr behavior (accidentally changed in 1.3.0) and to fix issues with logging. * Param's logging interface now matches that of Python's logging module, making it simpler to use logging (see Python's logging module for details). Note therefore that Param's logging methods (a) no longer call functions that are passed as arguments (instead, Python's logging module does lazy string merges), and (b) no longer automatically combine strings passed as arguments (instead, Python's logging module supports string formatting). * Improved set_param() method, now allowing multiple parameters to be set easily via keyword arguments (as on initialization). A full list of changes since the previous release is available [on GitHub](https://github.com/holoviz/param/compare/v1.3.0...v1.3.1). ## Version 1.3.0 (2015/03) * Added 'allow_None' support to all Parameters. Any subclass of Parameter that checks types and/or values should be modified to add appropriate handling of allow_None. * Improved pretty printing (script_repr) of Parameterized instances, and made available via the pprint method. The script_repr name will be removed in a future release. * Added (reproducible) time-dependent random streams (numbergen.TimeAwareRandomState). * Added label and unit parameters to param.Time class. * Improved optional IPython extension. A full list of changes since the previous release is available [on GitHub](https://github.com/holoviz/param/compare/v1.2.1...v1.3.0). ## Version 1.2.1 (2014/06) * Minor bugfix release to fix issues with version when param is installed in a foreign git repository * Made version module optional * Improved ClassSelector and ParamOverrides A full list of changes since the previous release is available [on GitHub](https://github.com/holoviz/param/compare/v1.2.0...v1.2.1). ## Version 1.2.0 (2014/06) * Added support for Python 3 (thanks to Marco Elver). * Dropped support for Python 2.5. * Added version module. * Added optional numbergen package. A full list of changes since the previous release is available [on GitHub](https://github.com/holoviz/param/compare/v1.1.0...v1.2.0). ## Version 1.1.0 (2014/05) * Switched to Python's own logging module. * Improved support for time when using Dynamic parameters. * Optional extension for IPython users. A full list of changes since the previous release is available [on GitHub](https://github.com/holoviz/param/compare/v1.0.0...v1.1.0). ## Version 1.0.0 (2012/07) * First standalone release. ## Pre-1.0 (2003) * Param was originally developed as part of [Topographica](http://ioam.github.io/topographica/), and has been in heavy usage as part of that project since 2003. param-1.12.3/doc/roadmap.md000066400000000000000000000062251434441564000154220ustar00rootroot00000000000000# Roadmap 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. Currently scheduled plans: - Remove deprecated API and tag release 2.0. Deprecated API is noted in the source code and except in very rare cases has never been documented on the website, so removing these methods should only affect users of Param from 2020 or earlier. - 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/lumen for example usage). Other items that are not yet scheduled but would be great to have: - Integrate more fully with Python 3 language features like [type annotations](https://www.python.org/dev/peps/pep-0526) and [data classes](https://docs.python.org/3/library/dataclasses.html), e.g. to respect and validate against declared types without requiring an explicit `param.Parameter` declaration and potentially without inheriting from `param.Parameterized`, and to better support IDE type-checking features. - Integrate and interoperate more fully with other frameworks like Django models, Traitlets, attrs, Django models, Pydantic, 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. - 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. - 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, e.g. using Black. - 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. - Improve test coverage 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). param-1.12.3/doc/user_guide/000077500000000000000000000000001434441564000156035ustar00rootroot00000000000000param-1.12.3/doc/user_guide/index.md000066400000000000000000000035051434441564000172370ustar00rootroot00000000000000# User Guide This user guide provides detailed information about how to use Param, assuming you have worked through the Getting Started guide. - [Simplifying Codebases](./Simplifying_Codebases): How Param allows you to eliminate boilerplate and unsafe code - [Parameters](./Parameters): Using parameters (Class vs. instance parameters, setting defaults, etc.) - [Parameter Types](./Parameter_Types): Predefined Parameter classes available for your use - [Dependencies and Watchers](./Dependencies_and_Watchers): Expressing relationships between parameters and parameters or code, and triggering events - [Serialization and Persistence](./Serialization_and_Persistence): Saving the state of a Parameterized object to a text, script, or pickle file - [Outputs](./Outputs): Output types and connecting output to Parameter inputs - [Logging and Warnings](./Logging_and_Warnings): Logging, messaging, warning, and raising errors on Parameterized objects - [ParameterizedFunctions](./ParameterizedFunctions): Parameterized function objects, for configurable callables - [Dynamic Parameters](./Dynamic_Parameters): Using dynamic parameter values with and without Numbergen - [How Param Works](./How_Param_Works): Internal details, for Param developers and power users - [Using Param in GUIs](https://panel.holoviz.org/user_guide/Param.html): (external site) Using Param with Panel to make GUIs ```{toctree} --- hidden: true maxdepth: 2 --- Overview Simplifying Codebases Parameters Parameter Types Dependencies and Watchers Serialization and Persistence Outputs Logging and Warnings ParameterizedFunctions Dynamic Parameters How Param Works ``` param-1.12.3/examples/000077500000000000000000000000001434441564000145215ustar00rootroot00000000000000param-1.12.3/examples/Promo.ipynb000066400000000000000000000245131434441564000166650ustar00rootroot00000000000000{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Introduction" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Param is a library providing Parameters:**
\n", "
\n", "\n", "Python attributes extended to have features such as\n", "* type and range checking\n", "* dynamically generated values\n", "* documentation strings\n", "* default values\n", "* events\n", "
\n", "\n", "**Param enables you to write robust and powerful applications in just a few lines of code**.\n", "\n", "**Param is free, open source, small, and has no external dependencies**, so that it can easily be included as part of other projects." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Example: A Parameterized Class" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The [Pythagorean Theorem](https://en.wikipedia.org/wiki/Pythagorean_theorem) is one of the world's most famous equations.\n", "\n", "\n", "
\n", "\n", "**We will illustrate how powerful Param is** by building a model of the Pythagorean Theorem." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### **Pythagorean Theorem Class**" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import param, math, time" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "class PythagoreanTheorem(param.Parameterized):\n", " \"\"\"Model of the Pythagorean Theorem\"\"\"\n", "\n", " a = param.Number(default=0, bounds=(0,None), doc=\"Length of side a\")\n", " b = param.Number(default=0, bounds=(0,None), doc=\"Length of side b\")\n", " c = param.Number(default=0, bounds=(0,None), doc=\"Length of the hypotenuse c\",\n", " constant=True)\n", "\n", "\n", " def __init__(self, **params):\n", " super().__init__(**params) # Sets values a and b if provided in the params\n", " \n", " self._update_hypotenuse() # Sets the value c\n", "\n", "\n", " @param.depends(\"a\", \"b\", watch=True) # Triggers a run of the function whenever a or b is changed\n", " def _update_hypotenuse(self):\n", " \"\"\"Updates the length of the hypotenuse\"\"\"\n", " with param.edit_constant(self):\n", " self.c = math.sqrt(self.a**2+self.b**2)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### **Pythagorean Theorem Object**" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Lets try to use the model**" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "pythagoras = PythagoreanTheorem(a=3, b=4) # create an object with initial values for the parameters a and b\n", "pythagoras.c # print the result for c" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### **Using the Parameters**" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We will now **take a closer look** at what these few lines of code provide us:" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### **Param Provides Parameter Validation**" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# check admissible parameter values\n", "try:\n", " pythagoras1 = PythagoreanTheorem(a=-1, b=4)\n", "except Exception as ex:\n", " print(ex)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# check parameter types\n", "try:\n", " pythagoras2 = PythagoreanTheorem(a=\"length is 3\", b=4)\n", "except Exception as ex:\n", " print(ex)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Param contains a wide range of useful parameter types, including\n", "* `String`\n", "* `Integer`\n", "* `Float`\n", "* `Bool`\n", "* `DataFrame`" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### **Param Provides Constant Parameters**" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# constant values cannot be changed\n", "try:\n", " pythagoras.c = 3\n", "except Exception as ex:\n", " print(ex)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### **Param Provides Default Values**" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "print( f\"{pythagoras.param.a.name} = {pythagoras.param.a.default}\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### **Param Provides Documentation**" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "?pythagoras" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# more extensive documentation\n", "help(pythagoras)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### **Param Provides Events**" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "You can **use events to react to parameter changes.**" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We have already reacted to events by using the `@param.depends(\"a\", \"b\", watch=True)` annotation
\n", "$\\quad$ to react to `a` or `b` changing by updating the calculated hypotenuse.\n", "\n", "Here we will use the alternative **`param.watch`** to just watch for changes to the hypotenuse `c` and print the event raised." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def print_event(event):\n", " print(event, end='\\n\\n')\n", "\n", "watcher = pythagoras.param.watch(print_event, \"c\")" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "for _ in range(3):\n", " pythagoras.b += 1\n", " time.sleep(1)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We can also **stop watching** again:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "pythagoras.param.unwatch(watcher)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## **Param Makes it Easy to Create GUIs**" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "On top of param you can **quickly build interactive applications and graphical user interfaces.**" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The whole [HoloViz](https://holoviz.org) ecosystem is built in this way! \n", "\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "\n", "
" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Let's use **[Panel](https://panel.holoviz.org/) to illustrate how powerful this is.**" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import panel as pn\n", "pn.extension()" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "pn.Param(pythagoras)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Visit the Param Website" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Please visit [Param's website](https://param.holoviz.org) for more information** like official releases, installation instructions, documentation, and examples.\n", "\n", "And **join the community** on the [HoloViz Discourse](https://discourse.holoviz.org/)." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "[](https://discourse.holoviz.org/)" ] } ], "metadata": { "language_info": { "name": "python", "pygments_lexer": "ipython3" } }, "nbformat": 4, "nbformat_minor": 5 } param-1.12.3/examples/assets/000077500000000000000000000000001434441564000160235ustar00rootroot00000000000000param-1.12.3/examples/assets/logo_stacked.png000066400000000000000000000260211434441564000211700ustar00rootroot00000000000000PNG  IHDR,\r_ pHYs篶tEXtSoftwarewww.inkscape.org< IDATx{|\u;i3MS\ \Pw?u -+ 4-t\/(^ mR xCˢEPmIܓzI2g3y>$=o2s{1c1c1c1E|0;嚚d<>r(h (NsL}k3 uuSgn"kP} )+,e1@ᝄ|<l<[a1pܧ3@bta !x~6ա P0EJ#/:B)g1mp"Se*nXaPuCC"z|g1mRu w\ESB> ῀Zy)P5?cZy#2S!,҅5@#EhL iS^30|0ºžs)ͫ(Z30(=' Dp( t <;xEsk2fbw\aT?,TNØH1^+@')r sX7:+ۀjY>%_I|'HaIWՉXY b<"UX XT K` H"UX*R;1DE*}gH([|g0˾3G |'0(;xD2|G0LwTa]= B~/|wTa(EȌ)?a"WX#Cr;12IuW'gS\a]+͍ɸ^NIT"WXaLDw;~,TOb9q^/ӳ%bH<զ㌨D?=V S3yq ;I."]X _.{~ѻc^!^pv&v3fTqUp2P;1 ! Tz~[[@a)Qs}}.J k(EywN.\Y)R4 #Yya(VLS{{|P5پV[3 L|t|0fW"P[ww'p9Xkee Vt}G{sLVV߀1Y|~[k<+5d3 E^wc +n|(U%Tʋ:zoØʓmk PKUʋ{S36!nh=lf5p, )|梎Ę=<[8Fϡ{LT$X̘M|&yƄ k}&/| 䟁oQE :/+,Ons| rtqٖ|1f5ɸN8wW8վ3VXyp}LV ;.|4}֟bXYaAdx,'gm];1c|(:+1n/rToK^d b!SGux6@^>È|PT5;5{jwUPU3AТ?./J3>,ǘ_J &W/kg՗ȏ;1K䞑=;J."]XkG U^1{$.8caGdSFf,W6ƃ|9)U}U1*M6 vK*c ;sіMcBgAH90ҋVڴS <0h2a]"rK­?2 (^0Oyp8GVWN.mmϬ /O#DPU9G5!J ^V/oyd}6c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1Ș2r3mȨk F jD\ uۊdQCŹЎMۦn{1Eo7^?`&?Bވ{^xr(/"<%"o}pliI4䍠F=*ܥ#5]+|w c?hjQ!H~ꭁdWv/=w cRaWDVt60Ƙzw]r0Ƙr{VrLK]c?cɯaOا;Oa![*dϭz Ƙpca!ph)_3+j,Ƙea7;1&ύ&-9+cUNcU;(Z9Җ:w%\9Cݘ*30| a)q/}՜w Q"r00 rhŞFx퀠πla#&=qAff2ٷӃ@=PzH_~E'Fu3 ϣ\|֫O2!BZKTN=2Kd'yTE_TMvRHUAC nU_/kN)D竧?_'vpFwʑ i=Am% u%͊AC8 ${Y}XM( Q84 ss(J6eL&jjݝ xXU^Dܝͧ4aS;ud3^7dꗬ>Xc!t.Srqp_{ wJܑ;{sˀ✕_JTcKI+,E<=$j.[{L) IDATH62zu7?$d<VP= Pq~0Ok~2rzfk9җƏ)SYkҹOOeg:w9U 'vŭ9r,-cCJmOTD Chc򩮊g,GלzɺA"^9^wuuM-%ZorCoBD;z_C9e"32ø<^:U4 9HʑmF}>ό^sͩgt勜e\~ +r$K.nBbq˯efL,w0r̈EÈS澇_N٦ %wNa|'N!!157;b5NAaT(]18%nz˓1I/w\ af*9JS M9z$voi=1pӹ.N)&ؘꊬHU KDviH>W:wJ$hU֫י)MqNLFfRE*e59Lڗx R0Ew^szB;nm[~@O.chܝ(r@.#sUٿ Wwq1rA7-Q\k,$D(rE+.?S{A pZCTy׋a業db*Aj%К#HOF@z(kTIx?)J/Mn L8W4 1鏩^k[.V گ9ѴvWqR|d3r`E".,sٜ[m業CX MGG!FLTO5W^C<#ekΧ^:+%TX<ĎDh܍NSEx!2<U\R)Ĺ% -An,hf…'Yc|sfq@AcD%vDBB!KVhl> !cdL@wi?wٚ| $rN'|oΧ;_LX oKot=]`}])PX7s) k"@Ҟnւ+R6?n.EUX"rQ9LtĒ'ieDy MՒ3ʲ Y)KǢJrݛ氇]?֔POE\:OyԽS".VZnQwb#@{Xy)r=Zza=:pK,lIv x{5Z*'&"9lύtmha <+cU qJ44s5բ8’ڽ|neNoG|J4~ʌI! {`c9I2lUxfy s(’ggT(Apr1iy*K1T1 bƌI8%lU%w&O  鸔w9bA KW]Cmsh E<<9n٨~01R_D6<+d]h̹X-X], ~ #J颐nA':a@FF"R+1KUCU@UILޔ^F @BkR WUܝ׭MducH9"u3Gpn OhcԩNO4ūd XT@]sCY gs%7 A~:b/#)|%8[uE8'zT<\SQVV `Fädua)U"\Uw1 `QۓFs"HOݖ{VREY:1BHR$OeRs.DOIV͆ *ު~,<3HJo׌*'=$O0>SCJ)%UXela8%K/ r*(p:dckl@yLtTa£! tH&عS$@Dɦ++/pޥ9@ a}ibi\Ӣ"Ŝ<.Yya tO]];xL-@01WVQtJef,Kv5`uDB/x,Ѵf"w;G2N2A@x,. A # Kbw8~'p^(4Nb!}Jm8a@HZr*qFG۷EMΤnM, DŮ4VrE*Lޅ&Y{36WX@E(/Qʶ-$u9Lda:Ms\0Kd*\z;G)f^G,&J*}wRֵl"r.,R+jk+y^:r[7SLq_{끏&Sk~~q:S "꣈l6:ؾ=N6*H-ODϊہ{'[FD6 Us&4wO$U5u&<^h#ǃܧS9[RuikC<=LC9ѽ0m`|d/l#^E{Dt*[%%,Oʟ(۬Ԫi}Su.84Pp`JZ =z.m˨< xmM(@Qh׭ kIENDB`param-1.12.3/examples/assets/param-is-powerful.png000066400000000000000000002330101434441564000221020ustar00rootroot00000000000000PNG  IHDRXesRGBgAMA a pHYsodIDATx^ |U/y$a c@ B%J+V+ZV xzsW`=T ==ZEj[l*H SqgOyZS|޼6Y{gM hج_""""""""&X Q`!""""""",DDDDDDDDqb(N ʼn"""""""81BDDDDDDDs"F[Ghl97,ݽijG """""""CΉÙtt( 1Db񣡵~,@^O%"""""""Js"dUu.`т+~vDDDDDDDDsNXRL.tu)tm vAgW/ QU@f)IvilGǫ20O txՋMv{8"""""""D9',mBAnj[:?N6FK{7 e #فzT{,DDDDDDDDrNXNVF͙ZUMG&KD"]qzF/Yh\tqU["""""""Js"،>܅$8P܁ՄdDrLNT܉u.NFNgwkFDDDDDDDDɘȒ&===8vjj0)/ ͮn:>[:qTJq =`J.;ޭ(>c>T)SJbACk~LVЉFt[f.tL 2ҒDTM/ QΉ,TJO@rM5`eHRd+QFGU m]L-m]*DDDDDDDDq%Xd)*DMM-UWMm|dLC;V7B*C't&ajA*rDz٣i7rժx> R8Q۪Jd2"DSinV[3R1.+g;Q  Owwe D[~:;e$=hvIT5GVF.t>?faHu3;XUFWΜ9sXUGAFTWFjM${ jl jw%݋v8PWW궹g"""""""hs׫?dۋ{B^f&$aY-X7 LmsZ{0kr;r-m,BDDDDDDD1s\pjxX,cO3(IQd%'!U6+lV9}SM e>kS BX 1]l o~>Hr8p/LV=lv}%Kx~KՆ=>^R, ψEUU465w>*@dge"??)H ddWz2pJXvl6q8 x<~>$'ِJ8İ]U""""""""HKK w@ Ͻ yC}L8 JGnN6塠 f`B8Nފl@W^"U%""""""""1 ȗGo6nǃ7;:j/ZZ\pw"T`vYdPƢiWh(|>Ø,"Wm.;pom'NVF&K@:4h.)T镴T粔,r*F聞m @A"""""""֢S7F,2uM-8SSc'N)Ձ#l[ő@zZb„|̚9E3!++)HqePllZZTh҄KjS@S[ZTM"""""""]cc#wg)" ݋n*TT@MmjwKUR,!JNN6fL+D8yHOO3ɡjCJrS}?YbFNe#zUoH܅_Up8)F&GX_ {FzeՃ=*=+Irj Hn:7|b?8=%LUA-"""""""jEbV-Ư2݃&T#p*Vhd`\^_q5^ܜ,6\׃35uȌS0hǎB]}nʵ3oUi=0cUf8<>=ՈvUD;QAOVm 7ۣ rPT#ފ:?IYpv&"""""""Oԇ`ExxUWV5xM \htu#)%.%K _[;ķxū+.% 9Eٞ (ƒOTFUU"t޷ZTAVߑΌQ'Qd(3T+^N>2Dk{7ZՌ$44.iIpجb83& '#EV&e` i7l5S{Gm-õ ͽh$e"5;܋sW}]%M_݂/+i6z2`  VHcQXi< xIJVJc-ǯ/7rd;+eVPrd5$'4Sg% /*@ d,U yCJJJKt!}F>SM&W7j;T![P~A_>9\#^H=>>\ǚQQۉ. r.Q jþ xvd{zTgjU{2 p2Zb$硖ӭ/txբ*lMZ3&rmih?>;=^F&䦩Ȫ@SJ]Ud YE&1YKr""""""",4il?&^kTAdPU"m"COv/_@Uq >tpDkLL4A+"RXd`@6zmW}d?gCU js,WͳSQyl݆=%JqZZo⃝Lm$c)#a&U"?C+42"Pq:T^D%Ld lm2F^Ó.۴1UH|WdoCSr^rr[|og]3IOu SM9r iN;GEIjf#73Y "0220gL}Pj""X.|'}?. " ~|^X1(9DMN 2?oGUՙK{h^UEHnd)99Y$ŦЉU Q]] TANZ)hYDHdHV Wd+2#/=%IQ)VrZ,9cI6mFDDDDDDDgS,! 7SeӍS5-,SMWAYaaJAZ;z E3`J !( gj?_߼>#q>]Ahti|wv3 'O "T?}1bm Jii)r?^|(SI^G+TIV휤%'|:{<8خ*ݽN@dW٩Hs-X%L% qMɸdxf$O }%<"%;699xKUs)@ZmYUeڄ,ÒͮuA a3Ybdh1Kfwo/zܽ,m2XQ[[٫Ѫ /(%DŽjItQK#=vBUE2#8-Z[o3gLŔIfpX,9c8BnKKlO%7S}.]nv_fU/Sg U 'KWmȿ31!WQ K2% 'DDDDDDDe#`XdU23-HOvB2PaHtN\85)N{_nU- K/_Kv<2v!ЈD`NBGg>Ny4qU23q왘{,}*-!dFV:~*bU.["MU%;; `AZ\hhlVe5iʎA6N+,Uu.vdYDN)PIr%m}/YeH9&lENKgшX$U2eR.rӝhpg!3M<)\].EhDֺ2`1ix81 |L/_'&'KlV뚕qZfY60(X!56=5L'qB8^ 4HFFf.ͭZ6M~vj NE!PU"4*znFS[d{/2#Q49eJ""""""", $';00>_5Y)m6 6+fNļ 0 d U {,J ,#3xwq?}.;7SPzZ*«89Nxw;;w@􊔙 A6rKeI61UMiP JDˬɹ:!KJ""""""":XERYJFjU5ƚ1] g"͢ hjB2a.1(15sfT 'M'M EZI=gXwa|:NR12-s2.###dInԉq]4%IQX~+_7(fl 7T'KA I6IKME'XIһy6wtyzA[3])Q_2!a,T% U%ed&/+#-EWS""""""Ϻ H?;#Eup y)HKR僑L3C'zi)HOK Ik[[êȶZNU 8x1 eG6+N_Q[4?9,=UuF5kHMIQmØ/ZAk4X~d#N-rAv{q ͮnUMH.Gdcr_Ų_>KFIc Ix XrLL-JfZamL2 )N} ٍ]eU2@zơO+p>&'; 'OP-H,y}8}V}W6;}z{Z[{֮9Սʓ>3Lthdz&"Yd Y%+Xg"\ },.R-ƋnԊd' &bfaR0>7]=|s鎑 K̽h6&N(h<[:TéS=xͷqt>F~7?/U"znwAd>PBS8q߁7MwmUfLo%}=ȶYgvQp[STգ.7Z{V[۵gݨXdI$ U٥LP"GTDܒAXduwϿ Nק#ɠ̂/BFf*3pj ~%X{|ŕb9 ,;-x7x嵷P}FVgVt\҈ydɕdV*@U8^V%'&ϯz jkh:{=Ww_>_OEDDDDDD5jp/hR)IvfZ7 7#G6RpEXz"8Ij}>::PW߈35hhlV%RdPD` _´*"]4]U䴲ܦVٙX^r%^M##w5b9Y-Iɓ&`Wai)_%e|2RT㳲ࡦ{(Glw ne,:puի^=gݨX$dD$2t`T)K8"עDʞ]_ĕK&Z@@A$=uquK +S#U6v{㗗@l3锥JljYHOäq+h'{}j ^E3a-7s&NSmtv6Ul4׮_l,=$xHjX|WolGDDDDDDYg^s{Ӯ8cR!McԐ/YY3R1tvuieeɸb} -Ȓ5_w/^*"0.kR$S1:20"_rr:Y*FC~.%W\VQ[OReI^!]I~oXh|m ]4YjQ ަ9z$DiQ$!S|?'#R:{tX_L n"DDDDDDDDq """""""81BDDDDDDD'X Q`!""""""",DDDDDDDDqb(N ʼn"""""""81BDDDDDDD'˧~Їh,  Q,]]] x`!"""""""3BDDDDDDDK@Їh_""""""""&X Q`!""""""",DDDDDDDDqb(N ʼn"""""""81BDDDDDDD'X Q`!""""""",DDDDDDDDqb(N ʼn"""""""81BDDDDDDD'X Q`!""""""",DDDDDDDDqb(N ʼn"""""""81BDDDDDDD'X Q`!""""""",DDDDDDDDqb(N ʼn"""""""81BDDDDDDD'X Q`!""""""",DDDDDDDDqb(N ʼn"""""""81BDDDDDDD'X Q`!"""""""XʰE} b*{Լ^WƛoFYerUw]arUo?!_Ok,x+4xPeq ~wz]yଓ:c\kLZt~lfaskϭǽ7@q)ƊJQѢOGD8ܿ[\uoTgX).^5pٴJ1]K܃<Xq.ܶk{tyeXFl;>Zۊo>U7/G6ާ'CFŖuXX>,0P}dwu"ԧv橕FCSV4exsSf<,+yl9GK ԧᕇC.}Wx-bC]`\J|lT,o֬Y?8~8z!<Dt.c%H6>,BD4\'*ϷBJTVz>(^VcW׫DPu\'Ɯc'I\Vnda*<|E'<*<{٠ bb}h|/5sN"ŘBn1]H]c;~} ޸kpC"ڮg6D:Pa[|}?FU/kW]'Fnq}YvAb:Ж݋K"Wno,r~_CDt.:+7aŕ[ܰ'?>n'Ο(YW7'k׈6 z(vAl7w\Y{8x"Wg -|'RC\ڃ8|{ UYܻd |;ۅ6i+n]MӷN]a6HL4'2+Il}\;;zIy%_DɼNLInyykkADnTϦy^EEp"opxV~R<4d>nD~v+ lśsrЇeq4}8׮l-C^YU_Yʟ%9KґŽWtk-y “ѼG>޺pMg7EӶ#}vDDѿ˭| Ʀ85xxoZ;nJ~7<t̅m?|`_{J'CxZĖmxOpX?1"Wq׽5L_1 ؁we hFwDHc7Cöj}Ę,zFݟ2lH5ȶ 8`#Ok?gies܈5bYû!-X14\nԇv`wP{V}߶r}HWoa>ȼWu7NߜvXQK[ߊ|^sxWrz~׳ѯrwp؉%n}ts v-l }31o4яox^T #runs$(]nzڊ7h֟kLkR*"QN|3tS?%Bۀ[ikpɲ>$X5u6'M+9W-E"Cm d׿ѹh,e[%K8h4jxL\]6˅*umTeĻhq«j_߃l}JeC f4U k˫zl5]f}kJbӷfh>>8Sp._}kJ̿ =צ){m}4oa~nMιk[eQRWgL!Ÿ;b j,quzmPy-# ܴ\Klb}d(_O/ugкZ<`%!p(udbX~^,öLm{^ـk8׋{l47gyodhP}}E~Zoneo P "sX>V>݋ =ɿasgkၥ?s[صئ!]Ϣ4>T8יL nz[0|UU1&{B~L O޾ k~k,~9")F@%Zq5\455)V\2NQC{&8=_]؊aig67L(]2~pS_)SBވTCe)k1tw"JNʯqr&<'ncTsjwHFb$>6PLs-KkC#Vtt Mq7f~17kV-ځ7_ӇC98lNϟr_._0:#᩻űGk Q3m{3狇T{ܫ(01|!v6_B}}E~uۼC 9\UGVև$S'\.!aݍAC=ڧj9DmwsyfJKCOd)f]|S5\|Le^E#KxOW*z)eOnƽ׬  +ԃ]hpES=KA\zs4E`Dc\&6ي.U\t'xqd2\򗟇Vz)w-T7WZiiׇX~Fi+qOǖCҜZj ] g-Շt_)&j֜_Gapܐoي'Ã~Aqc7B[F/eN4?Ԅ*GK%+|d[nl8bF#OjīōتH{4m P+q>Ї39 _[*غ .HFo%MYQs~J][טc?&%z@Dڶ!a_iɔTiLxew}X]5H\$q)BLa% g.) gW %ףa[ϩ`BP`ʯa"g>4V[x z0k{ͽc慞.sBaSn^\FC=H#C`!yΌuǃJGJK])aw'ӵ; CfʺѪt͟;)"f +D `'•~Z;XyM1z`xlђ N.BܮT*^hps9ehu.4Tale[f$i2vF*~tWRqˢ\vqpQbb,ч0~Hw.X懎7xޣ*s&,A%>Qi"8K[Fyi _}P<B_;,G>6Ӄ%};"yo>qX*_c",xl*Yϕ~m !F"Ee~Bq{E˯f q5o耑&Bҩ 9+`HfG_Sνq6T$LD:S-[lwHɔKVcUSTztӺW/F"/gs3'kN}F*ozl\q.b<=Ѽ|72@y)Da!9?oU=HˋKSE6~C Rc9 ĺCT6 r7$KƘ~rVF(3"-Ɗ.h|x6Rm?Zu/0zWtFв+YXq:lVy=\y>+]?k\/ Z<`uՋɍtw \9dbe+T;, ]j ^.+^}!V'NOVΌ_X\/YDQr1EŽwvomg߰Yg>mp00~uigbpHLZo)۰qsʍM0l ۢ;J߽%󧄕PX:}>4TndE;QkZq." wicݸU TFݦn~n = Za3" 3'_>K̢-(;^|o_" l|QZF8 M<|˦eW "ټXb[ [anɢѼ7Z!r.MFUU q>BH7Rc$>4c\g佄z7d| >A=Y:CɒA{K5H.s"qbp g'b̟_bBOV tc7r\7Y/KZz\[Ӽ.D sicޔYm{ qD<\WBp.07YWzMӋ~yusI] /hO|Zw)AT5.iJEM&sNP6{kvT3EXzS_b;W^b ݑyr4w:C7g,JچP_Tms8 ZalYyoy݊wſ/oz+iΝ7 ylt,-[XO;Pzh迭p܁o)V_v#q>7Y͖rlZ3R) -w)y3rBuXFĹk~bɨގޫһ2yz\2h8?n {r nsnj]7[3=۱ai?,Q lxn+*>;`ך(}H/[wcǫצUƷ\CA_oŎXzl5ooWv<h*+Q!cc܌rOZoᦾކ&&!S` "n;yY"SQqLnHsOUׇ-y()'.%1<\]'Gv T2ڑX'{*Six žk~ؓs^3KX |)il=~>jp#t~#rV|W&y~?,yTB\z)T%OuRO/gaS/DkY7r3ݬGk낏)Ə~>ۏbFFO/(?nZ?6z GQ_}UUh ?FДUO᥇=tD\ ^u6db+XL+[ٺ||+#Vm}H,Odl)=|J/օHZMx*jw,q}0k2Uo#_%rĹב99sB<07J[MĹl *TuD.EBc:sV7aT9+Oaqn_&;sP|>|)))11Bt~9 :1ߡl6lX1?t0p6X ,էkcw+ހ- R\}鶾IsSoowzHIk׊؉KK"zQ*{>~ߔ_,HZ=oNuۗd^~Cyx9Yb&6bK~vI1ȴq-(6\?;Ń[~~V^5R;W۰*M02lBPAAi͏##+LY$/C~':KH c%i' a[zj E[Tu'}wp"_<(M&2z,Oe>'dbHu~bK`j,>-(ߩ劇sz.W9՝i3bÿ76`Evf;W ~g}Δ4á?9QZUuX}Hp9`Sg/C+㾯. 5!e;E^”_uǢvп!%AqC$K̘1Cs\!:X>LDDaC*lk<|%/yM eUu:⟖bc9`J{D׾={$aժUjY(BDDtV̍㦫Ky(!d!pbܹ ,DDDhŽҲոTņ=[`RSSqe+#<^xvbp<*BDDb]"=ZUo8qa ] ` ""Q1)bͬ:>b#"" qa89%`!"""""""8 Q`!""""""",DDDDDDDDqb(N )5й/?CDDDDDDDDc """""""81BDDDDDDD'X Q`!""""""",DDDDDDDDqn?"|Y3 4QFf1^];2c]\t[F>JhxZ%:,h?#3=xʃ˻Hԇg%\?[w6v|Xіa)> QF'9YU1^΢#V>OUb!KS7{QbFn>1+ºMy׎,Aj; mAoV$y$Xd;UFH郲硘5Qˏ |8TT/5XO盥mԂrZQTK@Ї&|VȠ Q,N!"""""""s,DDDDDDDDqb(N ʼn"""""""81BDDDDDDDvLDDDDDDDt>b """""""81BDDDDDDD'X Q`!""""""",DDDDDDDDqb(N ʼn"""""""81BDDDDDDD'K@Ї^""""""+((ЇFFGG|>>flHOOǜ?`!""""""kjj "DDDDDDDt8+ҹ Q`!""""""",DDDDDDDDqb(N ʼn"""""""8Y>LDDDDDDDcT}}>X| pZZoxC`!"""""":O=Sxqqtvvꫯ?b>}Z 'v&L1BDDDDDDt裏! . 444^Q,`!"""""":A\yѡX0BDDDDDDtkmmŖ-[?YWFA2X3ٰA˞{セ_ ,DDDDDDDݻw_E6i$\q;y`~W^yX""""""?~2ow}jbg g޽{g[o6M'""""""meǎӟ'g^`.1s3`9| c}sŋwDDDDDDDc[<ٶl6׾5,ZH8 cdQÉ'wRRRzjdeecƮXJKKEoe]K,XݪhӁ1#@ƮX~ml߾]+_ .]K1r5r+wpER0DDDDDDDE/yD+s.rQ466'%"""""":|K_UW]D1KK/>O>^Ңa7n>&_y/":P7f^҉mO{fiC57D:pŭ'^3.A y&_2j@җV9mxn1_q)t&{+p@3ߗ ai+ Ghdhig@[ᅣzKGSp /zޫ:F:FZ1,|NL:<vƹL?;tncDt^ԇbՅcǎvJ/r\}'#+-;,2⋪h Uel9//OV0Ydj̻~kT12sawwJM>||GL<0\|XwPsD҇GU L]ޗV,q>lD,{D-",5'ohX؇? `t1" SGsNǾ 8^s\s5Xv-^5L#gXd۶mSQD do]dBC4ԣY5e|uР &~ 2]F)D# """"ӘEHr\^^_0[/c"E9s[lBjŤfϞYHFXYL]ӏo7V}Ȣ̿ǾXdͥ,2PQӻL4#KD,%>|08Wu¶M[<2t W_gA ,QeOCgܼ5CJ I~ao PBX'mmb}wV<AhJ{%h&274ޮB,X¾?yiSc?0 >*du`=He헁9`*/^=F[]8gjmnJ̞="h;>~6\J[ۣi\ ;B<Pw܁nэC j/Dl=73m/+jiӈtAvj3sbF-ݪ1ǘCLk#.E*Ez7i߾Vю}&>|"Xtm0B<=b1aMhַ֧O-zZ9O~izAyBR-A˫/觧%nT_ jeo>ͫ|L=۱q_kls8)˺jEM7S{-OrEЗnMPit3ݬkYLOp}>.e,{+:My\q x~X#r΃My؄J;;9FZp~WAM; }iafsJر/PYZj) s*/8s=E\}%w2BK@Iq /Qݾ|)_3K^o>i(^/\Ȝ!OS&bڏrXoy-t.+_? 2"A69(~ila,cHww>xӧOw],^v];F_1oF&jF8eCɦsLX͌y_Ɔ߆G5t-=ALe]Sڎu{e&vegv_o[V~2k4 M>JTTc+D"<` hhr6/1Wӭz2b&f!wtkxc̜ JpT1rATWUt-]Zu)cͿjӸ]< K۬:Ųb ŔnBUa30J\j(*RyEH'uY a>E[ xͳ?hY2WKصbX 19$b;>o:vÝ<1 =Ct1ƅw.d?,VKGCѶM| sf)SwiӦC Ȭ\%f E<>~Poqyn~'xy7A2i[c3Sq%\ƹ[ hAa /rPvU:My|t5KܵbD9$crHe't7a\y-q/DDD7,MMMغu.lvҤI3gzK/TWԸV;+F<~G"T0A{JEr$ĴyTP…}/0^35b<FTr#PZ/>}=2M*vv .6zOaA . i0>? 7Ճd$2Hwp+zWe N]CYO?ވ3]wL'FPJ„>'B(Lsĉ`+Töַ?83ի,ыm;05-/}B%F"QrB^#RugԶk8F9-D$|%Tƶ-[-UuDp%RBb4KQhiaڏbAL(" G~>8Λ$M$D߿?6oތ={0a>6f0xP@4|DZvGBv YODz %Kj{1zbBQ5h@.ѫz3ˤX/YT>iB:^"4,x!OhLXdvttW2oCmmG+v~hLTKv#9zZoW<@57pc~CvSԧ7QMu}П^%U "McߎV"H4=l󑍣VZD~_zE=\{_7[zteFUpŘaj 1) Vէ'Ǩq&92UUgDtq:S] ՗\?1MsUUwgt< Ms,7].&.eu%H7*"O=ޭe7Bhݒŧbzd)MBMbI7w}cte)8 +Qreli`-; .6u]~y>gFƼfowP,燰cYXƜ =c0m:. =t1]c?|we5WǾ8ZeWN8) RXvW~0Odw +]Íܵ"dνa}tT\?Իa>9$I@uyv.t@_ ~~2<^FWW~rtޔ]A}:!ޑeq\N 7,[t5NDD4<5_]bW.rh%O~7sz""2>dQ*Btr"vm1\W/Z#P(1Ӌ… Đ>Q4F4UmLT2l۶ ;`FŘ թnZӧcp7njh4bϞ=~&LPՈ̙!"""""""\r_DwwzK.$m%ʘ Hػw#+'DDDDDDDDcϘ KL7DDDDDDDD*X Q`!""""""",DDDDDDDDqb(N ʼn"""""""81BDDDDDDD'X Q,A&""""""1۷oǸqp- 99Y$11o<}l:u :<zp8p 7`ԩX%\/| ;ͫ'OiӦᦛnizzz/QOMMŭފ,~a"""""""pq8}4.B~̙3QPPE3,XիW+@ww7JKK޷<9ݻヒ+WkĉieEM6oތ6|IJe|O1{lJZZoUUU銊V""""""":L<*8B0F,Asc8$kǎ4iR_W9r^]ti_IW7>>#XJJJ߽{c,DDDDDDD\UZiʔ)ꯙO'{&Ydƍx7>B;I#ĉ\Y}h֬Y'ӧre{XINޮ[ZZT\GW+`!"""""":8NUe(1bflW/>˞={(]]]d??ibmvu#8jjj҇dH~WOdƘo(L"xn( 1d5sc_FneC2Panܶ,bPƕ?sL_Cr^tLh÷dK/ . [ɪErBL_V7Kݲ!""""""s9RXX?7x#*++Ky^{E##K˜{NL2&ZU1F;$F5'N`Ҙ_ Հdô\6KHȶYXAYulN5"JyTh[%``dh-PyyyyphpEdPa͜M d5!i߾}oB5$ +ؒ`?^O'6̩[b$X`G;H͖ =M3@q]SXmW _(IcR؃>Dx"ߟ QOXt3X;n66AI>2!QN ]SM! B׮'F yl^G ^R-7Iv`|1}F!\bi@qpj> l\bSi!'kweUrp^E_vdr} ѹt7 (7 >/`O?~{>l2ԫgm91 'CNv.TbtMͨB >XVau +/fM`\ؿӫ+s?^UmdPimX='-!1j|]phBTor# U =XFj&~姸gX| Cz/!""".B /lj*mIJ~N)^6>MiN>Y'%߹?jU%W6&YsLAy32S d@I<ʆ4|?9 p5j%%$\IFp{h@cȱu#/ 11~ ;X3pdΜ:hi<.W=z:tC5-cs )%91iY8'[jkWj 4~*8 <]Ѣ$IxZ["L鞨%g"" W`Q,3!K־G' U""""M$15:YP&Ut<.{k>cb;hs2a.3Wq$.4Z=Iq4F-u[=؜X@R6LdDϖ^_ ;Pc̘׋ͭ8]]iI:}*\)Ihzm)pfcrD̟? 5'bo]jV妿tH_P#fpϽeF5+LDm5z[`u؍WL%i40[W ,mD}%ciSܬ85x%^h,JWUv2CJH^2~>y[LT}P4}p FȓVBEG#kt,~oﮇ%orI9Mvlϟ 5{=>GU,t22hik3ՉAь _~D Kr S,Vk2l60))Ib(oxbuPn},pߣU DL,Ԇje}m g0RəEheׇ*DZ,ֻ/ &[@QXƬ#\M߱/{NX=cnԉlpVUzABBUci5%ڨX|o⩇UVHQ͉^kqE*zavcI`d0*TCsGµ+p3`u$ NX$9xH {ۦcX-(#,B/5ݢ%F4FEª)UP[Tz&d:L4!"""R0MՄ+Qzdne)EK0̍FBD4|#`s, s|<`K - uMxk6ɓ](~| 'ϴ"DXwƦ6ǮJXô2a4 BG1ZjO,FPvQLJ ՘lVbvxhV/YWr%(T_֗Q]""""; sS#U꛵Fg+q/C"+ZV*&Rl.3Q F43K~/k"[a%`AB̛W%ĥh?tu鬰X;ۃ:1A$>jXzXv\8;5 g-6i8~dl!GFG&jMhu&.B.#<lʞgѽsBQG]Lf}q}71O+uޓPFu=_zڏ!"""1A&ooA(UU+LZ⥵"+X cQ;k+D4|#` tWsjOSn>_/,.X^4;RpќX|xx\z-^~7sΟ d!'']|? gep݊+-c-+?^Mը:5Ȱ<*aT k0ԫP+x:G ,L٫PP#r=y&]X$HӆMUO abY?ڵ4%ϖT Jʩ}^    xTW7+kMz\@sK'</,+T$e %g,<^QЅhjF < z :YgIvvjoզ!"""""":%+VP8~ c 6o'5m۶g/ Όu#`wC U,yTShoFm}-N:#!KaM/DC 6k݅ST߀i./ 3g"@b e[/6{󡱥]]p5נ=$;!}+~ >}ZU߉ |K/UHbwYY<G*#%%EFk|VXX}z V pg^x}46ajA}} .,]<6 4h󳐙$G l6?^t"=O Â.Kqbm, 9 pzPw8|&4ƉcbAA2&""""""̹Kk.1AO>'N02ؼe))##CU#:`Ix%] V~,c"VE|dgd#/7 Y)H1Mvv7~-XptU{=HJNŋƉSgP`x+&eYi߫z[e"lII:QDGw\E,79֢| 8}LlTeٲep(#=sAՈd.J%O-z{xzU*V{ T#?gqD l8~twtcB$'ɸ|lx6̜1陰[p/^>e`U#مLkLJE2h;z1$1}vxsTtt!#%NK#VӍ!""""""hyA_hmއƺX:OA&t>76+)Ex7q)(K y8iCH+kVfGbCo;vǮ}GbHLCWW+O㴫nKOi)b}Esk7!- % ֕XȪEdQ*A` KlV/,{js7ϟo6}ݝHIv͍ SLDowj*¼4d hՂz\ho3mtKK/GF|a})L,XD _|XHKπCrJ21t4>5DDDDDDDI/`NCNd@eb-8wK/[>/q}$ydk.qb'EHOM/V_/:,ZĆt`<̟7_aqX_5Zm<̞:>'GSpQ_s~DDDDDDDE,ʘle߾}jXV?-!3Z_=좹>W <pw5}z 2zLEFz6v&ƞ:#8SQ}r4רt=^|Ng#%ʼnd$;l}b~^UH^1] pi>?X[CDDDDDDD:u*aȆp6Wo?C׿~~vlzXi Mnm''Py(\NAV^:@KK=uc\\7nd"73͜Xg>X~X<ըz{ ̙3^1nK9spMt 8R{uQ(|5p/bxU\` pB>k ;;:m .^6h צXШHx fwN4ڱt/7Pv)țqHNI՞.76v;$˝;NwV8S`q8aMr %7Y#-I`Ö́lb;aYlIpL3Utjj1}V6jgjESSX4d&g;ILBRy 56n!{d̸h2&Nͅ3- @OvAg >ݝ$&yKQ8i:\6`Foo~qnz;a xŴ1YE"Ng R'593y^ ÁOL)u";:=HM6( dgݞB<>8TYEh;^r5f̜-O5`ɣ(^8mE/ལxR?)<,DV>*d+R4atja^X6>nĠK 9) ~IN6}/|zz|/"8m!"""""""E,Sdς6iXXz|v@oO.epD̘>N1.@ >K:VX-HF80kz^'is8`ZŸ].5UÅ& NW  ׍4OgS1~|^X,~tvwÙ1] RFN5tg\8S߅+ƑtxS1aTnbg'OUj/,mBO[\xzϙ6]be pBŞݿAKC7d*6ջ3 HD]HvQW rpŘ0a:R)p{}~A1~+߃\65#hob՞ gJ&$4ɲ1[]pu Z*aЙv6mv^Vx>1>/շhצX cbϞ=6mn&}l89]yy9nL:U8r۷o[1*N<G_Fcc>K/|A ]^ibe[/ S̩:dde)TW7bq=4;zF;~dM3ىvWlIIE GzZXV2Ulي3pIțՉ=XHν_@S`ESS٩- l . zKǎX|9nF\q|-OW:q+:}Ӵ^Pc'iΜ9 /j\ʕ+qM#i3"U ]t=:@o[A8 T+m'qy):́iE8N`?o5u'% |bX̧Wk%aOU6T%Z,Q{O'>9!y!9U|p. ~]2 +DDDDDDtޛ1cU 3N!F\ l̛7O @ɉ'{ "9Y ]~*RVVx<\uU}-r *#_23iҤ/4GQՈX OWV} -5CE3f NÔ8U Yi} ΰխ]hil&ʂ^NG2V;VހSc3>Wd0n -QAVj2SYZ,W&"""""":? Nc4}ZZ*%Jy'^O?4N:}㥗^$!7VOjiiQe)YmPw{#x"$sO*Lrی5kfj ՈXiK܂T\x 'Xٌ/m=7k<\DG]m;sUa?"烯 VUV?<h9S׊OIS}JXNl1sɩIb|ٙDVV xu_ז%01Ѫ q,[omyϣ?"1!999 T͌I4rdiv5.##C %9C5i['p8{qqASc 2ibąmˇ]u k8u6?rnu&^ v$%{)utڇ9pqQ\$6rӅb$"""""":, b&$Ԉ$2KqqqX)ؐB.SߕU'cUk;F~&7`qؓqхpd_-nɣ혞[_ۏ@]hou Ւ ]..*{q(Z+qXcovԝlő :ۇ|X4%|%q܅vR4}-HVՄ*++|vھ^wd㳲 +,fFɑضm*t!LVG%mƍk)` ;-_.)B-aO zq@7z@ ߯^E&{`%&{Vp8Xl5f^y'lpHcTڷo* NYe8@dp_VHd)H%W͍`P~~~zUüwВ+r: -"]P̨X EW+^f ׅܤ^d;(H`֤<̛=[$8tuNƞp:asT˾@ ->PߖV;갡ۉ taK[˾tЀwb(>MePT5[5ٵEEkV{XcG1A"ZMֈر(db8Mh"a{L]8ztb6#}-†]1!N,0ڨѷallAa7c"<ߑHhB=iEb#10#r2BOhPF5F$K`Bd0J lsENnᆨ%a {)裏԰ш DYC6xgϞ5}p9kə1y- t:ju֎tnⅷ>~KY'Mpt6fƒhkSv5\ً~?2»^\[*+QY klahVbiU~l):gW>VZχöq:oVOF<}6gcʮX>bڅ-jut h̒%>dEv4 `l{hE>򕯨/ 9Y:EV1gd!YRE,"KLEA25Y(t>lWVC%]x\YjhwjŐ?mf/ | ܹl&dksᢋ3jknt1i \st?d DM?WB΄Rhu#fM;7VfY^" m si_^"_r)SF O+Y67ez$͟ 7"!ob-:ng[`۰;ض@.,bm?;`KA~xؒqo9bܼ[Yq\28nQ\qdNд0煰m ^:61~}rlסGqYϰ'6wLy'sz|bwH:b9%e}U:m-y}3ym8毶Kߖm44߰P2WcZnTy9犘ΛA1Ew4tR߿m*ź+lð,oh{:$oEf g|GۖhO6mX&t^@=#ݢ\ctaRuf<޷,4ӄO!yE-c6?,i~gsI+B~%"MJߝP$̐?L1_>^ѾwKw F-5ۘ3XKwu7o 7_"nc0t,5X>m0_w4vNiY 4F=}%bho1b}i%t}?Fƴ]Ay-tt S4ƙ=Aibgy_mߗOQDZ~ '!8ÏU߶}OǦi1lce_G_Gm|_?CQQS2ma&Lb,r C:mc۩9J:G"WFט"lvFUC_;a> 7d +puH_ V*X?/yl$ A nf@h4\~T#SuU9.76WW/lm`k y׫u+dH 6 Juz-Cts CFa|\}ϫĴQVW喠(q)Ok<Ÿn%b)v=Dt׀Gğӎ>}>(?rT,])OߴA]oBN!MMN+ZYXPG@)'Ӽz}ilO0nc OCS$k'a9ھB0m,v Ѐy^k. 51PS 5౟jP!;Rzp>< z<KBKcY Nj2XesfyZ:Ãxf+e, ,g~s2u ~QyC_kU6_׾h(v,i!(d(qScqo* 5.;>XoW;{[DnVq4Jq(f}S>dI̾-cŚ'JYl~ x>FۼW<uPI3Ct}{ 7,86Tr|[!hDŽpҝR,XΛ^&z0txC7ct!T߰ x]A1ݿF+j `K KocZ[_Of#sDDf E/ A-+nxSjPUEыQ_oV7#MR+&;ы_z%oCi}8CS_Ć/X~ʪ:EP 0UP &\bL kuXZCVVQfجi_Dê0kJ4*LPIJlcyp ׃T\$rOX# <A8cLM!/|> ֢˰*6O~8E>#|WQNm`hy@9shDcX0KH2Vca1rn*4ZqgL14~W^jwnnvCn_}F+OM*6~ {( ad:kLqկ,n~ӣLŃ:oō!8h0LZԕiH0Mk"Jоkulpc4<Yua4=ВnL%]r+j/6lPļC˾hOcFgw`25hsu15 ^Ϋί^rײߘ[0:ED; ӂԗ3g3ҬX,ZȎ-d>-.px)QIo0Gaƴ[v͹ b+Jq:I$0g5*ΗynŪK2_ GCԣr<:}Nl=њyylAMS:у]Yd\xcԱ -_?#v D9*mhal%i_cRK_!GiMGF =H-1Fhg"rMSƥuV$^?Wx9Df f}s 6]Ԟ#\P@{('5X ω;Ϯv.k(R{k@ˌ)1$ğOΕ~5&"Xj]llAk;x2=K4( fyr<~Qˈ>rPǾΕi Qf:v6;}z%P BorpTN-ӯbݫd @gO0q`pL,h ,-%p8XZ˾dG\(V6͊l쬘UGnCۂ|x".^X5K^Kɟ'Ei#CYbxu5C:7g*h46%-OZvwjmʁe?1{w|& *Y!F/V-q0@WbqeiKyZG@g'k&b [)FWuYgn~^$Ԭ8Ÿ=bK7XptEbm/5ҝwu|Ӳ1*LH&'oa ?gG9n[N-irx~89gGŲ>XPnDv҅n%']˫G>bteTWCbnB\\/Y l<3 %'JVtoses'/YJQ9^]UܟHyw멻|ƶF<3|R[O5\A9 ;]۹6?{BgJN_$kJ2:.΅\W]v2㡮7S}N O9X-]5OE {sH2펛]W}4T s:~\zoOLvlP] .^8zt<{͟7Cky;N«o]^]~s Nޫ5WD͑וO_cβ:x(4ͽL ӵɯ6?{HP.<^nydrZw\Hӵl#fy3圧{.9;'&nvuoSeו?&_~~*puliލl~{b6..)݌}6Q63j.e$R.P ˦$1j;I 9ǺitnR#duz: ubR^n{2O~_?T@K @ŗBܼg}Q/y8<qy-Ģ8 ^3Om3BIfjSܖ->{ZJ#e:Tgev '{o 8]_égHN𔗶Lw\W;Hgfz41_]ɍBp˳)gHKeK.;8bnq5x^ؾ_m9qlBAEʛg#!N)ؽ2{Ege}n^%m{Uڕ3cZZެ;׮Y@[ތN:Ŷx4awYʸ&aYS7\e]k8kY[)ZSotUt,eXK)bLGن>%g":Kz~-">G#%O{sn[NuR{(H}1X{nfwtR_>ryjKi BRʽ3Ƕmk}gmң{ HHPZv|u UK~5P,YPgdƒ5޼u(PF:p^KbtpsXٗv$TvăǼNqzE1q+g s ډ:i$r)F;xʔ#~2vh8v mNƖvLeguIA90~'kΕPG@&:!麔c +[Q33A9HǩumcEb9r{(J=Jp{欔zMl16@ޭH9SHצYqugȟ+vǐg^ZR3~'^RedGn̽0<2*);˚|j-sҘe&|+#U>Ξ>ѭ3@"S,Ԍt=} yۃEumrzԌ&AdߚБWز3p~hTdݤ%Kk.s,IsRkZvωkjߒWAE2sX8aR)(ˆ )gM>OcK/NVkbJzzonoۯÈUՄ,>.XqfY_`=33V;TpKg03妔ZRT K9Dvge^!#y/FW٣ɖ]%8t=XtHuc75=~G78,qZbN/5̈Ί}Zdҥe~MF!(Dh)@ 9*^% WԒZt@QߓCe-lϛwM{} &AKJQc"iIۨEۧ t 6)L^9-.]PYg*s#yUC=]:%E)$չ%ٴWJ+_/Tջc(K#\ϬevNX_I=۷*g92RNY2;GѤ"!zW3 @}x?0x=C@j}1`>b,wuAY rPnң@:f)mU`p8XZ @K`h ,-%p8XZ @K`h ,-%p8XZ @K`h ,-%ptY1;;YrXycl 5<Dˢ?NOJ_gܻa5=ѥ UmrWv8XH ߠ1 =fĹ<یS_iԙsᩑѩ0.fzd'= 6ʚXuOl7$[Hgo%pp`{gmJJY4` 1ʞĹę)í^7ŢVyMc%{ P=!g0=Pux&ĩYm@5p {f1XC$ W η3 α]Q)Z6iiEC0}i.C=e%&}}@,Kx9N^ߗ.XXFw?oΖkzL1ÜsqB_6&K$XS,R8|LֶϒL7R.uɖ^,/ak-+s!^QcAGs=[,ud=#F|~6s _c"Mz]oCYt9*M%>m?6CBݷ'Tʂ[F3<}TE`O=ʟE:e;em' y=w3dQaYdv/c!FYy=䬃n2[7OUhya>]x{xtѫoݷv19u.^]x=ݫgs-x n:]_F}{_1|ĺ1i{?W~C]Wfc"2焸vճƿs2vk˧#gDwM{yH*uDts m.DF»_VLAFVZ\t=er"\R#66TH>bU:K[`k~:J3X2)/b92LB%Ꜳ:^ __N9|* bF?2 )LFޱr B=lkMɓN̾>]~1*ǧ=BSiqT>o־^ל>'._vWZgO_{\l{7кA/7K#s`]-u>!+Mq[i)YyngtAV}%NJOdo tEPTԡ eO(fc [?P^ġB61CC5e*qi>~z>8HgTw\kv LX΍Gw`(ڟB>tSRztl^* ut<GĬ<>6F[N5Aۢ;KBv23l/oql[!ok&q_%eRRds <bjbF;JRڵJjL:L.%:LGZ1b켌-w~y/YfFxɆCA=SGEZH\晜VTj\HPӁmG};bM윴!STgwud|\~}g8MQbo>* \Ytĉ}vrNޓ>Cu YSLG]gA`v'U+u#yQ C2egTG)YJ`t*:c;H|f`f{=~&u9oRA5[RO Y׾3 2ڵ*:Wՠ ]fy[\ND]7IJ>sGivJs:[G$ET跗UAF5ҟ cH'4"tLw.r:SkCNwܦR3QEծt*ú-ݿOFX.eʫ̆::.co lH/aӐCuniY9[dIQefiv+Nm8YK3-R쓣z̑-];Yx "-{Y*a/WZDq9kVTܚǚ(y*DQgRQ1yHIQelDyULrLɒbo:OΦ{l,`:Yԝ2(Sg?3#F>_{Yw#+#qp֮gguYm \vz t;e^C5ѳ X >uR9S>mf-j#vPte˨fmxm;k-5.:ͤv":# Mݐy _PlPS'DEgERrmOhis3{ /ɞ,+D}KKEuVGJ(&M=Wt~_+ )+[%߳f 2qvH~8{^=#ތ`R&4e; $QmDJa֌sC2=E,V;U3,Q>c>l!73kNLO#M_.zvrl]G8c*R >b8̀p50#*ڵj9ӭK)D &{zkjdxփƒeHg0el 3%cs(p׫k+ l.I/2 ~$6ŐqL%SPח5Uј|s&ٸ=U~ɍ,skR-圮u= qbfh}-EXD!2zܺ^fZ~]8S SwOC_$k]sᄑ9D|A•9\>b('bNS7l;u_}^ 7'_ -\k=23&:~s(;=vq",έָ~dwWkN vUѿ7 C x"n:#1$j2$۪ω I:FB\Ze '[a @S;݈qeYq Ϙu48X&|X-#q38`Plbq`ˤ82+[!*$BÎp!UFnxyy}ʙoaP޵!|pC˒b]]CG->tDŽntcL'bM~5:]+q5׍wN" *l#JʭH&'oJ-W6;[kP[F,|i +Oo#æUuTA._di ٭]R~sb:P:ʂ~Ei_$Wf;RMl<:eW7&h#O;(]Sn+Q|;hr%3-+S YKw쥤--#/PPbe;'L[L_K)H-G[9L?}kWV~|F6wiWսbŒyz7I-OQǵ;,ul*}W?.v?{eW=zt[C\wB+{^PTzWeʌw]ˑqcY&e«&8;/Ff]~_G1G`j_[ҹi+V㰬% d$/smu"d[΁@n.L,&a:ω'+G&_ֽT:XeSU ,3ٜt%Ug#_X^Vړ7zΈ@}|2T_f+t PrI˯S-q=e:1!.QLPr˓GVVEXeYWlїζM>9]k̜)+;]Bmս.F}μ2+ԁumas; Ggyd'd(w#wDw~]+?`b|  ptᇜ-P8xA={$ {r: #QI+SQ5 uX-(L=dxFʻSm9 }$ݸ<AlԭHs8ocK+EѶл_ZYt`ovQ$ UGݣ@un[r>Vb]܏qӈE+ p갫q:j8"fer/wS~|[⳧=yʘ^KY& ;R;}qVUԬfF;.y[U7CQ9:<ܖE]ڼKrtř=>z A6߆h]2P6,V2[f )(Wն:nBR8Ա:d32#ѓ2ėUUYz^AF_dE"ڴ~ebvP= ꫶767m*$l Q>r#?ܑZufۤa 0u9՝iw]u;Mћy%OF;wlԀl.oET9̠5t«Av툻4FncHT*HwőF]G)9`ɇ/ESTtӲhgoQP^a8wgC)Bf1Է:@9tdߡqTǦmYh֖6 vR4lGETDA1M-Mgh ,`c9!J@YN'cy::Qs.hN1掆I>4Ռ9:%.d\aaU3[ڌs6m _~C]UފҩPqzS벫}8];rE-E\j6B;rb]TQ=8)ٍaa+NSh^퍣ʑ08YhE,nW׸$`>lɝ, vT4j}=뚆m٦vf<ۚ?=G)aꤗN;{~3EjEv ӱvf.m}uj+Bu^tލ ~e]I1i6fztKYL@Wy+Jh"[0TK8FQt΍|Q3m źUo'^ aG"6_v8p6+ ɷ)zNEYtdoy~^NoZy|FG&:Ү8J4n#/ɽe}j;5y_^RHg0RpImS!]Veєg[Vzwi82㙬rxyq>4먖d2u, ˎ2f&sYRR$g:=7%I2ZtY#Rww9adY]Rh mjq:u b۟="z]#}cn]K{=Сtxڧ@^/߻OK`LX2yP6ֱ y쵀C!te'ބBwcdSeX?Cvoy2/\S6lhlΞr0%f)*otzbU HZU/%SCv"f_ ?{~K-Kkgˣޟܾ_V}- 79 anw wN4!I)E6\WeKeo(_rQXN ;'eʇ6mkJu[B׸Z._Q}LivI>߳FoyԳ97bANy W6w2}-\i*_[t]ݺi8W <GTYIՙ܏4 <[Qn&.5s P.^|ıǿv(7cV{z&hEhҜցq?3 '8`O!pLXԆ 7zKZ^&^/q}foTxrע{ɫQux6pAwxe~_䇛\rpj4PR*)7d~@#E4yn$ ,.`h ,-%p8XZKgl쬸ßA fPjrpnMU}wV/& KO_8 wKZMi1ŠkNE? ъX7{~L$1-F_Bc7}ؗ?M+E1|s[loo ~̋ Sv&]-)"(jo͉|i2κQ#}vea9K۰ygK;':@N %0o`'P'$4 ay Cir: avZPOOy݇i{,\YD?o٢\&Z2P\/ٵ삐Q?~HYQezy82W2ni>#Ѕ{R^%~ZVnڊwWy۠"t6euDzW^73x8RnG518ϭC^]! lY}9pNޔ\\eS`g6)v!\NǖѲ_^v|mc:L+$g:.K*LY>LDWbPBN]~7Yz?:_-Ŧ]Y ŢjbG4B}-ۣaFJސW,sWe/m눤?ks_ ϋr_ЌcMZ\dJt/B8F;}ߥH[xy[l/R3bz:|N\n[]b5qF~qe35ҪX*f˖NYiU[q.ﵴ*fmP\u!..Tĝ7rظݠbegꦼfdMngMs8mαy %>}ˇ*);ZƔrnJzpeu̷ٳ܅\촑IE=k/.[ڂҎçs;TK(G/틳n%\xۧyvp# IxsꝪ5F 6{>ulBnuNUPlK]|%O>_횩]ȺU{8X,gE1{D:C/ KMa)9|LR"q5yũ3 %Jv7;"]t1p[.Ge|a۽E.y*:"9SJ,? c=rW rDEuVb1[9GT$ό8vLnKYo:9&WPv83E-)CW)ܜ]nyor˟=(w%14NGX z Ζ/}J!<;2+9;9m,ӊIil&HېnՐn;U *5'J5.{8XƅEƌj0p+H)]{7[yޤYň?s5"ܱs멁MwĈ;@ICwTrk8[TOه gK3oѶNUqQ*.9b۪ *'I >w):e 2ViԵb*h'Prn˜|2vʇ`AKRNnSagv^1p*X>'n.|{g!t&[RH|w6mv+}2T`/Ȅ5%:ŢJZu!uYUWH^w4k>uڒymVhy=ߤ l,ݱ`Ճ\g$=RH)禌'.O3옼/xK LYJ6)TQRgr&p.Һ`k P~ic:uhaCSʖ\5Ү:@2<x6?%c6^kMjdr g^nom-MGg/Eq7ZIFn>#Z㚂ٌlX$jO W8qx`EIR^} YOkW"k;S 6bMo ghv!-lú\vb֪Cyf<䷋8Xhp: f-aB^:/Ī$ȡ2V/Y{e5r@q>kz8S^FosL9ڦY(Zx3 '8`DCl9>iGS>. E Kw[^ Z!Ll:aS{ndK @>@{i0nѳκSSM6"ȁiU} ʖt36h#XFOB!v,At82]D%p8XZ @K`h ,M1WwsraV^Xn9eg(VSfm=qLoItYG+⢓ DSUvZۛ:I@U?@%7㱳G¶xmi~Plo[ @logМ[7N'Wbc@-}Y!F/V aG453[R|jOw⳧ER1j6ʚXA%c14bN cn qjVϠ ;kBy3y@)+ptΊfLЇ!nOZ0+Vhͺ46ro ñ{?w{<9"b.#KKŽ}yPT'$9U/BL8X"։0)Al?݇rP D<WwZvulÒp:O^A^=0#y#u՛0/dRgtLv~eSTeį!<*y<uM߽2QES4X()T9*tLoqiQ]։e!,5鲍 QPNؤ_SdP{pN_ʛw&\Tw%y);QJY ë^ul>n^3K~$ V:/2]\ST^yˇVX0߇eEXD}ywe ﭱMJ6>6SPZ2]Dcꮛ-Z=L_%db>i㶠3vvO3Xz_x8+bMy4_?+y-苳'ǜYoq,ۇ^S`&̓ʼnJG0Mqu&ybt'uAl69GIJȶ&ܢPWnzYs87%YvWD<=<>a<5nsسKپ)ؾ#bVa~xzr 'qf>#-D/ @H*u"J[-sTo㦬YmO)md~i[l|N-?xnyFyn҆7GK.&ٳ4ݫNچqqC#L+"}sϜ\qJ@~CWe<ߑgeUQu5ω[Yj-QKU,ioe8$JezD~BJ]jiPsѮ> }`*j^kN ?TGAߟ:|z)KB1Ƭes`Zً |H$;p/K`Ljv,T51jЪ zp8-,ڶɃZBEnG׉GoݿsLjRӌMW5MډJ%NNzg=*w$i`Ti;uB=!}W}jR-# SYI:38YRէ= ,SF=Pfr0aGaH3Cgcv/DhGƖufddz/(yDd!/"lVh刕GbP MPn˺bXh=t׵cS~IK.k6iS*k1Y1uݕiVWٜp\bh$ZuUR)uGѸXvAr8;,H:׉/V%c~n«6*O}tι, WDL:4uo K"s^y²ȯs@DƘN|][0ڪ)3+OB62{6Y\{"Bnrص]F,v,s}:`K&E\l81nΫ%`yvru8MxZX~7{n4GO&3g7kb[.̔-MS8|Nܒ>ݱASD 4pU~K^'O-Bgb k]ZU稍p2[Wy1ʒ똙vHiM ^>I&EF{p(瘪G#3eXl юift{OMrI])Nݐ#b֯o:"Ni&N9+|m3 Bs,]i(C;'"Ao^3KI8:|vʋLk{7”c0 $0p'*rap"EFQr;Ya4U{x"x9M ;*6;Uã'M* ARɶ'e+"cz{ +pt!S@#X``D<+PY{Lt{6CၚL4~{[H!1x+ZQ!hT9m'ER"(ǥ :%yee1oIruǗ]/v(9Y]G9fı:jO~GTF+MNB=ȴ7`AG5zWzu9gR>[O fkEƍ!fY375X.&>,^ZusbX8XqFvof"6nѥ|숾9b,`ؒ=uP83UY?Ś\[n qjVϠ =!EK]Á[bYDQ'zҲ +RswVZA G4Xͮy `2yJ*<I\'d"u:bt^G6x˥"oEz-tpt/U}2|MGGv,-ۉ/#od*Fs.L:zyAPK42r)8"\'6Epqm]ֽ.r 욿7e,Mvo /9yGN2v8m˧փWǕ>>+[|߆bŒ%vTvePdgE!9CYӷ]f%vf~sXa]ş}ory%J7BpSv9KlHC}&QR7B&VZ>}|y!E͸,}(w[qBSվ˜t#׮,iV[~pգGw}ѣW76w߳ϣoo\uwW/]~Ӎ)9%,ᴯUYޫw*vn&߻˻l$:>.|FS.O1̧w¼$ȿѤ|:tz4edi[cϱQxid]l?Zu"B4 7Kq=(\OI׉Ƴ:^dK)7>Ƕs-S˂t3bizɟ}NDAbqdfIa,SX.<=L;4FWh;*_Y:gsMW#c$}Ц, !>ջgX6mFԪGA2q؛֕].!+a5rrixxvc]Ӳ+!m:_~!L[J˲5scGLPLnY˜c;z]d죬t§qp om`3`=wΈkCq-[kkB"/Uk쉁XDzc >'ԯRuko51<%;_=Wس<{b,|o FM ~lz!}mM ^,MLdze9"]g|ڞ|=2klgTԕg7kb5^:ˑӤN4a)5GW#qÙ`:kP^ui@tS`[_~-ĵu[.{wl6qG}mMKgWo%}p{?ɘM2F7:ۿ,]41n}{1ϣW9͐i*M~?sZr\)sYF^|/.l{\Z̶Kדmi <ܖy0#˽tߏyǕn@zORd=G_֌MeNlT7/hW=Y}D~]vLO.;Eu3C-ދ202U^,ca^u: r;Dv"w ~7dxіx ;]fћCOSNc^~bE1vI/w>tıHUtYP#^g$S'U25 e2ԮPT&<;R#8b-vC=/w!3dlw1prHnuy,CiL69zΐ+݀& FL6(]dºi;*{d5G`fЊ쀋l6TxwN7pH9vd%`gOba%>SktTE=# -,zAQX[Y 6[^N4 [ y/Kb^PuiHΠK--` l=L҃;lUyOdT"jzMk=P Y]MHm+X/ۯ&`_8lj;-Y;6(ZEQMW! }*t\i|F&r¥zq(S4WZ=?˄bP<YbfgZVM-boy .Dvd'z;lwg /oDb6gYk;K*;,YgOkW,e~nPw,$f0oz\,Xu3 m-qW'!ֳ;XTSM2mc҈ -]H7igebSuʍR]"eq^GwFw%1y 4&`gԋvVqgASJ7IoۼF}g?lJϺڡTn9Z7 }<힟pЀe2|PpIlG>oQ. y1Yqcn]Πb(}sn95ׅ P-  7 =JT|CL N2z]8pS"y=Bֶ/ep5q:kɼzIv،^On˵օ):H)'ѡ>PX&IY&%IRys} z2[?yGYvˤN,Z"K{, -^|楙ޫ"^]iAj[7tU}1|Nυ+ dJ:d:ٮ YU o?˔d_`܆9J/͊uXD#Br(\ږ" hR[pJڌ>cº!ooJgxϔT$_7u*&#}|uGӶ "]7&VE\4@l?Вi]'&4}^4kd?A*91a#8MēLD(ZpWuBW[1䀚8<%. $˔{;9Җ G}ˊ 5h+\,2"h,.`h ,-%p8XZKgl쬸ßc3mvʅY1{m?Y(fg2G1i^+sR eL@"5X-Oλ''r[ 7ƗwmavԩlK{E)LFڊkn3^>]SB^*ʢYrPAZB efW[kBGY]{bxȦN %!ލx8X:iB=5U1!k_뤭fbEM:l:®shحuF]%o"pw{9!-ݸth@,K.>lJ삯u!̖I(nӕYP~kWavY/;yUH6 ryIp'2r<1ut_ni0x+2\}YY[aF]?Y)/ rgHh5rעL~v yێ0h?BY8E?ӗn `삎x{ѣGq-jw/˿}΅Ww[W4Lv寈ww/8Ikb}U)W/D>Gzdu)?]9:;׷/_]vxS@, s2ʥ2ұ1Ŗ\NL'<;p@ވ5z0_LM22BG,}z*bDNچ[ol#N쁿 ϐ@ײl֕1<'Ew.;+:[^=1qa[oxN$_ i;m#.j;ayY&wX ?/=IKǵzʸ>򙕅uRycs|'>LDtͥU18W5PyoegZj(ٽ$fuwv#^l=|N\(^ɴY{ώlѕ5x},knV#o!1} Țًr#>;.^{gRVʎ^vF{yUv v!y|Gܵ:3[)y_|)աX,k?N#CGw -7λc';)0'ze!-= _4LNPlӌc qڂfs:o#hXG^yfMl{D}Q^f4wupa^eSZ*{F6ftzaIuN}R:GSOm툻ĤڒwR&+'re<+mی<3:ܩ4W_Ւ|`+6uӹ,=6b!^pt\lnl PO?mm:Y1:,Y҇r}'Q%蠩`联V+d'lz.o gb라{B笉;o]0aQy:v Rb'∘%".l48ᴓSC>&,^%sA} Ȭ̉)pj֜-u2m:5EaWR);Ȗ@r 티i;,#s`QAi{)%-k]dcrtz;,Ɉo|]2[sԡ:'ym%N=l̝ W2gigFԑ;U'qA!4/*΂̗7Vx82m̚^HZ :Bj !ɪ6thJFFuƟNq8i YjɧNN$jC=]ZF(RnaJ-&)|ev.@=4ۿ-ߤ(bln=؎}{xӻߔBQ% nuF-U<ḽ}Li)6'KQ,KTSaGBMF7Roa^l~h,iCn}vʋ홋n䭜h2AEL;#K 5Qjynj2r蜈4k.я}QS[e#mtz렝5WFb6O7Ĥ=l̹yL}J5>UvROTeY?\5q}[)pLlpb>Rv&mfe{'kϐƞ̱I*MknṋKKi=FUY̠pGuO) #w:=ReQ m.$kvtARO]7 4ԝF΁ftyГ2(D{ce_ȃ2e*)J4XwR69(ēLDǏx0V^,?̴YP0s\@#⛏  xP#=`On8`œݖT/_ckX c^{ՠ{KMD8XZ @K`h ,-%p8XZ @K`h ,-%p8XZ @K`h ,-%p8XZ @K`h ,-%p8XZ @K`h ,-%p8XZ @K`h ,-%p8XZ @KJopҗ$>|(/O>:;|G?*[U ϼ/'`Ǔ\|y(8q7 VO_?T _#!Z>aAwyG_MM~ޅ+ɟ4d?GH|W| /2]`$>oCOL|#—?%[7fC_!8=_ġkUs~C?>ϋ-kVڛ*'xyů'~B9rD 8Xx&闅P=˗pG›)Ρ#v7>ǟ|'~T|>m$̌NP?3xC-ڋ˅H}Cσ'?I7q/忈˿o\+R|#O?t__T/lr }Ǘb+MzQ\!_Ӈ*+}}jߕ~]]_k@˂677cB9n߾8Wԧķ}۷K$}>/p`[&ē.f>Հ"X`.\1s% '"E+&z%dD=*~>">nݺ%byyYڛgCG+?*"+o>/p`%gG/QYGK母+wi'' ȒS3ۄ _5_M\!.d -2KloV9k{'~~N?(?bggGEIS'm9trђ&|'4_ψzWH|)v>H:mJ~O~xcDŽf꫾JB`\hW4.ă2' {1=/R+@?c%7A97Btδ^}MK[rb}~ n@܉tNS>چB&S"N-CF1(}:.伡ZFd D5˿,޽[[⫿77+ A7?G ʎ\*{_v$J7O=TAeIi~(@ _zG/7͉h.Ghǡ^4 _*^Aj4@Md?я\y葳ԉzuӓtm;"(҂#ݓ^M O{ݎȠ黽94oe 鋮wr~c_QQW~WƆcㄑގ?P__(|9ihEF9W;t`>Kb3#8Ҫ']~3j^3(" )DL43Og3n@췦Ќ=EL߹B]ZC&Ɔ>E%Μ9#{9)#qso>/ ٳg_Z__WNO8}گ"Ux!?gL凎-:/#I2T&3>Q ^,|PHzLmuO2ICv ifj47P$- hP9*s4ߦ犁C+ufIV5{ QBi~甓)ZCYSN)Eȏ_:6IK(ۿR6Ooz'r-c- :zr@_u_LD V?;;uA=ΟϿG`F֚_U>6ꑿ#I7j,UoO|H|~PY2CKhJDh? 4+4X&gA`+tF(#iODGؑ7tu_'> 6-Z*CP 9CS!*w0!ɯZNEC'F{ dߴ^&d(" C*f#J7^/{)5.~v|޲CaħNQdry>IuݴІ4p >i`+Ig[N^rhoVyA:wD5t^%8cӈ-D2ĦB>/_5' iCۊ%aẏ o)*,?90^5,|H|wU!5ԓykGK~}7'~_.u<^LdrN!h k/8Wڷً[ۻx 䋮ً\9/ ts@0=WhKvO~R\|9zP<ބ@6G9Khݏ 257Զ4q k}7kW~?A+icf[lȉaG`2M1DM~~p@+ '˿B#PS c6˥H+:)ݿwN--h({3[ZCiReՒTg5J8XsȌ[en}3'ġ^rR;d~u+敽:f̰e,p-ʢ/grٯW EØ7[*NhS]r*sxKI;Uח('‚)ΥA^z׿7F7}W-K=4SQ|?_9|b <,읁?Ͷ%g (LJ;b^ռ7 + z+A.Eo -!'-(lHd+BvMo5"{}x ?T+t}J4 ݃-"g^)GZ7}GrH6b9_?;}JЇ`u%@YĔ. se?jim{mf#FP//iyTU UOOfo~g~F9 Y9 Co7J >fi?a5݋nnn-H7   mnkh60M/䌡='(ҋ>9XBMտ뻾+piÜz;mrʯoG,}Gof*s<A8-#[6rOZVgyŤC@گ~2$.5mhO}Sux젷Sp}ښl+zWOZ%B^$)J\ 䨡~7O?49wЦ/_n<^DO4:0Q)'$!~'R (m{'9Xȑߏm6>32Upe9cd4) "W-EvtJQڶ3_r<#h٘s(@"hտRҦ9rh?8X}ym ;W&di ChW~W:66mG2 rF6J A``\1oa6+A{~Mht>9B Bzݲ6K2S+tЛ;sOr{݃^ haC'+M Acǎg!̛Qb_gG?.~7ӹy}>eG 8XeC??XKP E[,?W7?,H¼ڦlO 8XZ%B-%p8XZ @K`h ,-%p8XZ @+D8LIENDB`param-1.12.3/examples/assets/param_help.png000066400000000000000000004100621434441564000206440ustar00rootroot00000000000000PNG  IHDR\4 IDATx~$H4"""hAu[JCBDZ*iQuURZT߯tf}}>]q{M7ettK,a28*G9~9-ayk2u֍r-:>}:Cz>}zaJ|WZUlO̒( ( ( ( ( (Ч@#x_>}.B(;ò_ي+Ivh~tZ^~y#zZ8FP@P@P@P@P`J 4@G /8pfN9lƍ90I>uơ;uZ΢\37>:_л#7AW_9sfO/t7Z:_P@P@P@P@P@P` sOUzU9Zw6mڔ_=۰aC'6ΨwؑBw[ﶠ~@wy'[zuV:̪h"nOP.׮]k}uhK Y.wcfryK7x#QZ 22 '2EoD_xq8fzI(_K-meٲeO?Q13_y6v؆>cP/"JLxRDE9* ( ( ( ( (C@f>Pelbg<;`g׷&>;3ZR?;SD@jrŞ{uYy3 ?z?]c1"м~6)q1~':ak6#p.=-1-|۶m07o^KѓÁX9}S#*DX?Jvn:ʕ+_sy(<ꨣ 3peKPzӼ86?eyӻY](#Ӽ[ETl%a;M]Y5ktZI7=5c{>g;'`^^/Wu<I'-jlgtP^r6?ViknUW7|sQQzVΨoF9Ki@4ݖ*Pi!I6(Ƕ- ?g}X_tiKōof"Хzx|LQ.Uq>xv{;~"质j5?$==R<9V2y!0-ض2 .Nt}UW̃n+>c̛sz҈NiE ^zi:`tlQsla?(^>GQ cX Wo֢,*Z}ݾӱd8,G >l:Eo&P@P@P@P@P@)0:g忸\ tr FV%f鳙-ZT5ZgЙB!x6udL0; U#4Hs㫺eF+'HRpp 7ߗ[=A(CDi*S嘆yJ(+ArJnAȃp9u]t^bḋn='@,*nX(gQLCطlRiЙ}Q?x|\Xp9:g{2<ݎؑv^S3_GaD=mv_Ƭ[i9o&`#HZս>>t^6mTGNSC: Du\݌nĺb't* ( ( ( ( UѪM=UX:]ץz(G]\e!(V(4 ʉVaI٦u]WwfOP;1$@'_Z.Y$wyLZS#,J +V-8e' "8]sL/'*HtGqDyq %>G TB$+ ( ( ( ( LuDܰ/P~Jiyq>½']q*99ힷ. YZ) u=:/IXnaز eyO?=wut>K_}ՅQ9Ow: w@kؾZ8Utz#Kii{シyֲʢugk{dӸS1T5kmJ+u<xnUQ1 'AcZh_>BEN+tQNMЗ/_^O.ݖJtnDm? 0dRIP@P@P@P@P`r 4@4Rt7 .v<Ӕ&nd$]s5EK9Ʃ La쎿48Őq)9'gtt*5n{ &SNi)Gؿ+q H芜M' y￟w[oOg;*'۱XGT`F 23YTzz<{ʅiSdEUdɒc/>M)3<'2eYtiZI ;?WaOOZ?m~ˁȧ!-7ꖅiO+MŅHi,ͷn w:'xbK4zWt4}Kk4QC}Ր@T?x4i) x i9^3 $NU,F\\pAC uڄ+2R1>6|Ct:~<,˨]~Gلg\QG},oJ>Ly~Ui5M%QJQ٬yO?2 "Q'ܖ~&,ch(fa:05Pl(д) ( ( ( (*HU7 ™3f&3h\N=#w?nR~xC+r(rR. ӂ<<,g MeZ-rkkMKsY^YUAu aX.v{[{oD rU`iW|GH\y׭VU)m%\Dg}vqƪ,,#Wl+z[ꥡBdYnTgX.OHi0rȳ!džhy3 k2o($ϙ3e=,3-lr^]}^Hlv1dN8!Z ^"1}9[La=51w#SP@P@P@P@P@&@#A->iNKUQFS9ίY8zM<>}.T肘J ޏg"NWOU #l,˫/Kt]AA1_gZS- ||rcwؾJ+AWjW>Z1Z8֮X"?RquljFUٚ\P@P@P@P@P@S` Ϋ]= ( ( ( ( ( ( (#`b]@@#P@P@P@P@P@P@P` '+xٳ[γyf2fۤ ( ( ( ( ( ( (0zГuMvP@FS`ҥO?ߒ%K<}̛Λےup¾qE* ( ( ( (SCzwڕq[n{衇2ZP@FS`=l[jJVM}`r֭[7:r9 ( ( ( ( (0I OgP@˳38翓N:)y/_9O}[N}:EV@P@P@P@P@z0( ( ( ( ( ( ( [. ( ( ( ( ( ( (@{@sP@P@P@P@P@P@P@0>z%R@P@P@P@P@P@P@AzhN ( ( ( ( ( ( (0zGoD ( ( ( ( ( ( (=@IP@P@P@P@P@P@P@FOSHP@P@P@P@P@P@P=9 ( ( ( ( ( ( ( @u) ( ( ( ( ( ( `4'Q@P@P@P@P@P@P@=裷N]"P@P@P@P@P@P@P@ $ ( ( ( ( ( ( ('`}֩K ( ( ( ( ( ( (ЃМDP@P@P@P@P@P@P` :uP@P@P@P@P@P@P@z0( ( ( ( ( ( ( [. ( ( ( ( ( ( (@{@sP@P@P@P@P@P@P@0>z%R@P@P@P@P@P@P@AzhN ( ( ( (Q(3 IDAT ( ( (0zGoD ( ( ( ( ( ( (=@IP@P@P@P@P@P@P@FOSHP@P@P@P@P@P@P=9 ( ( ( ( ( ( ( @u) ( ( ( ( ( ( `4'Q@P@P@P@P@P@P@=裷N]"P@P@P@P@P@P@P@ $ ( ( ( ( ( ( ('`}֩K ( ( ( ( ( ( (ЃМDP@P@P@P@P@P@P` :uP@P@P@P@P@P@P@z0( ( ( ( ( ( ( [. ( ( ( ( ( ( (@{@sP@P@P@P@P@P@P@0>z%R@P@P@P@P@P@P@AzhN ( ( ( ( ( ( (0zGoD ( ( ( ( ( ( (=@IP@P@P@P@P@P@P@FOSHP@P@P@P@P@P@P=9 ( ( ( ( ( ( ( @u) ( ( ( ( ( ( 0kfn9 ( ( ( ( ( ( ( ]}ѢEٴi* wRs]vyM|BP@P@P@P@P@P@FN`?|eիGn j~nΝ[wgzkke۶mof>d7xclLsee>`id~{W+T}g ( ( ( ( ( ( (@OC@g)h]LЕ oQ wx­E|{d~{gYfLGy~^ :W@P@P@P@P@P@P@^2.̚5kT믿:;c(Yf3f(\￿}ɷtPߓov?e93g6_MڱcG{IP@P@P@P@P@P@hB`tZX"۰aC޲< }xvo}y˖-V\OmWr~gyKxߴiS>Ν;3puּ4ݥٯOoe|X>[?X'w}w=ݢw:=dbK̏|(K7k-+y ( ( ( ( ( ( (Я@;뮻ӧu]W{R/?3+*@Y8lժUmg7drn~<׻.[١Z{]|/e[c;ݺucٳii9N6zzK&YWdi9׾W@P@P@P@P@P@P@h4Nk;톝z7r ( ( ( ( ( (Uwڵ[kk~2ZXR.#n*Sgyweʌ۷mۖ}g]NkW|ꫯ>4~GW-4I/^\;[mXtZG}sguVas׏9~:… iEun+=YZP@P@P@P@P@P@Zz%[պj *͛+1Ӆ;rZW%Z=ݴ_sѣkvU)u-Կ⋢'|*F>K觞zj6wK/ͨ<0s"|_L5z19s U]eb ZGE6) ( ( ( ( ( (4@O[\^LtN.z̜yg]|GY)GUb}yerdz 6TeE ?_ߺuk裏f;ṿz,SՐ.;yy;No9PϺ<խ˺\P@P@P@P@P@P@( 4@j'^revwd[l)g_|`6,W2siMe4<ꪫ*xZGłtn]9Ú:]W('%zrwRQ"|ZǴ톬rkȧjVIP@P@P@P@P@P@U>E#P Gi Srsqߕ4k.O> czf'tRvQG .W\^w@'_*2յ7q:蠎C7vUcn*vat/BerDP@P@P@P@P@P@P,HLӖ~iy>-iL7 J VG`w]"Uxl̙_]ug]tgdaN_|Ex~i*k;MG}t>~W;IڋwRÏ>OP@P@P@P@P@P@MmVW >o͌x>y69cnGQ<@1AtН/R5Jlٲ"e^N'0]%yI}Evi%UgW^~)O,ݰaCUms ( ( ( ( ( ( (TG:}IyX /Y$[n]W_etvxw*<"{WX[+E2-.],i<7>k)FZNgi5|Ӯy睖y3u>hq믿f=\=Aຖe̋y^}EY~e|s9hE?s-2cǎ~iw^V,M ( ( ( ( ( ( (@?)7|-y7X/-[Ѫi>{[I[\Wg,/v;--vǶ?ޔNj{ny,\0&klx-6(CpE w\O>Y7Z}|̙2ݡZY&*.L7T`R@P@P@P@P@P@P@h4NAhLKkr2<ӳgU33h:]V=PKꘖ +-<  G_4óctw ,xzUJ3Ȼ+vt Ny֭٪U瞿_|1fKm+W,{zW~;ÆgOc'I}HeufR@P@P@P@P@P@P@h9 ,h)^xQy, ( ( ( ( ( ( _X:ݿOD:#ƍ'SP@P@P@P@P@P@FLZŋ{ofO>)}w뼝 ( ( ( ( ( ( ]_N;-dϝ;wJgpM7={vP@P@P@P@P@P@P@ 7'޹sgseBY&{駳]v| ( ( ( ( ( ( ^. ( ( ( ( ( ( (@;r4P@P@P@P@P@P@P@0>ץS@P@P@P@P@P@P@PzP ( ( ( ( ( ( (0G{t ( ( ( ( ( ( ( @P@P@P@P@P@P@P@F[`6mʦM͘1cׄK ( ( ( ( ( ( (0C@_~}@>}B9sP@P@P@P@P@P@P@0>ץS@P@P@P@P@P@P@PzP ( ( ( ( ( ( (0'ݵk$.E&aZEP@P@P@P@P@P` @_Ν[wee\sM#d֭iWv9d{GnQ/<ۼys7>stMg7xc^{e]K ( ( ( ( ( (@@,gA0rڴizٗ_~Y΢.({n;^՗/.ʵf͚QN o?p>`aR@P@P@P@P@P@P` @[;Th5kV7s̼x`g}_~E7|sGӤ# 6lHu)i ر#s= ( ( ( ( ( ( (У:l}̓NݮX"#tZwhv]/X σJ_5; /p41/w}7{W3~k"7~1.Y?SlٲVt,K/}cag}ʕ٦MiXa'k-7|IGP@P@P@P@P@P@0>UZ@Y]ZuYqi̙S;^Ji(UW]Uiw<@:izƌ4~xƼɓ=ܖiwׯf>?}5V׫VuV(côCy}yG:]| |Xp1ǴL ݷmNP@P@P@P@P@P@FUl:AbVO<Ѷ$qjӴygN z7f]W]S ̓@wߝ^2駟ft:lh~%diqE:R[oA9￟wNk(KB#-Z]py nZqӝzni;3{キ%t:wɃnYN'?toR@P@P@P@P@P@P` @zM7pCm6w]w+">xsc:=AkZUwҖ۴Ԏ7˹m۶,::K=PQƺ.龜g~_Q.,J ׿fi^t_==]2`J?l* ?Swt{&2?'P&;Ƨ?x:{&|_:eݚP@P@P@P@P@P@j#ox8龝h, hر4hc܅ ֎WEt;̓.<yygt. F<.rP+>1]GlK^ziv"N_]oܸOP@P@P@P@P@P@0>t^"O.K.-쨣*Yݤg< +#s9g<'+!A^\se\`A:z4o|W~C N4XLqZ{ϛ7/oM=&ctUQ9sq<Q^R<@{+Y^uU]GYfgP@P@P@P@P@P@J#\@gtAϺn.xt}NG˗1tﶌ_gk֬i& '[GGaDxU0=z|szjbŊ ǂcqf<;<=0봋~w^8T@P@P@P@P@P@P`J@j@fO?T,=Ҝvix'SJԮKJw\1ʕ+yNzUb@V̧.?Ѓ =eB]2F;M2G}d ( ( ( ( ( (H@j`d__:EzD|242Tlez%9]G|,_C;gt?̣= *~J7<Ͼ*-[YꝦx=Nt<P@P@P@P@P@P@&)[oa_7?O=TvwdrH$`7Ԗ?L@gxߞygQ,эx6oW^*5{wҖ-[縳.bX/1 ?3&kwyt<+ bb IDATڵk_|1y.\0xv{tgϳ'x"3e+{R:L2ijwiY7{Q*P@P@P@P@P@P@j^t%h!m֭[W7烷fXK|U&T6H߱ T?>x0 K+Umty}\?+{T. }-3hN{|e$QA ( ( ( ( ( (T0>5~Hbi9MkZ1hYvq_5|iEf<O|;Gs)>x%\|dɒhayƐPךjN9唢'P5) ( ( ( ( ( (0 'Z$R.,@O˽s혯 6E9]=4M<7q~{yjnߣ[fVx<믿}y1~'C*hD!0) ( ( ( ( ( (0 O5N+hN+ɒ'2z9_~rg7) ( ( ( ( ( (0 O~=˗O@tGƍPP@P@P@P@P@P@@ z<0۷gg.Z3gӂ~֬Y?̋0%';sJ,  ( ( ( ( ( (@*!lڵ9XիWxv{yȳM+pM7&P@P@P@P@P@P@IlC]]veK,nʿz(ib֬Y=ˤ ( ( ( ( ( (T͛.gΜ9דˮ ( ( ( ( ( ( (0`x^P@P@P@P@P@P@P@\ ( ( ( ( ( ( (Y@w3P@P@P@P@P@P@P@Pۀ ( ( ( ( ( ( ([ ( ( ( ( ( ( (MiӦe3fp) ( ( ( ( ( ( (00o~<>}! ( ( ( ( ( ( (P@P@P@P@P@P@P@ ]TP@P@P@P@P@P@P`<l>CP@P@P@P@P@P@P` * ( ( ( ( ( ( (!`}< ( ( ( ( ( ( ( @UdP@P@P@P@P@P@P@Cx(;P@P@P@P@P@P@P@0>* ( ( ( ( ( ( Pv ( ( ( ( ( ( (C/`}WT@P@P@P@P@P@P@<P@P@P@P@P@P@P@^Я"  ( ( ( ( ( ( (0Cy( ( ( ( ( ( ( _EPP@P@P@P@P@P@P`< P@P@P@P@P@P@P@zC, ( ( ( ( ( ( (x@e础 ( ( ( ( ( ( (0Ї~Y@P@P@P@P@P@P@P@0>CP@P@P@P@P@P@P` * ( ( ( ( ( ( (!`}< ( ( ( ( ( ( ( @UdP@P@P@P@P@P@P@Cx(;P@P@P@P@P@P@P@0>* ( ( ( ( ( ( Pv ( ( ( ( ( ( (C/`}WT@P@P@P@P@P@P@o޼9k3gP@P@P@P@P@P@P@C@V@P@P@P@P@P@P@g P@P@P@P@P@P@P@SpK ( ( ( ( ( ( (0) ( ( ( ( ( ( \/JP@P@P@P@P@P@P`>iӦlڴiٌ3ƙ) ( ( ( ( ( ( L%_>O>}*UP@P@P@P@P@P@P` 3S@P@۷ނD (0[aÆ]>LP@&N룉w ( (ЌfEP@YrklΝSTV@:.{w[{?d/]4?nE |~@P@GSg] ( Q^.[Wvj|GV@PPO? ֭+qw]7/\sx) (@(ܛshC& ( (Сmt믌{Gkx糹sV]~mݖ=S?uPhI~l}Q,(̙3_"qc%ADS!Y&8Yw 7o^plŊ>Hj=~ظqc~'-UAO9Ⱦ뮦yG9?Q{'1cF/u9-\;M+3Y=jK1YOskV`ӦMم^s_ӧg'|rv饗毯暶3m:r%uqggK( ( (0GqL] \tE{t<sqDs=7z-9և$ZiJl~]r%f5i={vaXw܌Ϲ4|ǵs; qs;}nɒ%ε~>(;DTtl=N?Vt;|Io_GQ4~39u'#T,KM#ͧGJ *2P@P`j @ݥ/\ub7xcv衇歜bL<֩nD\iM /zE+:*8GUq, a|Ox^F7tzr}u#3-8Gf\+@/fX g&72aɻГ3& "}׋DŽk6[lY}*UnjvĞ# fG8n曃* (P @O2hڵ]rk٧~X7B}] -cJׯV^ԯ\geK/}ŬvYɳoߞ}goQ"5v&:'[nt|_~%^y挋'M7MZt#o۶-[jU~ H-r̢3v{F4] "6_dnꫯf~aGV]~甅mL?|ɾܲ]lÝv?ru MS6n~{6lU9N~yck-NZrزeK۲c;3gqcꫯn)oyL]'S5mW;YLd 7qd0ߣ~rp޵|$|לgr|Z>(~K9z< \tsr/Xsȃyڃ v{:1cVfޱ?oq^}nҰ\ƾcAXDO0Gs^rϳ׼~>{M ~\S9P@P` @oxs#mJ t.(hųsgO=fy"p6Nu:*7ޫ3*:|_,W\qEKm \r#"M\,㤟j!rwu7曌:x‚:I"9st2I>?~ŴC@*q蠃ZB6hQW) nlDN:i9a R m4Eq'|U퓲El=>g•2x 9昖cZ>l0r#LCx_ `]L9AcLc=2d}뭷4M_qG,½۲͏u&e]2MlL{M7[fey5gT*'TR )UmQn&%b4Qᄏ(sSK_Yf?<=̓TBZ&,o̷@'||;[g05Ǵ8'*cU+Ygiq<̎ H9o$Y)[۪i^vZI~s9B?;,{\S~Y}Qϰk߿b]SykÐ}]3e"u]c|C(Ős4|:Z5uqr\D"0>5VȔiK985^o1}uGǖz΅{c 嚕Q8p?Q M3c^+Kq݂y95qQiQık'za}-1v/&P@Z^t$8鍧,r⛛tM'd%h|7O`J5\DhtHu N햩*?.MS.WG}r\0 8vKq.gZS'7=@9qӖ[Ǹgq1$n6LજP5 7@Yy1}GylOy<|+W$.nժDoUeϨ9Ocܺ!발 3yW|=D9cF?"*T[O.{YafF|VUcF@Yku*'{qj룦~Ӱkl鰼/.49v ( T袋8"ٴ!-Iʉ H;մ$LH.'r٪@''YKWӼw̏rҭ35niŞ8FJ𪫮*/B>G|+5_yӔs>mE mz@XL̋aڪݴ,cGK#n8rČ<2>! k|*SwUyW*glGuùQkaG*fDKG ZDzw4}v{e t@eOr} zk:VP^5-yq y'5-Z]py ^Ony1Ogw OzHSjl?C> eM]ik-}]҉&zLi_*5Æ>nc17F;7vYrcuzYZ napV^/x]޼`2o 2}7NUȓ\cEcr%(* ;MbT)ߔr^8W`| E_|qR3ݦ:=gIoz;L'5\sMG0={þiбu7c*9 s8.9>#íU|dMuk,q\?+r,;M|䓞mm}Nȃ7"~yyNJ+*E MGl' h%ZSHuuCa9u@O+ؗ+ ztzsnL+ηi9g+hoa|_w; ׬\G9Y180^kcFZ.Y:GhjߴɱE&N,u[ayU-{IP@rNй&NxVNf.V%qE9_N i~$J/Zӕؘ/Hii)``'U7q! Jyw*ypbM iYZRmu"_E\8" 뭮DƛkVkQF. bqcFzMeb{х4WRMn\ip&4h giJWNL ( (0u h]7Gi]h='p|򸜘/Y$U 'iMp!HRzI*/*̓ZX Z.pZNE7$"-zҖ1FyQF:N Po"@Ʌcl U?+)zUir+fs̋!vtAyKVr|ȫzFT-=#W(dZG>UA~-qEA̓!I 'RiJ/KݸqcM2#-En9lu.`y}z\ާ>t{KoDw샑 A,s"{Īk}Ϫ噈bd]K44 @{rƸ֜<1N 9F1zU,N3> J c%бViZgM?Y(c!6~[=ُ,OܘOMZQ(}Ϝa@*i!VnS{sÚ=ew5V; TܬJM^c8Ss>J`O_ݎkDH:P]7UA{\Sp44_W=|_ͦn_: TxR]Şfz!Gv׬1CocyR/iu84wz7(CyO4zLG2>MXwݽy ( L~Zi.i h1'j\7o^v駷ӋW_}U̚,kzܢ^I]/Їe,&EڳX]ڭjtvQҪ:Z ˓LUcuM@O0mGiJtZ@o|u?nF K'qc.N5]"3tkh룉srjr}zGn^c#NGU)u{*"=_L~Qi(N" byJ)'aU< o"e:3*SBhA1xӋ^iٹk 0rʬ9,gκIð}vSA|aĺzˁ9.8Pv=0bnO V;@7iE}ԥUtݴUwz(Ϫn*{B 4@oIb۩{zZ?Y1kge 4^w袋} =k +*,5>ߧ75х;ǪdXI{~b^#%X3i%N%8b=s>Vu-eJTG}tGwkŶ>h$M \O0.U=ͥz zta9J|_u; ׬q2X&c,o ~79Nu.@%L_ݹvSMn^)=%NwTw… aM4q1\~S\V]dw'@O/77n0T35:rsD κkXOZ {YMoз{ cZvrǑ75}qjWm7q.-[Onnd*Ey 㧏P~S0W7Q9~|qcϺϩNMWi v3:רoz{p45{k+f_`%@WKu=`VT&]N̛2PB /,Ko7y)A&@e}?t@c+Mכrhj7*u;n;z^4qJ+iRCȧ~Z[ zu9BS' Q,|?iNK9 ^WFT1) (0>u9Ѫj`q8,# yUMK0mcAӖ"uA20,q nӀkc|b zpY4VMXnS iKZ^t.颛.dXUb g^&nFGl7\S?]pJut]V R ika>c{x뭷R_on`c…I鱾II9 EK>!gzViPsuiժU2Ucǭyts(my*4<@'ϪhC oFЫjU2m=:~V.1]rhW(&z;Sxp]U ~= ^M\cż㼽jd4@Ӥئ8W 60nZ*@V@#s˩u7ά3-{j|~XMmQ)%bjzTrrOk41#Y^*(i<{cpڃG\-svQl8ҫ;4I͌Su}(u^P@FS`-g:dE΋Ssƍraʍ'ziJk8Lf͚< [܄iQMv۶mE6t #e9, R K,٭\h%aÆ,}Wų=?{omGq{'^zv=\׷v۾j\v|mWy*cm ̌`̌ H$4#! N$6's=>GZď.&exax ćf$3-xڸVC0q΀D=^le`zt D'/3A͸b{ X` ʖ8xyHi j3kmy~FzݾDD۸7ٟ;]3t$Rv#08PO{챊#/"Ƶ8I'QvV:X?5EsO}^PG'mn>B9';f~f>iڎravJ-cVs<<[ʨtq\C 匰lуk蠤ΝY!en`=)s9s\C- \G~kGrV 5OubeY~1 Oy/˽|p }!JĒGʳ+w9BYc}`Hn; v-S2X(˿xG};2kTVdK[|PyE=J{z"]wL^ڛW?.3fL~Qe6JNkGav=`^nVYwo^E^֥§O)}M}mNɵ#tU?'>ƻv<r5"_a  e0, H@f! Mj|E(_`,?y/_ ,AA\o<!G\et^V !UvYpkڶ$/MI9A'\W.VhJ4,-Qn q* g$Xx-s_;kNG>kkfBBӵMBB\|vY,/mmm8mK{|'K9OKߴ%`IG9Lmߗ5&dt ǚ,^ EKIʤiǩc1@--g`xܠ2kf`OV1I=:WM}/qӧۼ׬|2d3M·LK[cafAzIm}><ۖT6$  H`P@RN:m$aot2L7lr0+GɇO;y3GqF3),fQ!q i. #3'/qA'd|}rKmI q >e]`fᨯQ,-3NuY?sL8 /`b-| |.-}ږ67n_G|䝎&fD=\t<}DZJ^`6*NtdІ2CR,;_LGhf(KK_R=!$ض Zkܠ.@Qg( ݈ܲ6*un"l|Pݢq2#B04f t p|p䏺_v$\Haڳcmi [Ro?y-y {lٳ~fG=Bya.>7㕂!fa >&oVvrvm#<) i;;BWsilhK郡{7<2{B$  H@)u)h:r5y?k&bI%nLQ': #@18lN}>\養aOz],`HKS`IiJ?|X QO]&j6?Uy:FSGA]?qfv`–6ja2XN:XGg~fs| bo2F@4ґI$tu tbf< +#p<<0aψAq7 i7p=R 06|ҳ| u#:.2@d8-, ;Hg$=)< <Ƃu:wx^rPxC*gilcڰד]&u1rK̀J9fOn67+oۄ>q,D,Rb4TDa\K!cl$Æq, HI@$pzP@RyЧH%f %{饗^$0 4w?F S}Tb^/ ,<\{`<$ Y ,k1nn$ҒXpĢ2`MiAo'RS<& H@P@5&>f6lC^cpgt }kN4ĺv7`g{6E;3utYN' H`0.:/_yYHi$  ,Ŀ9)i0R :$ep;Bp>vBcKmMYj$ 뒮Z Q#XbEumMz: H@ػwou 7TOBQ2>[3Ϭל_( zocyu$  ;.w:0{7]{uENW_}l%H@:./D$  H@?:ӣS}ԭ>$  H`?^\ ˗Wt_^aA' H@K`t:?яV'NR2$  H@$p?կNwe$  H@$  H@y}x$  H@$  H@$  H@$  H`| ӧ$  H@$  H@$  H@$  BO4+$  H@$  H@$  H@$0>S$  H@$  H@$  H@N! Pa H@$  H@$  H@$  H@ {>Tϥ>%  H@$  H@$  H@$  H@ y}Ν}lxZ$  H@$  H@$  H@$0>S8E \ywWT{sRr/i 礤H%  H@$  H@$  H@F@t+q+%@܉jߡ}Ֆ7Tk_Z[mڱ:xࢤػǪ3vFo冕oɅw]K_iK@$  H@$  H@$  L@=q'4K!9[} H\8HZcX$  H@$  H@$  H@ IDAT(OwTw +fsIu7TOtu) sټ'v3?D,bHu#d e]6>{.g^y:AA;{iU; H@$  H@$  H@$ P@kV]BecDw 08Û~0,G1|~y/ kҋ30h o]ſEC%MM?ʏI@$  H@$  H@$  LF@}2~y>HDk:+Qn/3{3 }h?}esCW|Awٻ.׫{v۳Ů$  H@$  H@$  H@>)0~mkշvO 1Y:SkY/BKOUl~6:]Woy{hZ!y~Mlx/Ąmw0Z䄻n뺊 `BZv1z] 䇼lھ:tаI_{ve}0{'ޫgf0u\[G9گ\{cp=r#z ? ܥ+.͐{ B}s?ߗ8+6{~oZsB$  H@$  H@$  H@@;[fx?.C~|κ5`֝D_jӎM}K;_" l1H߮mo=m 10))ΚWyCV/& ֻ牏FnrdGx_<}ھ4\qoB1k^cdA>@ƽW_͗(8NgI H@$  H@$  H@$  ,L@}aFc_E,G,OTCg|>>;{KQ/0s:~3m;!]7qdl3okX}Vk$  H@$  H@$  H@J@}%&4󎸚])r^e~y/z冕"^ 4y_y9w_߶9N#M?x,>cum[aR;ie~9{sBXC/ ,0`tvhdq6mBo k1V])c>'Ͽ|uwϫXI@$  H@$  H@$  )I[1g-Lcfx#"oM_jXǬpnzWTo{bD6w]wm  SlY=&}vת[u)iGxllxLW||y pOD0zۈ9LYG)&>~#6@/gÅtN~/3.ji;8B3u!L3"JX9}|gp 1۟r~JSw+G/;{0{{q;{mkMs<Gd6Sݗ/hBFD~35QQ?}̶GzЙU@_c[K7x:dBзLjo4uޖ̩w?9.`Z͝J][{W7T=}ξ5H@$  H@$  H@$  )C~Y3a 4wNJ)"#\nܶ^G> 4GhUϮ^z}!,+W^wʫ*fi6zJLoy}4z0( c{L7Iy/{ki`ҕKA9qg mپo:qļJ!.w F6zޑ2[g&Пxڤ>3=TπM7UV/>Kc1ֱ&mzh^X+13ܩ.soD݉mSmq H@$  H@$  H@$ P@-o5z w!W ?t}˱wճ5쿼5p3KG7N /ȍ}y@ B,k8c;@̈`iβKѝ'NTy t1SW{[!?k֑&`D>9lղ꫗|4dKLxLr#qy6˾[xd/nEsmTiu50#=mmnrev7`6ǖ7&᪕W Ń:[;I'V82׮޸$h%  H@$  H@$  H@"|5Yجhf{Ix-ί~nu4631Oz/AڴcSf;+xX{S }u(X ^P0N`` \b:/*$0$  H@$  H@$  H@R"J˴J@$  H@$  H@$  H@(O K@$  H@$  H@$  H@R"J˴J@$  H@$  H@$  H@(O K@$  H@$  H@$  H@R"J˴J@$  H@$  H@$  H@̼o߾hO|bj X$  H@$  H@$  H@$0E$ H@$  H@$  H@$  H@X  A8$  H@$  H@$  H@$  H` (|@ H@$  H@$  H@$  H@X  A8$  H@$  H@$  H@$  H` (|@ H@$  H@$  H@$  H@X 3/z3vFb`b$  H@$  H@$  H@$py .? ,K@$  H@$  H@$  H@bP@_ ! H@$  H@$  H@$  H@3O@}J@$  H@$  H@$  H@bP@_ ! H@$  H@$  H@$  H@3O@}J@$  H@$  H@$  H@bP@_ ! H@$  H@$  H@$  H@3O@}J@$  H@$  H@$  H@bP@_ ! H@$  H@$  H@$  H@3O@} Eb7?Ϭm]W{ǫkV]S ퟸν}a>_0.]qiuuo~vF'QMY7Uq a|Ϗƛ߬`E<%S~o<" H@$  H@$  H@$0q qϞ:dfƯUe]6vmz˿an><|,~$yJ$  H@$  H@$  H` #M;6UW?puuʫk{wQ;Q~n~#\:z;G۟}0{a:G |?3=T IÐuKfpH@$  H@$  H@$  LD@}"|힏{ a9N_`sssCawv'N9 dG9a'eE/IDdz?E$  H@$  H@$  H@8󕋾20Ǽ// ש[O#=05_}b7nR]!3[<:g}O$  H@$  H@$  H`b #vuqw0ugtaحu:ScW.Xt<ϨnOY߰$  H@$  H@$  H@ 4[~^7ks!ua19Xwᾀ;Huc]Qě9QP" yʤe"ٟ"I@$  H@$  H@$  C@}j$  H@$  H@$  H@$ S)WfH$  H@$  H@$  H@!>5H@$  H@$  H@$  H@)G@+R3$ H@$  H@$  H@$  H@P@~$  H@$  H@$  H@$  H#~$  H@$  H@$  H@$ q(CM?$  H@$  H@$  H@$pP@? I@$  H@$  H@$  H@8ǡ H@$  H@$  H@$  H@8(rEj$  H@$  H@$  H@$  H`3/_~'osɣ~$  H@$  H@$  H@$  H@ y}x$  H@$  H@$  H@$  H @4 H@$  H@$  H@$  H@XЗ~ H@$  H@$  H@$  H@耀z B$  H@$  H@$  H@>_@$  H@$  H@$  H@: 0:kg+֯ ! H@$  H@$  H@$  H@O`so?'/_|~<" H@$  H@$  H@$  H@耀z B$  H@$  H@$  H@>_@$  H@$  H@$  H@: D$  H@$  H@$  H@$ O@}闡9$  H@$  H@$  H@$ (w $  H@$  H@$  H@$  H`P@_eh$  H@$  H@$  H@$  H @4 H@$  H@$  H@$  H@XЗ~ H@$  H@$  H@$  H@耀z B$  H@$  H@$  H@>_@$  H@$  H@$  H@: D$  H@$  H@$  H@$ O@}闡9$  H@$  H@$  H@$ (w $  H@$  H@$  H@$  H`P@_eh$  H@$  H@$  H@$  H @4 H@$  H@$  H@$  H@XЗ~ H@$  H@$  H@$  H@耀z B$  H@$  H@$  H@>_@$  H@$  H@$  H@: D$  H@$  H@$  H@$ O@}闡9$  H@$  H@$  H@$ (w $  H@$  H@$  H@$  H`P@_eh$  H@$Lh IDAT  H@$  H@$  H @4 H@$  H@$  H@$  H@XЗ~ H@$  H@$  H@$  H@耀z B$  H@$  H@$  H@>_@$  H@$  H@$  H@: 0]^}V?,$  H@$  H@$  H@$  H`>'#$  H@$  H@$  H@$ (w%  H@$  H@$  H@$  H` P@_f%  H@$  H@$  H@$  H{ 35D H@$  H@$  H@$  H@XЗ`d H@$  H@$  H@$  H@ {>TǻϽ!J@$  H@$  H@$  H@> 0Ν;kc& H@$  H@$  H@$  H@5` H@xwzk%$  H` ;}1F$  H@$  +S$ӂW\ߑ={N|/v&NRs݋Ցݻrsݺj/V;8"ڶWlY5Rr=plϨ͛W_~ߖ cw`։t,ӹ {֭+ |?z8'7/{۫?'~HӸx9G{|?.p11V/tS+?}|WU?#Na /vt /{k4DZ &8npCz~5W(Ӽ#4zث}y[ aPLi ƌo5}vP} NmjLdr`Ś ~Ljo%\{{#0 H@$  H@ )`:x__;rh rڧGicS&u}=V^,al落_LȲbE?B*5 ~AԍY= 뗗SlC@o{V(46 xatnc`V~/^TyW'}wI! ~O_pAat=)%Sr;=+:YӸX:xwGvKW2!I@$  H@蚀zD:C/}t0 #wwݚ{Vk5kj5ӎ_|}s٬l/6E c _;ukoq3+2ɞV^{B~`d,yٻys&&)k:9DhLd$'=p]^wyPG)G:HF)/~Q^~sȆ/Xٸqχv_&ofr}¡-~;gH~.~?%6nmI8u{umP qR㴝oU};on;;VX]5Ө]U|e8_#O3!Q˓8M7xz}ًJ+>3m\]{/Shxw `.:n}8y'^wpڶm&m3dEO9NۉY. ;<7]{m_;E9NRPcI$'%ef0^(O=ZEZ]N,߼.nSy|v=tKr&)qd~zvSSL/h}@x8ʛ5sKw>/G~y^~tbWW}eM9#>vhKi`͊R@>kc;;y}< cgZWeDoXRy~!>Gض FYr?ύCjr,)G#3Js0%~rt!7}ϳG=ad9 ngZ'G ڻN1h|dh[ Dppe}2s;Sqssq{}SM,Ⰴߗ^P یy yoly6i3ϰ>7#<tdnZܤu0iyנ*1B;阦N}di\~bw̨]8Ls鬮(q7e 7T \#{| uO=$  H@$0{ЧX&3! %k :%C`s49$y>19/>\),<옉O:TZi w,ʸ\t0;( YMg[PP q\O MҘ;RuFb;larN߈m[Z(;LƭQO`}< t}{lJF-G-xS9 Ύ&? xᦛq=Q#ޫ33\)4]:Bt<#csMIgSXϴ޸cMeʄcMz<Ď4<.6?8Ϝss^:ڶ?wU0>eJ&mA޹gV9+_W~7tfÀQ:ܾhgg)b"3- *ݤuog~oλ-~OS@,44} ,/p]|cMy(g V$  H@P@bd#;ٕ"7LزsYociy0מ^/9HC8ajKeiL\~VS MZ@^ !gϾgfp>"E@o♟Mg{X0bKĠ"f:-Л?`ݏwұnÊ%LHSnBgBq,t%{ MLӠi >=H@e;"?<x/o٦Dݘt;7֤ߛ ڿzEV'-yK@$  H@鱝ש m[wmrtbw] G79Ġ6Gb}:s~D^ss MXtG^9NKvYLϲ*IfE:s~J1#"5gO8tVc)`W*qmi~,M+E¦tѥkC@^(Op?h+ggјt"f.8nIxXMyapeq477>-])puI=*^yyrmBQ+8Oڧ\+f!͑ƺ]J\8&~~E|n~'ϴA 8ˁt{˦zޔ|PGup L!̄.ەzy"047(Ǜf 2\w>Q{p5irO+tΎc9/Rv>~s#:uYPkpG•khF'vϖ2MXaDm+Pu2ZK=V8:sö)mij;;)c";<0 -\3*] >t {(sbIb6i)YhE@sݺƙ9_ZT! qΉ-[;oӳk'}E|]l%ffMu,:Z66ٔ8>T `ږimq6zfc0g>ɓ3V 93XuQal|`|M)Jo]ق)gfgK?7֤ߛzZ@.I*%o H@$  H@C@}ePgg35B(l5ʝ9@8i4%q m$*;w08z`bq dAN:^w4μ`4 Ob)R$3i"lC.wYЏNOD\)UZ_:ٕrEq,(RVúrP(ϴahdV'K]MiG@/m$ K#QhnrTXb ]/xm}y{_ jb1α&?ez\1(]ާ9-0(ae=[D'gw!|Y袎6^z)۾}) }PZ k6e|q2u{~|o',pK@$  H@")[ԦY4wNJ9q`s3nԷ~49d H:FX۳PvfIT:ѷ=@O\̗^^yE>]ﳶ!adM&4ubЁzmv@DxX1 5 7]w]c'3XtYn4ˏUl,"ŹE$It<2yFyqMɢ cIs{/\iqwOU^mZǔlgCE=[k#`ڢMu|.5ebnoi%G~*##_M&<&} /dWm}vϽX;sdpTV=;kN3"n|u)7/!!2#SGh 2Gdv}tٙU{߸:i^Nۅ>+

..73IzfqlyB7^}uب~SV"->˰&2b\3Iӏwe])E }),󠱚 )?ai,Q^ΌᲟrbymbuQvF|q-BgS<[ = fq嬀8϶sK+;ڶmO8]# mE2rkbk[noɂHeVYWjWf,n3tï4̓I|x1BL)x4t+j]C(I2m:ez`M2 &~ӡ]n zifu7ftYpjcmägLSgZ5鶴J1(\t3)O0pҕsZ A~\"o⚶~{gߥSqtL'kg7J\/NV'8{E׎þ!kznk>|\mF[؃HY:(}Mλ,$ ebw< fqz.GxD M|yQ.|G< m[/; zKSA-^\?[Zy$  H@$  O@}|v><\k̰aҭμg.mmƏΜcp zt69:JS~q|36fVD'`\ϖ#:XJ΄<Öa#sX|i'sjZ;EY؇3:UEüeCV̳l-W:m<@:ڒ'ʭATe9Le6NP[??k6#ur}LSJGt=6p}n[ξm $~g[SMyNqDfde~f>mEڽ}G9C #a^~˸h0[".K!mb <¬_F\7 ~҄kfY#@|ضo3-L,z~F$.m9IxNSz#ox?&OcV\-lYc8eu+D¦:(}c&)睶y!73I3"dz\?~vo2/{/Pi<#Vlk:{4:ۦ }gƁɞM7i HOi6X5ўjzxAMR M,ibI9SD.]lFScCh:,E'ubǃ}׶tDv7V5&ߵMﲴ%M:X9H^0XM' H@$  H@'>}ŀ?:l͓08O&St4M [ֽ(@0%_P$Bґ @&1#I`` 4m=Ot2cpV?/F y@fHPƪ,?}K@x\˖ﯶrsWӮyn$  H@$  H@$  H@K >яV'R) H@$  H@$  H@$  H@K Ki$  H@$  H@$  H@$ G@}镙)$  H@$  H@$  H@$ )P@T$  H@$  H@$  H@$ G@}镙)$  H@$  H@$  H@$ )P@T$  H@$  H@$  H@$ G`={TGK)$  H@$  H@$  H@$ %C`;w>%ՄJ@$  H@$  H@$  H@#K@$!}WG} J$  H@$  H@*Zr{nn{pΝ[VϽzuȑ Cw{^v7x?cpGxNцÂN{¦_;qbfK_[zkQH/8ۺ{Ϣk$$  H@$  H@$0gl2DWW?'pWhǫcQrbߙŴaǎFN:箻/>~!F([˭ّh[lcDpݸkÆň8$  H@$  H@$  H` (Op:TT_ʞP_οuWWmwf&.'swl+z~~G{i VlK7O.oۻբz=G;mSe_߿/^rUs, H@$  H@$  H@}ptcƍ}MA>WvyKH)k׬z/)ׂ?:7<}{}߾9q%~{ қx۵WV?|Eݝ}xm۫GX=Ֆ]s28䏁øqy0׼q@WGhjOO&OsE'wY1.U7q{YkÆ4ݻqc@|y~B^~?V䝰;7T4tri HW2i'3~[]wj[ok1ν_7L"|_|91P%󏼰  wuQD-[cǖzl]f77<;/ʫKБ.@gS G]KXKW?Xk-ym:r"]Mg`y6y0`+,nﮭ{Ыwml}z]˘D~}}mAtŕɴE#STCD>tۇOWm]2 9LMRggΗ$  H@$  H@K@} em:үzyi.۶m^JWA۫n^Cy|n٣kҀȑ/ߖ@$2Wh4M? O2,F|r]1ׯ`.纞;lg|]3,QtD2~/:HM1\:ʄt}y zB4ܫ{)|J6mĸ-3u3u/r٧nVx UpƛD𓷷}^F|//OS@ǒB+wkL;ڄAybɴϴ<(i?|6Ֆ.ϠpYyr+ H@$  H@$  H@} uS9鰾jԘZXvP|p.ݰ%0,&Q8W#Lˍ"Rܛ'V7 ?Η:305QN Й7=T-: $#!3c9>VrTC@|$3kL/oL]+ $(7X_fNK@`y`ϖ/ods ,M&ጳE@&m`>ܬlбД,d{5Zζ 4vlzpKZ 0io[>`4{qi%Jsm%I .n%  H@$  H@$  @@} FcDfq^fMmRֈ.;JS>BmIX!zw5ցtqpɌm\8)TsT[ʇήKS\iwiǂBg~s>X {gN5o{z]qnx~:nʆ[ eI{ʺsˁY>p˘8Ȁ8?r}߾@v!6 b4t횬,{;ӿvu}:uEn`y`Kv1չm1w=|x:41ea3LB8D+t<ѫqW<sfS|)s <5#>+R4.ͧ#h N# ?85]mK0ˍhY*;XAg*v=00}U@'_2^w_ߘ?7߫yeEek SMO{]eB !104<–y "&sNJ mi8"hNv^D\y719樻kgaJWZ57.E8ªFv&'gK@$  H@$  H@P@B@ Cļ/&ؿUfIG4[̥0krv:䧒٥k%i(Y\&9i Y4,Y /LO9p`9'0'! ԧ_|6?۫xx/Y;1ز %3SZhP _C>XRcE wqn-k! >yhJ^?q[aG"x{c'BĥZ^9g,·8W3pW+Ydʼn @^v ']bL)nYPdz^[g~o?IҹNt9OYKq3αy-}9!+aוg8<*<y夝XD=ԬЉ8 w[Q5/ eFewn݀qjK0Mk{Lj YyM.9oG[<. H@$  H@$  H s]5!X˸ڀv-6w].ծcmuv|tv|<ܵqcN? 53߻BiM|-9XpFt tf-י羥~׮쪸M|麾f^ZRv]E)5JEB k8W'vEb.(۸<&A9,+瀥~mXo]lv|[x'|1qiԃx6Ktϋg鉄@m œ$  H@$  H@6I@}!:· O%Y[xzy`]H-ĹWW 5t2qw.{nKظI},cyiy`<=–"ԟn}uAD=ƛtH3٭[kmNbЕ9|Lf}o;T@|3o{ 7F)ܙ#ҲF0րXoF@0BTdiD,.,2?̨aZǚt><5&P͛Wîyu_wYﺫWV>W&2`Q|CzF-o(5{LAGzxAAǕ"–^h~ekEܿ$<4V &}PG4A;s( 7x5mazx|sύh{2e㴣ObUK@$  H@$  H@J@}("#H@$ K_bǻA$  H@$  H@J@}[}[$ mnq$5,a $  H@$  H@$ m-$  H@4?_#< mӟiN^$  H@$  H@6 (oRK@$x\sʫQB[$  H@$  H@ؖ (oO߲K@$M{Ӧkm~w5^&Xh H@$  H@$  H`&-[߾i'e%  H@$  H@$  H@$  H`^@kM\$  H@$  H@$  H@$w V H@$  H@$  H@$  H@@4 V H@$  H@$  H@$  H@u@$  H@$  H@$  H@$-Э $  H@$  H@$  H@$ `yfkvqG$  H@$  H@$  H@$  H`n}ӦM=}v$  H@$  H@$  H@$  (:裏6y{w}K`~>]zWs-&K/?ۘ4]v٥Y@?蠃e71$;~cUW]$H|2ϭG~?6O_R3QlX m5\ve+nfp,fѱ 䉦^c)Y1ҍ$ H@$  H@X ?5B{|;iN9xs衇=أ?O߼ysw;k^~~<;~ #8ymg^VG= w]}o .VY{ܳ|?{%L|OC=o?OLZ+Ѳs7vXh׏;K_RoIm7{(Iiq_r?h9䐆A+y7WO 9Xwr>?-yoO>O>d_{r&h>[Xϯ[k+t|^5ϼ坣xQG5^z)vl-+_rO_oag24߿>>WKV^-@&Qۯw-Cީ2w,ic=ˀy%7$me>z6JEg\wu62fr` ;A$  H@ bEz6@58<׾6p)BD+_|q?oyCYC; |ǭ;VD&uD=e᷿~e`aýk~]6+!I -"d+i\5L@l+3&Xb Ҭt.-qOZ:3X !Vkik0~=K>#ΰe`:kma<{0)ei\^3M65.q/SO MnY^x曛.5޵Z1y2`-k.[ۙHQC~t}Hc_}՞e^G`!{sB%]OóVV2vv=&i3ǩ/Q7Jw;K0 w})Q¤H{xyBw,cI|x;=[#?ʌru1I04nX}}YrGX2&u|X ϓVfU_u\I޷;s5ty&%ʿ,?6 =VoV^a=/ H@$  H P@4fyG1XO6Q.H#}pgulg0:qO xoA2p|<)\2@k\r,[p=B!"l=sÀx<L(֕s+"0Oܗ|t;s={Qr&reɿCD熠VĒQ&|~&ϧC\D BG ~HK{E;3ˁoX#)`5k@FRq&Gڻ:53|v]a6#fWu|]z,gBEr+Y[CLig:߬pN҆F`bHپ#`2zX H{(]йcWoZ߀(׺|SF۲IgLJr}&XX v,([ȴ,Ym32ާO!3&POu͌:]Nm7Vs|QID;33IL$X,|<ȁ)mLlk¸ :=![/E>oV`V$  H@G%5F8G}tQQ]"MW~͊'as`vy dv7́Dǖ@闡M}\ض.U:񺶵A,u]`1kθ+~mX^0 OH?o F&ɔ&Tw mY@-&`8JFn]vedaD6:2Ļe,I>#QQ_#aNkBUcMvmۄ[ۖja胴;Si eDܫ c+U-f;sKD,5YjX9,8Sw7/{};j3JqF-d0όt>~_|3OS LȈkbb|}r;eϲ56|FeLxLx+r|ve~{'/&/߬}yK9\⶜u0csd/!0Xpq,ΓV2/ ۖ4/~w/Ģy0iWHdwe7q4`$6&} O5+2~ oLz1Zixrs/'XD97u{wo޼e>s}ޫYix<)r`ɽq^6 JGη-2m>p@uLbXu6D&v,Dh}:g޵{·cXx衇׌^<&S?r*M !JW $z#1Ʌ 3˯pv̱=e*ۋa?XoG&}ȕϗzSOGPx=Y\j3~%FǢCƵLh[6M@mQԛQmc=_b"QLz;:VMq9; griDxġ^晴X*&֨ʴ~GB9{٢͊%/߬}n%  H@$  C@}Z#ƍ?e`5Q%0`2+9Ϡe` q2I~ς'5CH+󓭍ʐ3[3SZV8a"_e` |ı2sjsr|[ߊK G*kg"GoNA(!k~3!%U{k | DFz=maZni``)Sʺ-QtZ1 *t.cmY H IDATsBDE@^30 ٍ2,C̡,y֞y]0( coxr|kׄ؁GWy9īqm8ŝ'18L%jyf> a 'xb?! ;$a!Obcb? tb&6s%v'zI7ߤhwDŽ,<Ѥf/!ϵOZ `aăe5kbWޤS Lc" 11!!ri\n3J6Ɇ,Y&qޅu~FL8tҜXa2!Jӏ'uo錓F7@>a όgB] g"mtBLG[;߬}s6= H@$  H` >1E eǸo^ *AHذh&4Vk;+!lwY. .E9v,#ʀ[<"=QBw\[ےeIWGEm$6NLcMr\'QB~7"Ƿv)8Qa>ikmFEɓ儱썥tɞ=IJ]zNk@}?|rkWG=_(&Ծ? 43s܆V@g-/'[ Y@gb@- v냷IskʚY>pXZVṴ3-[`VD@g2\j&нu֜̊G <3/k[({kާ!{O`(!گa߯IGmaYkX19*qmqX#,A)ˬmB8D,gb_0zGN Ӵ1)bسʓӲ'uy<31-\.w0=YU@ǒ5Gp;D"wM(!;Wnq\1d"QxxOyoA;"OayYR@' O.mͺY?cӓ$  H@ sxy@k8d 0h9N@\kk@K\71gaZXyGGV E[`ob&! ĞWm \WF@g@,ak7#B#(E]T}aݳ(h\ |<BG0 <&Q\s`x]wU.GoB&Vuy`  σ,d2(;b#6,p^ 1ϳSIGYer溕{FbXL `È5/ L Flc}Q1jixnr"iی]V/c~mհRRƩ>B%dsޙa츆yg?ڭrcR OC޽Z?NeߑoI_ġ[!f3%DNΗz?8&FOVi Ikx&߻K/^mj~# j3"K*|MȌ7G h䣫 P{gk"ڍm;6jyv'/T} p}ѯbwK+]qCT@g{\=rюSfD2C< _~y?=\IGTF ϶6)[ˇzh/vWqYl`}$ygng{%\p +&&Mfu/+dQ!&“>"~?=ʒzqJcx٭qWm?;t11-?s`|&k/ޒ!Ǖ,1 fN`́!@tBҠ!.BEw--ΨGO?-Vt9ff|>m8+wJ5 Mw~<މ2Dq D7{[chCۢ- G E,qS^ 9E(m#<|YNo6bME~6cf>)S$  H@$Z@l DmzvZyu{ԋHյF;3>)Vffd i᧞z*oLٖz>T'QOS8ᴓHK}"K}8H$ E] 7L;9ߥNː'&0!mv:OSz{&>"dqzQ*O [-}?qH-Ou>,ڌ2"5]td-1,^gm!{SI:7WeZ&~Q&Na:Q[Yh/X!eXev?xm i;~Mjr }%  H@$  6ڹJ_.~ 0)k!Rlk*#>H+[ aZXnj~΀  >֯ 67xEٺ&Ghb6r!E{Co ǽgМ͈% (V0Fȃɜ^{MpCn#gև)&QK Vʇ{( VwS`=j!Ēe tY !$Ľ<~_wX8r^{~CS/=X0v(D;9P9QxwqG0ea@v܇X‘y-aK];}dK̜8ɑwM"#,<6}k(WއĠ7CE1z\wr0+gyfN xGxGLτCr$mzL~e]kK~L('ewݼ7|)Kv7҃,pw6ڨl&Q=3oiS]Kx6]79hDP/A"2=?3,ߝwٰnx BW{ӠCs-"Z܇uCfא86zO[r<eaON|wwgq\h3r>WWRc&e_c1G(/6S{M:r"}&qM~[$/ûuo˄ʖۧ3O>ӦAd.wXSrvw y"4x62xSGkLa'{ Goy﹜K@$  H@X A#m`-d K G@4i/iy2۞VB9\smW9]![8<5D>oyv_\Sے.9dKkPE^h-#sm<+C׾Vw q:,L˳dV~#.BQJr1Ǎ}D؏m >I+tď-5@,#(\7 l ڗx:~btmyyJ@^⾱-%."Nlc`\׶3EU]k} r1>e^xsN37L@fYY,([ H@$  H@kְ~7-k QnVwiH3,`U}>\Źnpg>M|wI GfN+#r)I@?=Pv!ߛxNmmFeX{ۤ}.Ҙ͈|9?Z$b6>Jy[;3kw 6'űimfK.iK[^%½y>ߵ_ރ" ҡ]g@QZ;f7No$ԡZ~+m(^eIr- Y{q"xJ]ebjZ}/$  H@$ @@}P3.qv͚VPtHbdЛy.!Hg w{^X+6\|X7E%0/ٳ ^i $  H@$  H`(/0/$t{タR$ y`}Ò* 'xB H@$  H@$LЗi H@X:X@>t3C'X7X77k@'Kߵ% H@$  H@Vj?/ H@KM7W_}uoΥΨ,}A1r O=TsꩧzyC$  H@$(jq$  H@[o9##8.[[2$  H@$  H@X^@߲eK7;l%  H@$  H@$  H@$  H` ,͟$  H@$  H@$  H@$   9Z H@$  H@$  H@$  H@\$  H@$  H@$  H@-$  H@$  H@$  H@$  LI@}J^. H@$  H@$  H@$  H@ 7onnfw\-$  H@$  H@$  H@$  ,%7mwah$  H@$  H@$  H@$  H`}P@_RH@$  H@$  H@$  H@Ч$  H@$  H@$  H@$>(h)$  H@$  H@$  H@$  H`J Sr H@$  H@$  H@$  H@Xs$  H@$  H@$  H@$0%)z$  H@$  H@$  H@$   9Z H@$  H@$  H@$  H@\$  H@$  H@$  H@-$  H@$  H@$  H@$  LI@}J^. H@$  H@$  H@$  H@냀xB$  H@$  H@$  H@$>%@/$  H@$  H@$  H@$ A@}}$  H@$  H@$  H@$ ) (O %  H@$  H@$  H@$  H`}P@_RH@$  H@$  H@$  H@Ч$  H@$  H@$  H@$>(h)$  H@$  H@$  H@$  H`J Sr H@$  H@$  H@$  H@Xs$  H@$  H@$  H@$0%)z$  H@$  H@$  H@$   9Z H@$  H@$  H@$  H@\$  H@$  H@$  H@-$  H@$  H@$  H@$  LI@}J^. H@$  H@$  H@$  H@ [li~fvZ-$  H@$  H@$  H@$  ,%З$  H@$  H@$  H@$ uG@}=R $ H@$  H@$  H@$  H@P@H@$  H@$  H@$  H@#$  H@$  H@$  H@$ I(OBk$  H@$  H@$  H@$  H`Xz}vm$  H@$  H@$  H@$ !Mz;<̉$  H@$  H@$  H@$  ; Z H@$  H@$  H@$  H@$ԼF$@/zs}57l_y[I@ܷ~{m; $  H@$  H@O@}9$  5#z'$  H`9 |ǷTt-Ͽ6W$  H@$  H`">o6\DSk{|os=ܼ7s ag=;Owi5ۤܓ$XϾZ>w}=K3/֜t`S&qզ{%NƹcJMu犸͘gǟz_>O- H@$  H@F#>crǟ~g&0h;ޑQQ {Zp.y;g/D3}gy/'?~5TR::Ag_ƣ>=ݨ?̓ompz~JK@$  H@$ !>c/V%{sWn| ۛM[^<\sm[?ߴgåU{y`cgcoJ<0{}Oo36˞5'\Hdw=BmE!<29C߾/AsX!DZ 3GWBLὧ72=]rSMMDA \pC rϳ@`Uܟ(z䃺e<t/$  H@$  H@C(4ij`X#/aP ˈUJtbX ^Xd1qOxAǸ{I$ʑw ג]q&# H` :>] u=slӊ0qt[H;D/b'y]*!2 Á "]&sq]ǽJ@Xx)۾-bD2{+@hctĻ2p]YB].ܱ4/q[;_ި q6H`xuGٿ] ›ӄ^z5 ܟZց{}poR}&x Gp{ҨϓxL~_Y`Xzn/ H@$  H@C@}Nm)3m>\n|_r+%ߓ J6 H@$  H@$0 sBoRVR F4 557 c@l -Džn>C˖4qMixzqON##_-xe7H@XJkN+xB@'\h}*Ҟٶu57m"+YPN^ ZoNnI/ H@$  H@G@}N-:2`^/t-rr{QB_淼%&2b"z>vm{7lRTV|Q `QtY /XK//g?ԋfFkcys&DqO?=.NTy u%e'ed׃H^GNk68Sv_$  H@$  G@}<^#΃!9V)R@/-9y\}~{HkwN\5"c^7td`F$nف6- 3祛fq+]5*><^;y9 ('<م{顄?9;pY6g\i|O%0OeU@/- ǥw\c2N[Q e?0DN\5ZcbS~nTzgb2;Vkn~̓FiO?Gk Q$  H@$  H@-짖oC@dmCJ={~~Ƿ(w==g^z bO&dEYbtŏ+I>ǚkzf9H`Qrz=ze}":qiW b}Lj}Dxv!hӇ.@ȓV]օoxᒝk^l}?}|rM>ԉY V$  H@$  ̏زvy?_ۍO6Ot:!p:9-= kb[B:Olc{sQܗ$ W !`X ,tf  H`=oްMfR[zt[m[ i mrfuMy6+: ,V7l9]2ҭvy8:}>^`Fec5,ᪿ':3jiTA<068I/ H@$  H@'>>+&b`6@IұHx@7j%ljXw}=˥:q x[ݏsθk9`5_s=s 9 %  U2mm;7h'q[qxsr'?|H K~v{oYeci20)^bKr7yj5^OޱaayOR@OR{mKt\]tၣ6y~*5W_{7a'Í;YJ1y7{( H@$F9nq u{ނjH']XRI@$  H@$  H@KJ@}Iْ$  H@/M[^py։wW32n  H@$  H@$   kYS H@$ x7\w>w7 ۾7T$  H@$  H@$З+ H@$ 監{9 ޼y79͙y9OTq^^A H@$  H@$  H`9 ,e˖fovi$h$  H@$  H@$  H@$  H`]Xz}]P$  H@$  H@$  H@$ЗA H@$  H@$  H@$  H@XEP$  H@$  H@$  H@$ЗA H@$  H@$  H@$  H@XEP$  H@$  H@$  H@$^@߼ysv5;4$  H@$  H@$  H@$v ,iӦ;]\$  H@$  H@$  H@?"3( H@$  H@$  H@$  H@ C$  H@$  H@$  H@?"3( H@$  H@$  H@$  H@ C$  H@$  H@$  H@?"3( H@$  H@$  H@$  H@ C$  H@$  H@$  H@?"3( H@$  H@$  H@$  H@ C$  H@$  H@$  H@?"3( H@$  H@$  H@$  H@ C$  H@$  H@$  H@?"3( H@$  H@$  H@$  H@ C$  H@$  H@$  H@?"3( H@$  H@$  H@$  H@ C$  H@$  H@$  H@?"3( H@$  H@$  H@$  H@ C$  H@$  H@$  H@?"3( H@$  H@$  H@$  H@ C$  H@$  H@$  H@?"3( H@$  H@$  H@$  H@ C$  H@$  H@$  H@?"3( H@$  H@$  H@$  H@ C$  H@$  H@$  H@?"3( > IDATH@$  H@$  H@$  H@ C$  H@$  H@$  H@?"3( H@$  H@$  H@$  H@ C$  H@$  H@$  H@ [li~fvZzfP$  H@$  H@$  H@..Zs. H@$  H@$  H@$  H@kZzZU$  H@$  H@$  H@F@}nhMX$  H@$  H@$  H@̫$  H@$  H@$  H@$  ̍К$  H@$  H@$  H@$  %K/o޼nwq-q5$  H@$  H@$  H@$,iӦ;1fW$  H@$  H@$  H@ 7nlnݺ$ xכyWf̄-[gyf&if"/b㏯ffrW_}k3ID$  H@$  H@$  H`({<%~=o}gfOh=c) yony}5&\#^xa'pB^s[w_s7$  H@$  H@$0H@}SOU ;S={O~~ysZ?>s9ge߃:_o|A{ .9裛N;[7xc,`uߢwc]kd H@$  H@$  H@X> Lmq?e|7"3mX>O5޻?w;~sy \@y 㺰@FH 1O-vs&\z?O:P}뮆|Gkr`;l q7,a$  H@$  H@̼bB "N0HCP$03{4w,Hו9nLA Art\GO?N:ygھ/O<>gpQ%\&7n~?=ϖ;8裏w/ ? 7xm C9ĥR䦭 R3Lk΂2K<Ǻ&Z޻5gLZ_zR$([ H@$  H@$W6zOac  XsM^W`[qΓf B9;9E#GO:E{ !N^y:\Sn~;S7ވ̣X`KQӞG?a2 Т~m?;'}gY1 ix%m{~]c/q}s&G?&޿d!x?rn&/A|ò_cb;瘔eWlٲ H@$  H@$ (j]ܼysuW6|'#<ҳꪫzV,>-e)7xc3t֭[믿WW]< 00|dhqUXW]x^cuq2쥽(a WX.҆#M ;2Y8ӊK]BTgW%X@bz7MjkN#Nk"V8p{w=t'sǵxg@nn׸hRs~g-7Kc3`Y~-kF<7F;τ6 f2Fdё~PkcaՎB|?x8?p +5)7mvP)!g=/br)\t8g6V9뮻2>' L8ad47 08e8#^m[yowZ)&}cği\òFkk"6lHc{gUcoP //o;Pp1 L\ E(qn$  H@$  H@:?b |, ct .cǥ`k@}mE~p-e9c iX"/'>#7BcQgPZ)Eܶ'\& "fzE@/$& #aQ /,s5`sdz+Vy) ,r)bNw#7گR xʸ[zKPN>ַO/ЖE/ǂ qo~6Y'Yni)'ڣh?zgĕ{6>Op3π~ tDFaxEm!KD&oa8"J "_ZbR n=}wo}٘}gbܿo\ʞ!׬G9b,,B_grO~ҊLXB 16%mOwtUg@q-W[yvmev5Osnb?E@A0 pI X.*`;L@'O0.9Ķ홷 WȑXo}'YǣΖlk"0G?+%& mny#N;<{Ek:gQ.yu9o~~Ya] ʾPNcs?VLn^! 0??LpƹF~bR.:L[?BLȓz'O$  H@$  H`, c-r^s+^`e ~C@ kpq*/a)!0W`('BFW` e"?b,,ky`*yeiY@/E=a4ݗz"yjȖX!/0ay4?(RnX`{c &Eymr]PVu{o=!b9ZA` tH5k#N]奖<+#1iÆ bbT "¤_+uOn&u&xm%޵VUz0gΜjZ )mՕYֱo=_zσ_~G8A๩TJWoy];_]*0#۵L{ ӕlXr!e'4#З'蹈hH|x蟶J=3w}] ֗_~yn{_X`)U2,vak׮-vhTb3{ oAO?vinO7bŊ[ZzSp g1l)^_|Eu*1l]7>t+gg\9K/UDŽ{?<1*>-D GVU㼏.XFkS&CuxizgR7) ( ( ( &i`yex5^@gyK4"i?aW]=琇>`AIG0y@dN: &nz/ȇiJ[ ]-xG˳XXn p›n_8 d>UkǬbE'2ZSxL 1Sex&d3f̨ygm۶UWAt}HkGuT˞/LsrMkE={\wX_l fo_}y};Pw)ؾ}{KYPyYA|Ȉ|w؉k5ekӴ~Ec ]FTXVi)1"-c=V}J?÷)؟vg A$*K]ҷAӰaJ*!q-[VӃL e:Ԛ$];o޼Ckڳ w}F%M 1w(l>b͚5%G::cS4{^/͵x͛ˬ)+H0E™;\r7R8+ o*T/_^mw/MTѺIb`ߟxtr1/ȎD" FkK5#,ZbvS* ;kͺVMצ8D>Mݯ4X4mWz >mJT< :RT@?}OeN) ^릯**JJ:u9^ui:`q/O15e+ݿ~q*=le{+ #Ou#+Sz衣Zs=e{ibY~{"ѣF_8~g͚5b{T&L{ "qM^AZ=y38cD+g}M:u Ryb믿>EBN,{N2M+cDUwV&P@P@P@P@ تc WM 2`^ IDAT/.h!,xf\0^tnuPFЂKԕy (0q"ױA(Au{֥[|*iŵ Aqe￿W_*] --g^1їr]NN1 瞫i{ {?0 >%&\MP@P@P@P@P@P@PK]BbGC<P@P@P@P@P@P@P@ Ztkz%_:v+2ηIP@P@P@P@P@P@P`@߃]w]1eʔeL=P@P@P@P@P@P@P@="`}0c#ok{'`iܔ ( ( ( ( ( ( (@*0gbi} ( ( ( ( ( ( (P|}{kf ( ( ( ( ( ( ( 4@oq ( ( ( ( ( ( (0>{ ( ( ( ( ( ( ( ` ( ( ( ( ( ( (K: ( ( ( ( ( ( (@oڴ2eJ1mڴ]p ( ( ( ( ( ( ( ԩS[sP@P@P@P@P@P@P@PAzP@P@P@P@P@P@P@&u[P@ _OpP@P@P@P@P@{1D ( |WmVzCL2w}_Ż۲oP@P@P@P@P@P۹ (.ꫯ4'p^wz9x7;8sj?[n)^xbǎgɞlݺ>}zqw'\fMqWկGַ~ob?|;A3f~]ve 7P,X{[o{ng4mذ9sfr|/?O ( ( ( (@лr1P@*hN禛n*xϮT_j(*b#xW`3O?tu|֭[g6:Cuw>>i6)JN:ӞGy4K_+ ( ( ( t)`K(S@P`oDC)vRuϏ?|@… :я~T?O6mڈ駟ޔ,OVoېs\ٝy晕%JwuW1w#q{Cz׊O>/~Q?qꝖ|TQx>|\YhYVh!~Oj;زeK/P@P@P@P@0޽UK~_K3*b_/j=^ЌqCNeiiCkn:Mo߾WcyN[L]xyzc]"@Q8D,/mqlu9OP;" ok@|:ꪫbv5%@)+VO_ w~//.7r_=Uzk*R9͋'{"'?NWT%c\{2K7_T{.cvQ1^{ bG{Jy{YcSbİS:ۣz?yyZ{bUP@P@P@P@JzWL-z_Wl6~[!в̋|i N"iS;vyZh^7c:Spuj.c|z ' (0\>Ƶ6)Ы@?M[V<͜9<N>!/ǩRw]=χT= u鶛{4{=Ҡ\Tv+|Η늳1w sDŅX^ _zUhecv#=fy~7) ( ( ( /`kqC,Z\{e ,ZЂ(wO~qeKm{LM>(@7lguVKi p^VQNf0"]&~$=ٳO?.@暪,GyEXr--yE_}l| LZ-AFU ~Gt0rMs3f̘Q};֬Y[w!*|Ŝ9szN[rDy3Zs/3HD -wʞN5#j=k\SOT~L+_KmVvi"v U4-Ñ{NJG#u 3j֭k)F|v1Ts"QY|D"NP@P@P@P@ ZiS_珠=P"@NYs0?8P8quTLJTN5) ( ( ( /`dJ_}A&2ЋRu) Ejx`]uôx[ߴ(KA3yGP?- cFpoٵӴ{ӬKvݘu;O\OJ47}sqѢ1 [8Ϛ` >{}~ۥa=7o^=)R+}d|<} bG&I[1?'pjPXRZazþJ/`8Ji=T7)e"E/~z{=)Z?c{Q@P@P@P@Ozn]uuUAe1/z18 Y*C.? uyVX'K.>-Dp'dYi_Z>nKv&P` GzL+8M6 wp\jz6i)KsCоݶ3{S]eb*=FzW14DzLsb~,l@=W@O"Qq>h:C5OsߜO?tٍ>=Ђb+W}v ( ( ( (xh4s ]l D׉)gi}~tvMr{/-xxMoq u2 s )'ݻwҦ6"Uܙ ( ( ( (f֮gg:--.1V8ˍf=Kˡ{キҔGydp<}M0>O/qy(C:ib;

{ŋ˱<>`O (0f͚U]O6U4f@F7ޮds{@jvyNp4ƍk7ˌF={M81ѢEe೩Uw@oj_þtZt1?3ؼ:yJ@J G/w\ .$*PitkP@P@P@P@ ˗/tZ.9):cؾ+PΣy<{N Xv֭:<3vjatLV@ƛ7\]K[T7?,߽ źtޔ[=kZsCތ#ˌFÑ?AӒ%Kvyk<iDŽiӹzڵ+};Mt6@|Av#H2LT$*|f7) ( ( ( &`}0ڵ}A֙g9b(;x5Zt\rZ7NL]qԣ:ѓ}Li oAP)iNk 4ujdb^zNSxΜ9սi} m<#6iӦ0t6Jay(G NO?+/˭Xʋu^+-ԇ}ϕ^{0aDŽ֭[W[dν8~ ,]f;s̪NU1٧zZ()M ( ( ( ( 9XCY{2AYT瞋(8t1ar1mk*p+{ Jp>Q"%?A|KrO4xUyQGL ( ( ( (0>S[fxM+Bly)}.jiiΏ=[|?n5ݫ.'O+w}-ɏM:uX&MZe @%(s)0~6nޖҔW GZva|/M ( ( ( ( @̯qmZ9vŔvٸqcR+3M,$~kZH1&9顇9y*;RymѢ]JqO.Fqnv|iI =e`g1 tMۗO>n (0ޮN7)Ѝ<~gϞMVܹskz6ڵkGd\rIR:Mg֬Y#lfGkqMVw-2E7Aoݝu|eh3=X xHT` 7=HcUFIiLjȋybX.M ( ( ( ( @̯tI7=XK{2V-$C5Z?3]GNe43.gx0Nk|Lt5RLȏ&P@t!MӴ|"zO*K,) ~-ovyx_*(K~/{V> 0:OP@P@P@P@!`3AP@P@P@P@P@P@P@w ԩSzQ- ( ( ( ( ( ( (80>EW@P@P@P@P@P@P@Y ( ( ( ( ( ( (80>EW@P@P@P@P@P@P@oٲg}ӧoIP@P@P@P@P@P@P@L`гVP@P@P@P@P@P@P@Q0>*f ( ( ( ( ( ( (0 #fyP@P@P@P@P@P@P@FE ( ( ( ( ( ( (x0>ގU@P@P@P@P@P@P@1@ߴiS1eʔbڴi` ( ( ( ( ( ( ( 0ׯ/SN) ( ( ( ( ( ( (0jGvbge˖O>;) ( ( ( ( ( (0>G}TK-/r_쵒_petoraP` |דuoP@P@P@P@P@FUU@5o,\ 'WU@VW_-=b}- =PY,7{2|;_oP@P@P@P@P@㏋o}[U: _s5=c)|AO-_*#Pꠃ*.bC-) ( ( (Y88yT"K}׮]Ue޼y_G?*:ꨂ xKp@qǏ[^><3P@P@P@P@. w 5Vi=̙3VvexP u4o{WnJ<@oao.\XZ9>??zi8'X'|?m IDATo+83+KN:4w#q{C^{8䓋&Z?Z3F|ǘ>~W-wN(oy{オy8SP@P@P@P{[%x ӅSO=U]$C@gɒ% wJ_u/RA_~iM6_K6lC[nP Z/k۶meŋ<0ʬ9.y/0>zoŧ~:⋲+cc5)l!YD歷ު[w$z8쳋c=\zE4^Ĺs;C̋{[Ѕts 7=ϣ}/`*=&;Mnz?t;\B ּH?sƍݨYSN-O ˰UE~ϐ QWs/\}~p!b|g?|+ ( ( ( `n+c*C,SL!XCbOk7G}c_׍A-[h!L:9{nm9L :o/*;Jdklٲ'?IzǾ['#Dzl.C\)C/S,%Ɩ%(`J⏮ ,]zu]vS`TyS뮫Nj}岜 !W-Nu,sUWj{X.RimŊa^ z_|q6iC0ߍXirt= =YZ)߻?)v_EZBu߿X$gqen+ms4Q>]"@'ZG>ÖMCu/馛<17 ( ( ( (@{&k-qXLVw߈ymMAȇcLɷ'X 0]hݴN7ia'ޝ%ДwaLN埿z,sʾ6fk&PWyMA91MV<<ԍe O.;]&!2in>T{JΫ&~饗Vee$*TD]C\Y'n) ( ( ( @o;]tQC#RcQpl64X.]un5O?<謃\P-6hQ$*-"_Zq)Z.k-[Y8TRH]Ȇ[nʲu]xZ3=1't8)f1V#Zg38#_ >CJ1/F`)τ:+|Ϻtey4 Q؇V#(4͛7?V_F[#x>ݺh뺊gYLٿVQQ w{kdb*BE2wESQb*oԥ{7Ҡ`!`^JTSp$NҞ1އq@@4hJ0;Gu~ez@MX.g@(hULkbo1zA{q_Jys}˪<.jyIP@P@P@P?u\k,У!bXitܑ#Ed|%Vb #Dv)uQYO+v5i)>ZiP>|]‘ \1 ڍ`C>,(M! m -V=R$.] 7 g zkP|wi?O=Gz19_yDfB wi`߾-r4FKO<:z+i- a\ hSAk)R+MO8+|~BV|?qQtߞ~4u:L:P`@LjxI1joW|KqM)szO5 cYz\c蔗+ ( ( (Y(}Mi@i]f~0n_"rxPK._mхkX6m6 EЩPn>+ZLb T+ٍ7֭^D/i@"Ӫ<]wUy͝;ZXtQM7p!p 馛e˖TzƹM&u)m}²iLKs׺MJj';5}mg ɑtuҊW|b>+};@Ow1>`,ÿ7?zٛ:U)7:t$*P@P@P@P@0ޟ[ǵҀ= 9[GZij \$@_z-ݦF}ҥxLwo(etZ%tu+48Ӻ1Vi11^nK[x{֍glucE#,uy0Hw nl_ @`>mce-ǞHq.Pɉ  e7s>yW0 Qk0|eu5#@O+oѥza#@mrv6!b{Z4ιBywQNZ~{#>g0.},Wʗ ( ( ( (@f>IMUI/Atua=.{ Uy< $@0(6-;y:(- #`Lw-i1h=h1:c 0!8VquU3~9<#<3p@ly`x˗׶"~7-C 8uiw=ݔĦ%C4b( _YCu~˫k0ӊVb! 7C t]\xq"tΦkZt1~{o:)1lBJT 锦O^xr~ ( ( ( ( x)Z䛊'G;N xȃAӞmw]}@ܻM 2soaZ @gLXz;w,֗C,-GޱL~J< -Moݮ+N ED.ԵP#aEp)7]c뼉!NtnST˵|WV7uM Qaɒ%Uy*49^uUzd<iǜiӹZڵ+};Mt6@|Av uny&Gɤ ( ( ( (пڮ>";mQ2t ;r]uzS7J{:V Z}@sU4?/bΜ9-jJ҇<,fNK:M۵r15Tnϴ]|TۨݴiSY9$1tk+Vu^+:mK/Uq෯)_7Z*:}Xwz盺Ĺou 4]gΜYrg:{.!bIP@P@P@PO3whk򀏇A.OlܣU9z뭖e\z-yylٲI/b7t۶mD<8ugϞ]|e_~yAeNZ6cOLO?{ 5-lRf]=PCN)]zʆ;K %Dz'|r=;o(Y& "1ǔr⢋.Z³̰[ ÂV/ed N3} ˾ v-tKQ!sn;4?,oyE|?>`Uqז;s9~9|wWyq.5U_(A];#aqlq^Gy]mpk/s_Cqݣ3N)eVt=o7ȟ)w}vR_}Y>~n*_}cY: Qf͚茍5üG74!HofRwҔV( ?C-uq.87b9nX:'t="NjgǛPGS:9C0q~8Үܹ7>-s,sr'gڴiMe#? ( ( ( ( w6{h]:M:ꨖm-I:B<Ƀ|xhz2W:/^ ĸzhzIi)J< O.J-ayGNZʗLTvԂ\ZxMϘ- 1K%\Rq>ДNlwީJ ru;lPe ,@3f(DIiK\ow7y8>V6jګ4xcx4øLv5oa)-I>S'`I2O%HüG|TJz-&7m3׵O2^SY BQvS* DaGe4q;C刨d:U@P@P@P@]zf]C.l3!""h>XKl`vyםv{H 24OsZ7x&haxxOJ{au(Ȕ^_SA ( ( ( (p DZm.< ]6ۉztoӵ("ZK\ﶻyLkL~/xڔ =mk3SIg)%oD7G XKZ>1㔖t%O Z~G#3qư'x E_V*L2W_=v* ( ( ( ( ( (0|}˖->SL>}Z\!yNgTP@P@P@P@P@P@FQ`GqZP@P@P@P@P@P@P@JzE P@P@P@P@P@P@P@,`}2}]P@P@P@P@P@P@P0^QBP@P@P@P@P@P@P`2 @G}W@P@P@P@P@P@P@|}ӦMŔ)SiӦU ( ( ( ( ( ( (חS{OP@P@P@P@P@P@P@JzE P@P@P@P@P@P@P@,`}2}]P@P@P@P@P@P@P0^QBP@P@P@P@P@P@P`2 @G}W@P@P@P@P@P@P@ WP@P@P@P@P@P@P@'wP@P@P@P@P@P@P@*/P@P@P@P@P@P@P@&|wP@P@P@P@P@P@P@JzE P@P@P@P@P@P@P@,`}2}]P@P@P@P@P@P@P0^QBP@P@P@P@P@P@P`2 @G}W@P@P@P@P@P@P@ WP@P@P@P@P@P@P@'wP@P@P@P@P@P@P@*/P@P@P@P@P@P@P@&|wP@P@P@P@P@P@P@JzE P@P@P@P@P@P@P@,`}2}]P@P@P@P@P@P@P0^QBP@P@P@P@P@P@P`2 @G}W@P@P@P@P@P@P@ WP@P@P@P@P@P@P@'wP@P@P@P@P@P@P@*/P@P@P@P@P@P@P@&|wP@P@P@P@P@P@P@JzE P@P@P@P@P@P@P@,`}2}]P@P@P@P@P@P@P-[4F IDATO1}оP@P@P@P@P@P@P@P`c>>6?P@P@P@P@P@P@P@ ש8OP@P@P@P@P@P@P` @tV@P@P@P@P@P@P@0^<P@P@P@P@P@P@P@I'`}rwXP@P@P@P@P@P@PN`7mTL26mZ] ( ( ( ( ( ( (P|}e}ԩCa3Q@P@P@P@P@P@P@PNzP@1 ŗ_?<JbP@P@P@P@P@P@!`}rgRnZpeߪuݻww O` b+.w26'P@P@P@P@P@P@F4~0fP8;, $[zbΝ48Ÿ2YP@P@P@P@P@P` @#ş^Sq77i{}LndHy/俞>myyϯg{5GU\5) ( ( ( ( ( Q0xNu~ꂠi<8£mI@Oև]q^?j3Q{<@P@P@P@P@P@ G(< >v_gzGKtE?s ]ۯݲbA4y,tn˺eTlXud?5P@P@P@P@P@P`@6T1*HZt KMk.^z/oxo;nZbgfW_oxxmkŲ[VКu_uB([^Ua`e+_QPn&e5]g>:þpeϺ-zlZS_h,w[najݪU,i^p|Oy*⃍C#W*ߋ+XBv\3 n )r9s),p~偯3̬ʻY,y^/(Ǡ$7[GmRIM+ ( ( ( ( (0>Ǟ Y6$ڷ—8i+tY^ӒqT4 &?sRM/Mi@;N+?[#t`nl/G l(6lKO#Ȟ^}낂0TvL6c] Ԝ yju9e'I]6b8@W=*}),!q |f]qs" Ƴr0őjاn*D-;FZ 􆐖-tW+~?O8&i|orӍaP@P@P@P@P@P@n w2y2YȈټtx%<LGl8An&48$ZFuyKߧۧtu@BEitzgHSzMb(42AL]}$G/\xbպUSP@P@P@P@P@P`ЇٚYJN2(GWt;K]2}ھ[(o7vt&@T.y7^׽uvy)懲 ]yö.|\@I??1xI๟D) ;d۴|kB$^/D@>  ( ( ( ( ( (0GQ8 H17)w: L'poH-5b ݑ8ů/.nY[|"F@_3-f?=l<7)݁eCc>y׵%@_e]~Ou]">4N: cҽ7)L{hʫ|G7>fvzSr-[7WXz D^LTӮS_+ ( ( ( ( ( [4/ %t~K57[ @K-5yNpz:6: Dp wXԬ09y]3vjWX7fP֥֙@up~ndzQղi΂y/-Iuyp̚s++8Zko>xiϨxw.HqޒG9Z9>aTs0bʱ{o{n2y+F]}DŽw>FU| ( ( ( ( ( S05 d|ݗjAYzO;XӺ9]&v0:θwFYޯN_˼*hE`x|Kl1ptYN }AwZ$K@g>z[`Gbo0uםW&`O߹ℛO֧ i5o O2a eUo>r1jx`|%]mc~T>`_/<WZG-ȃ ;^/PPaʘrs9ƚ1DeH׍<&ן-0L ( ( ( ( ( ( [(6?qy0u |;m)eN c=*Ss{Yﶧnk G^tO[L{'jW:O>?MiuO 6ruǑY4ijG1{r8SF\\{hA8_1"}9g9U6M޺u %o:a^tO~:TBgt^`|$: P f֖)" ( ( ( ( ( (0A֥n k~3V<>LhMܴLiQ}GOȋ4W@' @|hMmr:hߨֺuy:-,GHt?/hB,Qδ^,~`h`mnZW" ;χ@7=LDb|TKV t扮G4,SN_WXם>&^;T` ( ( ( ( ( (0GCuy~՗e[Zb3~:Aڻ]nZ*ַ1NuuF3ZMmdAP@P@P@P@P@Uk (2 KS*[E@GYbP@P@P@P@P@P68~ L6%HvٞyS0٬_P@P@P@P@P@x'1uP@=׽;ކ+* ( ( ( ( (cUX=2K ,m?Wݻ"IP@P@P@P@P@P` +S`__ܹb exwwϝ ( ( ( ( ( (@c>e˖b})O ( ( ( ( ( ( ( zZ ( ( ( ( ( ( ( &`7/V@P@P@P@P@P@P@'uP@P@P@P@P@P@P@z0ޛK+ ( ( ( ( ( ( LP[ ( ( ( ( ( ( ( u +;iWm\ZP@P@P@P@P@P@P@.|}悙U} -S@P@P@P@P@P@P@P7y ( ( ( ( ( ( (0>A ( ( ( ( ( ( (Л޼\ZP@P@P@P@P@P@P` @R@P@P@P@P@P@P@Mzo^. ( ( ( ( ( ( (0A On) ( ( ( ( ( ( &`7/V@P@P@P@P@P@P@G>ʓ.(λ缮.waeib9Ō{gt>ۚ`fv26-'o-v~c+<]h: ߜ[zWx~~s]}\3 /P@P@P@P@P@P@ iOw|Zw~}Ÿ孿>۽<]xmy<e1?mðFzWĹ?\}1P@P@P@P@P@P@> ״ vaeD7cScʥe\Lyl۱l~tM}1P@P@P@P@P@P@> nkVw,\VߤA-qn_MkFyP@P@P@P@P@P}[`m~"=}qgy\}1e?}zbC8G \45x ( ( ( ( ( (Ч>ڭvGxl?q'.xns}VI7>~c_y~E[zWx~~<}\3 ( ( ( ( ( (}@UV}lUO-ƵNRei|=瑮^ː[ #=[ʠDO=j(r{P@P@P@P@P@P( ( ( ( ( ( ( (0>; ( ( ( ( ( ( (Ѝn\FP@P@P@P@P@P@P` @T@P@P@P@P@P@P@Fz7J. ( ( ( ( ( ( (0 OC* ( ( ( ( ( (v IDAT t#`%Q@P@P@P@P@P@P@'!vP@P@P@P@P@P@P@0ލ( ( ( ( ( ( ( Lx ( ( ( ( ( ( (mOVZ-ysI72 ( ( ( ( ( ( ( ,0=+( ( ( ( ( ( ( (Ї>\EP@P@P@P@P@P@P` @x=R@P@P@P@P@P@P@Czh ( ( ( ( ( ( (0 Oc) ( ( ( ( ( ( !06m*LRL6sP@P@P@P@P@P@P@חSvG. ( ( ( ( ( ( (@@sP@P@P@P@P@P@P@'`}SHP@P@P@P@P@P@P} ( ( ( ( ( ( (0>{ ( ( ( ( ( ( (Ї>\EP@P@P@P@P@P@P` @x=R@P@P@P@P@P@P@Czh ( ( ( ( ( ( (0 Oc) ( ( ( ( ( ( !`4WQ@P@P@P@P@P@P@x'1uP@P@P@P@P@P@P@0( ( ( ( ( ( ( L<G ( ( ( ( ( ( (}@UP@P@P@P@P@P@P@&wL#P@P@P@P@P@P@P@> * ( ( ( ( ( ( (Oo{O@ EH6Eo{mrӸ7nq$^b[lǻ[}lKdKVkEIH(Js;9Y!,3gμϜsf9@@@@@@@ @oM@@@@@@@f;       @M         @}#B@@@@@@hB 46A@@@@@@}rD       M7&       O}S@@@@@@@ &@@@@@@@` Ͼs!      4!P8E]         t|^a               D}H@@@@@@@5@@@@@@@`ϒa       &`8…^ڑ5       5:>@ .a        (pLC3 vMÁቹy5      @vvؚ4K·._N)M @K;F*NT^?/{oʑ!    99wʧȩb德vgt) @=-wM麺ۛ-;R{n{CL@@@@@fl938.ypS%pY5Ro}RZzOm jO=bEuu-3GeރUߋF      0[g˙86~(~~k=[l!`3<"[U3~7kӽoڞ-kZ*-Ӈ6W~/ui9&v     @;ۡJUC'*aBZzܢ}aއ{º#Uܵ>w}5ZOjs\={p}a}A)     )8N ;/6 S]hqdwG뵭=]:Bמ;.;)Q_ņ]a<b. oD|ϗo_*u++YG?-/vWڸxsmcbᰦH+ktG.'N]_m=t,YZm`YwckpnEm@@@@@(C!3TV^E;/,R@fEaCo7UKV3hV-Žzzm[[j[{ v8X' O,@onXsQ]rzc遪N}0ŏۘMh@@@@@m r*aKkۛ n+ Tpo I*qzEKr-* EU>B|оbwSmRTOO[sXaɂI.^# Z\+@ꉮYzmzhGS+z  }Kw_-zz>Ҿ}꧶V/Z{\l;u*]`^#     67[HB [wjz[L{ۺzdPq99toXnaޥ9>5K 7Yޑ)ZS浶wO}7>{ ic^ x7qo/Ok7>!B5뷻H9xmK6nx}MBrcw:`LeXt=W{ێ]#lغ,uְVqY"     нFI33/F ÿnc:G[Q3үk8{gX:R=}]gxGS@ ?Xu.`~ia˵M*@=@MG%l?Zir.c}];V NuiT;$ڧULF8y&hwoeYt=Xǭ{tY4R]9XWFV}n7v     [ 7  @ υX}]7 O:Wxm{r)GV? Ϸˆ_f+\SB9Ta=L`۵t[}rkB)TEξ.ކFO0еl9z!P%ߒ ]kVjZgRFN0龼|AJa}pAӟ이 v 杛OidH7 ʯ۞;_<8oӇr=9YQz@B eL6'?sW =GU^^?w >* }˲{p#凼?/o/?[{pr      @^o@ TS%zsm]=[Ssա9Se:GߓаE婏rjxz_|xzl }ݼFL2ˣ]ܽ>_k͏^VyrAVRe˲{p#zA0Ǘv7oY={GN]#@GpV׼螤\vkXƺskdz|/X>?Gz<{vn$@׃'{ӛ_q@@@@@6Wh!h@!oέJwpT~n}j     y6Rs* ӽYoK?̷0qf]x~X۽X[Ymꌃx.Y 9څeso[]qHށ9mTOj˾/vg=Rz~N:)߽o{6VS{˗B)|0cml= Uǚ#ݨ_}ƫLw[=# =~*usAה/F]q0v-i}]3vmh;Vzn+փ7*j*ui2z}۲bww>Y=4mtǩ!,:>0TskT?|?Fapw預=yjY=k@@@@@L$J\j\89]{jZ)a֍{zq̾ey a#j3^}I#ƿܵ,vʁ*[%ENKIMۧgsC 5"Ej{<{߃IC=/w]^A\Q.<=ǭx^VoTzڦkթ6(4Kמ0~un4zJvP}X\]*0I)+Sz1fך8]rtSצ oX{п)LA@@@@@`Ч 4OS- 0k g-     UU$@Cks  ^ajF斀kV&v\S.[J-     s[} Ͽ!Ħ^k>l  @w=u]DW;F@@@@@1Sx~w7+@`|uރoeH@@@@@@J}84%ݡ#'jF0W      ??\tE-@@@@@@@&zێ@@@@@@@p       sW}{@@@@@@@ ; ^"      ]{9r@@@@@@@'`8…^K@@@@@@@? /r@@@@@@@p       sW}{ *'}@@@@@@` 3!u}ܷ[oݣֱ%      G}K޽asυ5<ߦg Gdjs+kr      0W@`({~g[^xY@@@@@@@N:X T%^[~o?rX{zrWhx     s{$ IDATD} N􉱱p;\.[<\u8{:3OUϙS؁a`l N?mvM̙*cM&ۯqhkWܼ95"++Â8|T>0]|'0|m,@׽bpӦ:yXez_Ͷ}D7۷kd *C7ݷt<68XÑ={{׺O>q"]eٵX`Nr&R     "@.3g) ? {/j^˿lkB?*wҪ:z2Woڵf ?MaO\?xx[ϭp{e%$ s7}46uqnSnC!Зzk*Z}+3ZEui{|Q[QЯ=oeܜq4z7G[ n5u=X=v2ݟ|R9to      0tz-* L,8eZUVUG mɖp-Bv' Wpu>=cկc7mNh*zKgtP' uW{OVݮ_6Z*hNmr K;ʪ{~[W=^Y{?7ww=ImtS;      0t>4԰|rȼA@@@@@Ctb4G6}KWlu<HpqH}; 8YruU4/{#=lNrヘ mvǥ̚WC9J=j׎Ύu6Gz*LǢךg18|y6TZ=xz?12wkf`=R+@ל۶ *ǘ 9sgT ]?BZM[TuZ#yk{ӕ*uucW~<>guoԈK6Wqs8Fn̓6={ឞrV%{8IV//o@@@@@ m>K 4+pAC*9BCY/VǢ/q˸n 8llOgsgϜ c9qtkui[%zr(.hd_oٺqm}y ΃ߟ^kHx 3R̙liww]r -׊>x59}C,Ynχ[[җzPH=vjTTy~?_G*=ů׋WpF CUM{x ۋ=:8>@@@@@@CtbRR2͉7 ދV1}ڧyj+4.jF}f(ului߯_ZB4_Ľ]}_ؼF_U ޓ P+ﺫRz_?P誛i2t-k/_qi"ݤOuOjz:̎1v!t=8@@@@@&@ކ3a~-Tg W=<5\Fﴌq5+SOz4 |J<\ևS{/z]ZQduk5Wn >i|dCX"0K#@Gr׻(ֿl{0W=D+!u~7olF{S*}:<Z{_:L@@@@@fzl Su\4dPp\+֜fBb ]aRkv޻li~ەuN޶>UC=Xm=-kueg5JtǞ?!0S#@߽pazuvں+Z/4|\>*mMZltwߝ=Ȥ:OMLC]]aˋ/&j-:Lx     0pV| ^pVΜ }KW /ܻhQ.4>68]ye%Q=ٮyU3Q\磏*+ذlgTc/l^UpJ(NKԓah۶J[f+L3㡌ՃSmCū>}fp[*, 04tze0s lx+[$w],p|h([C|~얛+zQ4}j$Z$P_uosM_u=U׺m72¾{| ?_ؼ\[Z}Gh߾G} Uد,I;փ`(z*)^?a{Z |6,ի0FJ*W\_WWk5]s(w-ELWNמZQh_- uQ— O=UNu(e7O=ʦ _ЃAE%+h;}::jYY"     0#t4ܭ |d׮*zGx뭬5*YFz<~<9wVju+,԰~hu =qHejʚ!hXZ=d5$uek~?י;M _tcNׯ+\/ _?H7F3]OE-_}`o'HooH ?nua=Y;J!uШx< =Z*?:]رRٳr8o뤖 peh!xoM     3QgMCX:h.p+ۼ_U}] ?t({`]c->ٜ 8>TO=)~;&Ys#z6-}<12RzTgt}BsV O<5kt9uԜ uۓCRopde6߃';vG@@@@@`ЧZ!      tzG       0S-@@@@@@@:R#O B@@@@@@jg       )P8E]ԑ4 @@@@@@@`vt|>;9 @@@@@@@N @3D@@@@@@@D}J        @ w}       S"@>%@@@@@@@:>@ w^ ;ݒ!      ` .4@@@@@@@; uhOS:      4.@޸ٔoXXc${C;ThX%־ii;EgFLxlTX}$w~~*d+@@@@@H 'Y:ܽV[hmFZyLGN6Ir]-_Yҟ\?x{Os=7][z鍥rs-A@@@@3<悓z^v^~S덟8Vn l rEhXe8 HmLso=<MY[MױhnVz4wҏO< _m=>^;^6g|alTV׆]A窞}*@C94ǼʶcUANSUttdeWädi˲ms=?AbwpmcyŶ9>zo94:.?UC+{YתA&\׃˻W tm7ܮªæ+m_WUOC2(w?wC.p !4wzjarDǼ {kwsYG0Oǥ.nܢ}ᯮ-LքL5'Oy6jOAvVBdE!dWv7۷-u,7c:sڠy(OzonX]̾SZYɷ.v[_ڙ5cpDyKZWX\欷6i߱.KT[v<0/sm6߽qMVBK\U=7A(T ϟ>ڮ LK]oVtL!ekֵe=RrNj[w:>ߍ2QyyIǥYEVVո-z?]߃-/SPH#<0=Uז~hпkkoCzEw&]q=Ruk}Ms߼6ٖrUvuO%     @y@/2i#IoSoxu[Z͉Z*}׳TEE!|Q REd6|ŵ5W_ѺYǩ^#uQOU y^Ы.y(JVu}#ֺ8.>4tաg0ڗ_<{htMsB?ycWU/U o_!\̶(u븷}kf0֫g*(VF7hWڗnj`J#Kǥs{M)`,k5u[^R#lO}WC-j~ Clשac5zJ#hz*>@@@@@(M4s;4^Kt{x6DOy-\m^5+OZC*[!Hܳ(@W] K}5^Ѿ=|쪯*/ڿ6|u07DBé۷V TVjBYocmQk-{D c|x4w/9 4Տ={az{*C Ė[_ګ,hc1뚉豳khf6k^ԳU/}@Z]ZX}{{+jMVvlum u\M޾uN|;֔,Iߍ׈vj>o+6szZ[rEe-]֠!_ׯ[G\CU*sw]suIpMmzڢTA@J_v}(8V pKs2sSHu)|U|`baG#\!7Q/TQ t5?UrW}98# qA/sEëvB{2 dy0UJ=E! ˗8Wkh;Cs|dd6'AQLWQP+*o-?PYWۨ|wթe";5|}jhi_Z eoAw_+@~55Cמymۯ/e_w3υz !'_dG&(zoZ ЭB#! =s;ݿCRZ_0X=#+k=kW~$k]?{J|xyT1d+|񾭺ťg\@@@@@ @/׳R['֘x8<]P]J;^U,Ӹ7om= !-w$_*7zp8Yq*as=8@/>pOAᖆuC s:£1539IDAT8WP^h|oŖqY*ǮzNu,@O'st󿛲InMJ{Vue^^q;+@:]}wMK=WUq]})~ʂ{#^S$Xit kF0Kϸ#    +@^gԫ/{;z:%@Ju~vZж ez=[5}҇at޾=.#@}ʗx8@/}7:(Sʺ˸VNGL SI*@Rz״ hyM!?k@_>䏷>~HIj鏉      o |Oe^.zHft wړ 57zu1Eǭ}6Q.{/!&;QcXݹ1SO+?^ֶm멗z~4$twvXwmWvƿ\珼'3J+Sm@jl:I6e\e]tqs]hjHSḎcu U7!EKZ<\F }GZ>aݵo;͹noʶ{۫fږ     >6ƽi5u;t߳[J 5BɊoTqVW{rEzθltTCU_lֲ2/\'㲬+?qk2t @zka @='Xa*~W ,R;=`%f[*p6c˲tbgڬwcteV (˸˺Vc^z0$.Avz`JYaj: =k[CugW'z6>w1֋ڦKtO4=     @zVj01¯z}!cB]!+}EI] 5gq sej6w ൮>x=2]~m9wU\ڧ[^ }ASXhm~)c[J/h68@ܴ{Au։6f|j<= 퇑WxjTNټݶ :^=4_4]aVշ{$Dw:OVt}ߗQ(aX_|#>:Zߞ;oKaus.hš޶WA.BM}?݋Рҁu^68rªeJMб(䍏GװS-{Hzq]rt n=`ρ~~יZs8އZ:_PסU-E2@.[j;-9>hun}igнzH7?ʆߧ?&^#    +@^g6 msޥ* l=ͷ*qoc[?T(KsN ԰Ӑ>@m?x.6԰qQh׳:>ؾdQ ǂBk'@~eBh+ u4B(eơ:,Jկ}Xo=}<|oS\Z:NՓ }+T{4J/~x}]7@/      gYUSsGz҇QX^nz穮CHQ/\m14y*@?ۣqѾ:TI,~;{K( l>0jBӟ\am_,$7ku;^+S}ќ-5\8CrՁ/0V9^8z2b곞~2_Uo ]BL?REz94:!nSjCv\eBYMaPv_ ǭ/uۦeV QO~ȖʶB5}Fn{ -oe>=,@@@@@ @/3WT8$jxd+y}+TXTCǺ_= k΢~Z!ְPZGÉoߛ5#ɵO8~n۟V=U}:5y(EA V ȼ׾;G!cێYK6}vZ.kR0*~ۺ~8{*|. ]]T4Ajhf^ݯ䆡W] 9$zްu! 7qװ}I'pЎsb۷01-uJ+{תU^tL?}zZWw2jQ|ˋ$kM90\7@^XՓo0REe?ڥoi^pеcve>}F@@@@(Gǚ(LVjHn.՚K]eH,gX65jvVt+<΍V8px" ptnT^ғէn5Tǒad4ڮ}j(oFѾuH;~뽆" 'No ͥmn=c0ߩq"U}j9Ƨ-}mzb:}v9ilfV.uukK׭FsluK g^P=x6)0W=Z?o:u;#]*C>,@@@@@f9W@@@@@@@mĥj@@@@@@@#@>s-\sp3@@@@@@@f9U4t h?xI{n@@@@@@f8M4r& G@@@@@@@CCC]t\:/,8tDⱮ4Create and register a `Watcher` that will invoke the given callback `fn` when the `what` item (`value` or one of the Parameter's slots) is set (or more specifically, changed, if `onlychanged=True`). If `queued=True`, delays calling any events triggered during this callback's execution until all processing of the current events has been completed (breadth-first Event processing rather than the default depth-first processing). The `precedence` declares a precedence level for the Watcher that determines the priority with which the callback is executed. Lower precedence levels are executed earlier. Negative precedences are reserved for internal Watchers, i.e. those set up by `param.depends`. The `fn` will be invoked with one or more `Event` objects that have been triggered, as positional arguments. Returns a `Watcher` object, e.g. for use in `unwatch`.\n", "\n", "- `obj.param.watch_values(fn, parameter_names, what='value', onlychanged=True, queued=False, precedence=0)`:
Easier-to-use version of `obj.param.watch` specific to watching for changes in parameter values. Same as `watch`, but hard-codes `what='value'` and invokes the callback `fn` using keyword arguments _param_name_=_new_value_ rather than with a positional-argument list of `Event` objects.\n", "\n", "- `obj.param.unwatch(watcher)`:
Remove the given `Watcher` (typically obtained as the return value from `watch` or `watch_values`) from those registered on this `obj`.\n", "\n", "To see how to use `watch` and `watch_values`, let's make a class with parameters `a` and `b` and various watchers with corresponding callback methods:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def e(e):\n", " return f\"(event: {e.name} changed from {e.old} to {e.new})\"\n", "\n", "class P(param.Parameterized):\n", " a = param.Integer(default=0)\n", " b = param.Integer(default=0)\n", " \n", " def __init__(self, **params):\n", " super().__init__(**params)\n", " self.param.watch(self.run_a1, ['a'], queued=True, precedence=2)\n", " self.param.watch(self.run_a2, ['a'], precedence=1)\n", " self.param.watch(self.run_b, ['b'])\n", "\n", " def run_a1(self, event):\n", " self.b += 1\n", " print('a1', self.a, e(event))\n", "\n", " def run_a2(self, event):\n", " print('a2', self.a, e(event))\n", "\n", " def run_b(self, event):\n", " print('b', self.b, e(event))\n", " \n", "p = P()\n", "\n", "p.a = 1" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Here, we have set up three Watchers, each invoking a method on `P` when either `a` or `b` changes. The first Watcher invokes `run_a1` when `a` changes, and in turn `run_a1` changes `b`. Since `queued=True` for `run_a1`, `run_b` is not invoked while `run_a1` executes, but only later once both `run_a1` and `run_a2` have completed (since both Watchers were triggered by the original event `p.a=1`).\n", "\n", "Additionally we have set a higher `precedence` value for `run_a1` which results in it being executed **after** `run_a2`.\n", "\n", "Here we're using data from the `Event` objects given to each callback to see what's changed; try `help(param.parameterized.Event)` for details of what is in these objects (and similarly try the help for `Watcher` (returned by `watch`) or `PInfo` (returned by `.param.method_dependencies`))." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "#help(param.parameterized.Event)\n", "#help(param.parameterized.Watcher)\n", "#help(param.parameterized.PInfo)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Using dependencies and watchers\n", "\n", "Whether you use the `watch` or the `depends` approach, Param will store a set of `Watcher` objects on each `Parameterized` object that let it manage and process `Event`s. Param provides various context managers, methods, and Parameters that help you work with Watchers and Events:\n", "\n", "- [`batch_call_watchers`](#batch_call_watchers): context manager accumulating and eliding multiple Events to be applied on exit from the context \n", "- [`discard_events`](#discard_events): context manager silently discarding events generated while in the context\n", "- [`.param.trigger`](#.param.trigger): method to force creation of an Event for this Parameter's Watchers without a corresponding change to the Parameter\n", "- [Event Parameter](#Event-Parameter): Special Parameter type providing triggerable transient Events (like a momentary push button)\n", "- [Async executor](#Async-executor): Support for asynchronous processing of Events, e.g. for interfacing to external servers\n", "\n", "Each of these will be described in the following sections." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### `batch_call_watchers`\n", "\n", "Context manager that accumulates parameter changes on the supplied object and dispatches them all at once when the context is exited, to allow multiple changes to a given parameter to be accumulated and short-circuited, rather than prompting serial changes from a batch of parameter setting:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "with param.parameterized.batch_call_watchers(p):\n", " p.a = 2\n", " p.a = 3\n", " p.a = 1\n", " p.a = 5" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Here, even though `p.a` is changed four times, each of the watchers of `a` is executed only once, with the final value. One of those events then changes `b`, so `b`'s watcher is also executed once.\n", "\n", "If we set `b` explicitly, `b`'s watcher will be invoked twice, once for the explicit setting of `b`, and once because of the code `self.b += 1`:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "with param.parameterized.batch_call_watchers(p):\n", " p.a = 2\n", " p.b = 8\n", " p.a = 3\n", " p.a = 1\n", " p.a = 5" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "If all you need to do is set a batch of parameters, you can use `.update` instead of `batch_call_watchers`, which has the same underlying batching mechanism:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "p.param.update(a=9,b=2)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### `discard_events`\n", "\n", "Context manager that discards any events within its scope that are triggered on the supplied parameterized object. Useful for making silent changes to dependent parameters, e.g. in a setup phase. If your dependencies are meant to ensure consistency between parameters, be careful that your manual changes in this context don't put the object into an inconsistent state!" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "with param.parameterized.discard_events(p):\n", " p.a = 2\n", " p.b = 9" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "(Notice that none of the callbacks is invoked, despite all the Watchers on `p.a` and `p.b`.)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### `.param.trigger`\n", "\n", "Usually, a Watcher will be invoked only when a parameter is set (and only if it is changed, by default). What if you want to trigger a Watcher in other cases? For instance, if a parameter value is a mutable container like a list and you add or change an item in that container, Param's `set` method will never be invoked, because in Python the container itself is not changed when the contents are changed. In such cases, you can trigger a watcher explicitly, using `.param.trigger(*param_names)`. Triggering does not affect parameter values, apart from the special parameters of type Event (see [below](#Event-Parameter:)).\n", "\n", "For instance, if you set `p.b` to the value it already has, no callback will normally be invoked:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "p.b = p.b" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "But if you explicitly trigger parameter `b` on `p`, `run_b` will be invoked, even though the value of `b` is not changing:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "p.param.trigger('b')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "If you trigger `a`, the usual series of chained events will be triggered, including changing `b`:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "p.param.trigger('a')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### `Event` Parameter\n", "\n", "An Event Parameter is a special Parameter type whose value is intimately linked to the triggering of events for Watchers to consume. Event has a Boolean value, which when set to `True` triggers the associated watchers (as any Parameter does) but then is automatically set back to `False`. \n", "\n", "Conversely, if events are triggered directly on a `param.Event` via `.trigger`, the value is transiently set to True (so that it's clear which of many parameters being watched may have changed), then restored to False when the triggering completes. An Event parameter is thus like a momentary switch or pushbutton with a transient True value that normally serves only to launch some other action (e.g. via a `param.depends` decorator or a watcher), rather than encapsulating the action itself as `param.Action` does. " ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "class Q(param.Parameterized):\n", " e = param.Event()\n", " \n", " @param.depends('e', watch=True)\n", " def callback(self):\n", " print(f'e=={self.e}')\n", " \n", "q = Q()\n", "q.e = True" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "q.e" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "q.param.trigger('e')" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "q.e" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Async executor\n", "\n", "Param's events and callbacks described above are all synchronous, happening in a clearly defined order where the processing of each function blocks all other processing until it is completed. Watchers can also be used with the Python3 asyncio [`async`/`await`](https://docs.python.org/3/library/asyncio-task.html) support to operate asynchronously. To do this, you can define `param.parameterized.async_executor` with an asynchronous executor that schedules tasks on an event loop from e.g. Tornado or the [asyncio](https://docs.python.org/3/library/asyncio.html) library, which will allow you to use coroutines and other asynchronous functions as `.param.watch` callbacks.\n", "\n", "As an example, you can use the Tornado IOLoop underlying this Jupyter Notebook by putting events on the event loop and watching for results to accumulate:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import param, asyncio, aiohttp\n", "from tornado.ioloop import IOLoop\n", "\n", "param.parameterized.async_executor = IOLoop.current().add_callback\n", "\n", "class Downloader(param.Parameterized):\n", " url = param.String()\n", " results = param.List()\n", " \n", " def __init__(self, **params):\n", " super().__init__(**params)\n", " self.param.watch(self.fetch, ['url'])\n", "\n", " async def fetch(self, event):\n", " async with aiohttp.ClientSession() as session:\n", " async with session.get(event.new) as response:\n", " img = await response.read()\n", " self.results.append((event.new, img))\n", "\n", "f = Downloader()\n", "n = 7\n", "for index in range(n):\n", " f.url = f\"https://picsum.photos/800/300?image={index}\"\n", "\n", "f.results" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "When you execute the above cell, you will normally get `[]`, indicating that there are not yet any results available. \n", "\n", "What the code does is to request 10 different images from an image site by repeatedly setting the `url` parameter of `Downloader` to a new URL. Each time the `url` parameter is modified, because of the `self.param.watch` call, the `self.fetch` callback is invoked. Because it is marked `async` and uses `await` internally, the method call returns without waiting for results to be available. Once the `await`ed results are available, the method continues with its execution and a tuple (_image_name_, _image_data_) is added to the `results` parameter.\n", "\n", "If you need to have all the results available (and have an internet connection!), you can wait for them:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "print(\"Waiting: \", end=\"\")\n", "while len(f.results)= 3.6 only):\n", "\n", "```python\n", "@param.output(prod_num=param.Number(), prod_str=param.String())\n", "def products(self): \n", " prod = self.a * self.b\n", " return prod, str(prod)\n", "```\n", "\n", "`@param.output` also accepts Python object types, which will be upgraded to a ClassSelector:\n", "\n", "```python\n", "@param.output(int)\n", "def int_product(self): return int(self.a * self.b)\n", "```\n", "\n", "We can see these various options in action:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "class Q(param.Parameterized):\n", " a = param.Number(default=5, bounds=(0, 10))\n", " b = param.Number(default=5, bounds=(0, 10))\n", "\n", " @param.output()\n", " def product(self): return self.a * self.b\n", "\n", " @param.output(param.Number())\n", " def product2(self): return self.a * self.b\n", "\n", " @param.output(result=param.Number())\n", " def __call__(self): return self.a * self.b\n", "\n", " @param.output(prod_num=param.Number(), prod_str=param.String())\n", " def products(self): \n", " prod = self.a * self.b\n", " return prod, str(prod)\n", "\n", " @param.output(int)\n", " def int_product(self): return int(self.a * self.b)\n", "\n", "q=Q()\n", "q" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "q.param.outputs()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Here, you can see that there are _two_ outputs from `prod_str()`, one of type Number and one of type String, and that they are in the order (number, string) in the tuple. The other outputs are all a single result returned directly from that method, with the indicated types (defaulting to `Parameter`) and names. Annotating outputs in this way can help you build large, flexible systems for connecting Parameterized objects together into larger data or computational structures." ] } ], "metadata": { "language_info": { "name": "python", "pygments_lexer": "ipython3" } }, "nbformat": 4, "nbformat_minor": 5 } param-1.12.3/examples/user_guide/Parameter_Types.ipynb000066400000000000000000001157721434441564000230400ustar00rootroot00000000000000{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Parameter types\n", "\n", "You can get some of the benefit of Param from Parameter and Parameterized alone, such as having constant or readonly parameters, parameter value inheritance, and parameter docstrings. However, you'll typically want a more specialized Parameter type so that you can enforce type and bounds restrictions and enable suitable behavior specialized for that type of parameter. Param ships with a large set of pre-defined more-specific Parameter types, and additional custom parameters can and should be added for any domain-specific parameter types needed.\n", "\n", "The predefined types are organized into a class hierarchy where each type is a subclass of Parameter:\n", "\n", "- [String](#strings): String value, optionally constrained by a regular expression\n", "- [Color](#colors): A hex string specifying an RGB color, or a standard named color\n", "- [Boolean](#booleans): True or False (or None, if allowed)\n", " * [Event](#invocations): True/False parameter used to trigger actions (see [Dependencies and watchers](Dependencies_and_Watchers.ipynb)).\n", "- [Dynamic](#numbers): Base class for values that can be set to a callable that returns a concrete value\n", " * [Number](#numbers): Any type of numeric value\n", " - [Integer](#numbers): Positive or negative integer value\n", " - [Magnitude](#numbers): Positive value in the inclusive range 0.0,1.0\n", " - [Date](#numbers): Date or datetime type\n", " - [CalendarDate](#numbers): Date (not datetime)\n", "- [Tuple](#tuples): Python tuple of a fixed length and optional fixed type\n", " * [NumericTuple](#tuples): Python tuple of a fixed length and a numeric type\n", " - [XYCoordinates](#tuples): Position in an x,y plane\n", " - [Range](#tuples): A numeric range or interval\n", " * [DateRange](#tuples): A range of dates or datetimes\n", " * [CalendarDateRange](#tuples): A range of dates (not datetimes)\n", "- [List](#lists): A list of objects, potentially of a fixed, min, or max length\n", " * [HookList](#lists): A list of callables, for calling user-defined code at a processing stage\n", "- [Path](#paths): A POSIX-style string specifying the location of a local file or folder\n", " * [Filename](#paths): A POSIX-style string specifying the location of a local file\n", " * [Foldername](#paths): A POSIX-style string specifying the location of a local folder\n", "- [SelectorBase](#selectors): Abstract superclass covering various selector parameter types\n", " * [Selector](#selectors): One object selected out of a provided ordered list of objects\n", " - [FileSelector](#selectors): One filename selected out of those matching a provided glob\n", " - [ListSelector](#selectors): Multiple objects selected out of a provided list of objects\n", " - [MultiFileSelector](#selectors): Multiple filenames selected out of those matching a provided glob\n", " * [ClassSelector](#classSelectors): An instance or class of a specified Python class or superclass\n", " - [Dict](#classSelectors): A Python dictionary\n", " - [Array](#classSelectors): NumPy array\n", " - [Series](#classSelectors): A Pandas Series\n", " - [DataFrame](#classSelectors): A Pandas DataFrame\n", "- [Callable](#invocations): A callable object, such as a function.\n", " * [Action](#invocations): A callable with no arguments, ready to invoke\n", "- [Composite](#invocations): A list of other attributes or parameters of this class, settable and readable as a group" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The full behavior of these types is covered in the [Reference Manual](https://param.holoviz.org/Reference_Manual/param.html). Here we will discuss the major categories of Parameter type and how to use them, including examples of what each type does _not_ allow (labeled `with param.exceptions_summarized():`). Each of these classes is also suitable for subclassing to create more specialized types enforcing your own specific constraints. After reading about Parameters in general, feel free to skip around in this page and only look at the Parameter types of interest to you!" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Strings\n", "\n", "- `param.String`: String value, with optional regular expression (regex) constraints\n", "\n", "A `param.String` may be set to any Python string value by default, or it may be constrained to match a provided regular expression `regex`. Like all other Parameters, it can optionally also `allow_None`, which will be true by default if the default value is None." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import param\n", "\n", "class S(param.Parameterized):\n", " s = param.String('Four score', regex='[A-Z][a-z][a-z][a-z ]*')\n", "\n", "s = S()\n", "s.s" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "with param.exceptions_summarized():\n", " s.s = 5" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "s.s = 'Forever after'" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "with param.exceptions_summarized():\n", " s.s = 'four of spades'" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "ip_regex = '^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$'\n", "email_regex = '^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$'\n", "\n", "class I(param.Parameterized):\n", " ip_address = param.String('192.1.0.1', regex=ip_regex)\n", " email = param.String('example@me.com', regex=email_regex)\n", "i = I()\n", "i.ip_address" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "i.ip_address=\"10.0.0.2\"\n", "i.email = \"user@gmail.com\"" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "with param.exceptions_summarized():\n", " i.ip_address='192.x.1.x'" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Colors\n", "\n", "- `param.Color`: Named color or hex RGB string (with or without a # prefix)\n", "\n", "\n", "A Color parameter specifies one of the standard [web color names](https://www.w3.org/TR/css-color-3/#svg-color) or an arbitrary hex RGB string. To support only hex RGB strings, specify `allow_named=False`.\n", "\n", "lemonchiffon" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "class C(param.Parameterized):\n", " c = param.Color('#EEFF00')\n", "\n", "c = C()\n", "c.c" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "c.c = 'lemonchiffon'\n", "c.c" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "with param.exceptions_summarized():\n", " c.c = 'puce'" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "with param.exceptions_summarized():\n", " c.c = '#abcdefg'" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Booleans\n", "\n", "- `param.Boolean`: A True or False value (or None, if allow_None is true)\n", "\n", "A Boolean may be True or False. Like all other Parameters, it can optionally also `allow_None`, which will be true by default if the default value is None." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "class B(param.Parameterized):\n", " b = param.Boolean(True)\n", " n = param.Boolean(None)\n", "\n", "b = B()\n", "b.b" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "with param.exceptions_summarized():\n", " b.b=1" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "with param.exceptions_summarized():\n", " b.b=None" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "b.n = True\n", "b.n = None" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Numbers\n", "\n", "- `param.Number`: Python floats, int, and bignum values.\n", "- `param.Integer`: Integer values.\n", "- `param.Magnitude`: Same as `param.Number(..., bounds=(0.0,1.0))`.\n", "- `param.Date`: Date or datetime value of type `datetime.datetime`, `datetime.date`, or `numpy.datetime64`.\n", "- `param.CalendarDate`: Date value of type `datetime.date`.\n", "\n", "A Number is the most common type of Parameter. All Numbers in param are of class Dynamic, which allows them to be set not just to a single value but to a value that can repeatedly be drawn from a distribution or a sequence. (See [Dynamic Parameters](Dynamic_Parameters.ipynb) for more information about using these dynamic features, which will not be further discussed here.) Any Number has a default value (potentially None if allowed) and optional bounds.\n", "\n", "There are two types of bounds: ``bounds`` and ``softbounds``. ``bounds`` are hard bounds: the parameter must have a value within the specified range. The default bounds are (None,None), meaning there are actually no hard bounds. One or both bounds can be set by specifying a value (e.g. `bounds=(None,10)` means there is no lower bound, and an upper bound of 10). Bounds are inclusive by default, but exclusivity can be specified for each bound by setting inclusive_bounds (e.g. `inclusive_bounds=(True,False)` specifies an exclusive upper bound). \n", "\n", "When not being dynamically generated, `bounds` are checked whenever a Number is created or set. Using a default value outside the hard bounds, or one that is not numeric, results in an exception. When being dynamically generated, bounds are checked when the value of a Number is _requested_ (since it has no specific numeric value when first set). A generated value that is not numeric, or is outside the hard bounds, results in an exception. \n", "\n", "A separate set of ``softbounds`` is present to indicate the _typical_ range of the parameter, but these bounds are not enforced by Param. Setting the soft bounds allows a user to know what ranges of values are likely to be useful and allows a GUI to know what values to display on sliders for the Number; `softbounds` are thus suggestions or hints rather than enforced limits. \n", "\n", "Similarly, an optional ``step`` value can be provided to indicate the granularity of this parameter. As for `softbounds`, Param does not force values to conform to the provided step value, but (if provided) the step can be queried by user code and used for parameter sweeps (starting at the `softbounds` low and incrementing in value by `step` until the `softbounds` high), or by GUI code to determine steps on a settings dial.\n", "\n", "Several convenience methods for working with bounds are provided:\n", "- `get_soft_bounds()`: return the soft bounds (or hard bounds, if no soft bounds), for code that needs to know the typical range for this Parameter.\n", "- `crop_to_bounds(val)`: crop the provided value to fit into the hard bounds.\n", "- `set_in_bounds(obj,val)`: silently crop the given value into the legal range and set to the result, for building an API or user interface that accepts free-form input. \n", "\n", "Numbers support a `set_hook` that can be set to a function like `def logging_set_hook(obj,val): log_value(val) return val`, which will be called whenever the value is set.\n", "\n", "Using Number parameters:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import param\n", "\n", "class N(param.Parameterized):\n", " n = param.Number(5.6, bounds=(0,None), softbounds=(None,50))\n", " i = param.Integer(5, bounds=(0,50))\n", " \n", "a = N()\n", "a.n=2\n", "a.n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "N.param.n.set_in_bounds(a,-10)\n", "a.n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "a.param.n.set_in_bounds(a,-5)\n", "a.n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "a.param.n.set_in_bounds(a,75)\n", "a.n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "a.param.n.get_soft_bounds()" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "with param.exceptions_summarized():\n", " a.n = -5" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "a.n = 500\n", "a.n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "with param.exceptions_summarized():\n", " a.i=5.7" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import datetime\n", "\n", "class D(param.Parameterized):\n", " d = param.CalendarDate(datetime.date(1900, 1, 1))\n", " t = param.Date(datetime.datetime.fromisoformat('2002-12-25T00:00'))\n", "\n", "d = D()\n", "d.d = datetime.date.fromisoformat('2000-01-01')\n", "d.d" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "with param.exceptions_summarized():\n", " d.d = 2022" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "d.t = datetime.date(1900, 1, 1)\n", "d.t" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "with param.exceptions_summarized():\n", " d.d = datetime.datetime.fromisoformat('2002-12-25T00:00')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Tuples" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "- `param.Tuple`: Python tuple of a fixed length.\n", "- `param.NumericTuple`: Python tuple of a fixed length, with numeric values.\n", "- `param.XYCoordinates`: Python pair (2-tuple) of numeric values. Same as `param.NumericTuple(..., length=2)`, but semantically representing a 2D coordinate in a plane (e.g. for drawing programs or GUIs)\n", "- `param.Range`: `NumericTuple` representing a numeric range with optional bounds and softbounds.\n", "- `param.DateRange`: `Range` where the numeric type is a date or datetime (using same date types as `param.Date`).\n", "- `param.CalendarDateRange`: `Range` where the numeric type is a `datetime.date`. \n", "\n", "A tuple Parameter accepts a Python tuple for the value. Tuple parameters have a fixed length, typically set by the default value of the parameter but settable as the `length` if the default value is None. Only a tuple of the specified length will be accepted when a value is set.\n", "\n", "There are many tuple types as listed above, accepting either any type, numeric types, datetimes, dates, etc. `Range` types support `bounds`, `softbounds`, `inclusive_bounds`, and `step` on the numeric values in the tuple, similar to [Number](#Numbers) types." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "class T(param.Parameterized):\n", " t = param.Range((-10,10), bounds=(-100,None), softbounds=(None,100))\n", "b = T()\n", "b.t" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "b.t = (50.2,50.3)\n", "b.t" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "with param.exceptions_summarized():\n", " b.t = 5" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "with param.exceptions_summarized():\n", " b.t = (5,5,5)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "with param.exceptions_summarized():\n", " b.t = (5,\"5\")" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "with param.exceptions_summarized():\n", " b.t = (-500,500)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import datetime\n", "class D(param.Parameterized):\n", " d = param.CalendarDateRange((datetime.date.fromisoformat('1900-01-01'),\n", " datetime.date.fromisoformat('1910-12-31')))\n", "c = D()\n", "c.d" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "with param.exceptions_summarized():\n", " c.d=(1905, 1907)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Lists" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "- `param.List`: A Python list of objects, usually of a specified type.\n", "- `param.HookList`: A list of callable objects, for executing user-defined code at some processing stage\n", "\n", "List Parameters accept a Python list of objects. Typically the `item_type` will be specified for those objects, so that the rest of the code does not have to further check types when it refers to those values. Where appropriate, the `bounds` of the list can be set as (_min_length_, _max_length_), defaulting to `(0,None)`. Because List parameters already have an empty value ([]), they do not support `allow_None`.\n", "\n", "A `param.HookList` is a list whose elements are callable objects (typically either functions or objects with a `__call__` method). A `HookList` is intended for providing user configurability at various stages of some processing algorithm or pipeline. At present, there is no validation that the provided callable takes any particular number or type of arguments." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import param\n", "class L(param.Parameterized):\n", " ls = param.List([\"red\",\"green\",\"blue\"], item_type=str, bounds=(0,10))\n", "\n", "e = L()\n", "e.ls" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "with param.exceptions_summarized():\n", " e.ls = [1,2]" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "with param.exceptions_summarized():\n", " e.ls = [str(i) for i in range(20)]" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "class multi_stage_example(param.Parameterized):\n", " before = param.HookList()\n", " during = param.HookList()\n", " after = param.HookList()\n", " \n", " values = param.List([1.5,-8.1,6.9,100.0], item_type=float)\n", " \n", " def __call__(self):\n", " for h in self.before: h(self)\n", " s = 0\n", " for v in self.values:\n", " v_ = v\n", " for h in self.during: v_ = h(v_)\n", " s += v_\n", " for h in self.after: h()\n", " return s\n", "\n", "ex = multi_stage_example()\n", "ex()" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def validate(obj):\n", " for i in obj.values:\n", " if i<0:\n", " print(\"Negative value found in argument\")\n", "\n", "m = multi_stage_example(before=[validate])\n", "\n", "m()" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from math import fabs\n", "\n", "ex.during=[abs]\n", "ex()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Paths\n", "\n", "- `param.Path`: A POSIX-style string specifying the location of a local file or folder\n", "- `param.Filename`: A POSIX-style string specifying the location of a local file\n", "- `param.Foldername`: A POSIX-style string specifying the location of a local folder\n", "\n", "A Path can be set to a string specifying the path of a file or folder. In code, the string should be specified in POSIX (UNIX) style, using forward slashes / and starting from / if absolute or some other character if relative. When retrieved, the string will be in the format of the user's operating system. \n", "\n", "Relative paths are converted to absolute paths by searching for a matching filename on the filesystem. If `search_paths` is provided and not empty, the folders in that list are searched for the given filename, in order, returning the absolute path for the first match found by appending the provided path to the search path. An IOError is raised if the file or folder is not found. If `search_paths` is empty (the default), the file or folder is expected to be in the current working directory.\n", "\n", "Either a file or a folder name is accepted by `param.Path`, while `param.Filename` accepts only file names and `param.Foldername` accepts only folder names." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "class P(param.Parameterized):\n", " p = param.Path('Parameter_Types.ipynb')\n", " f = param.Filename('Parameter_Types.ipynb')\n", " d = param.Foldername('lib', search_paths=['/','/usr','/share'])\n", " \n", "p = P()\n", "p.p" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "p.p='/usr/lib'\n", "p.p" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "p.f" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "with param.exceptions_summarized():\n", " p.f='/usr/lib'" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "p.d" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "with param.exceptions_summarized():\n", " p.d='Parameter_Types.ipynb'" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Selectors\n", "\n", "- `param.Selector`: One object selected out of a provided ordered list of objects\n", "- `param.ListSelector`: Multiple objects selected out of a provided list of objects\n", "- `param.FileSelector`: One filename selected out of those matching a provided glob\n", "- `param.MultiFileSelector`: Multiple filenames selected out of those matching a provided glob\n", "\n", "The value of a Selector is one or more items from a set of allowed values. All Selector types must implement `get_range()`, providing a concrete list of available options for the value.\n", "\n", "A `param.Selector` accepts a list or dictionary of `objects`, and has a single default (current) value that must be one of those objects. If not otherwise specified, the default will be the first item from the list or dictionary. \n", "\n", "Providing the objects as a list is appropriate for selecting among a set of strings, or among a set of Parameterized objects that each have a \"name\" parameter. That way, a UI that lets users select by string will have a suitable string available for each object to let the user make a choice between them.\n", "\n", "Otherwise, the objects should be provided as a _name_:_value_ dictionary, where the string name will be stored for use in such a UI, but is not otherwise accessed by Param. The values from setting and getting the parameter are always the actual underlying object, not the string names. Because the string name will need to be looked up from the value if this parameter is used in a UI, all objects need to be hashable via the `param.hashable()` function, which accepts Python literals plus list and dictionary types (treating them like tuples).\n", "\n", "If the list of available objects is not meant be exhaustive, you can specify `check_on_set=False` (which automatically applies if the initial list is empty). Objects will then be added to the `objects` list whenever they are set, including as the initial default. `check_on_set=False` can be useful when the predefined set of objects is not exhaustive, letting a user select from the existing list for convenience while also being able to supply any other suitable object they can construct. When `check_on_set=True`, the initial value (and all subsequent values) must be in the `objects` list.\n", "\n", "Because `Selector` is usually used to allow selection from a list of existing (instantiated) objects, `instantiate` is False by default, but you can specify `instantiate=True` if you want each copy of this Parameter value to be independent of other instances and superclasses.\n", "\n", "In cases where the objects in the list cannot be known when writing the Parameterized class but can be calculated at runtime, you can supply a callable (of no arguments) to `compute_default_fn`, and then ensure that at runtime you call `compute_default` on that Parameter to initialize the value.\n", "\n", "A `param.ListSelector` works just the same as a regular Selector, but the value is a _list_ of valid objects from the available objects, rather than just one. Each item in the list is checked against the `objects`, and thus the current value is thus a _subset_ of the `objects`, rather than just one of the objects.\n", "\n", "A `param.FileSelector` works like a regular Selector with the value being a filename and the `objects` being computed from files on a file system. The files are specified as a `path` [glob](https://docs.python.org/3/library/glob.html), and all filenames matching the glob are valid `objects` for the parameter.\n", "\n", "A `param.MultiFileSelector` is the analog of ListSelector but for files, i.e., again supporting a path glob but allowing the user to select a list of filenames rather than a single filename. The default value in this case is _all_ of the matched files, not just the first one." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "colors = [\"red\",\"green\",\"blue\"]\n", "\n", "class S(param.Parameterized):\n", " o = param.Selector(colors)\n", " ls = param.ListSelector(colors[0:2], objects=colors)\n", " \n", "s = S()\n", "s.o" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "s.o = \"green\"\n", "s.o" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "with param.exceptions_summarized():\n", " s.o = \"yellow\"" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "with param.exceptions_summarized():\n", " s.o = 42" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "s.ls" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "s.ls=['blue']\n", "s.ls" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "with param.exceptions_summarized():\n", " s.ls=['red','yellow']\n", " s.ls" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "class F(param.Parameterized):\n", " f = param.FileSelector(path='/usr/share/*')\n", " fs = param.MultiFileSelector(path='/usr/share/*')\n", " \n", "f = F()\n", "f.f" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "f.param.f.objects[0:3]" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "f.fs = f.param.fs.objects[0:2]\n", "f.fs" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## ClassSelectors\n", "\n", "- `param.ClassSelector`: An instance or class of a specified Python class or superclass\n", "- `param.Dict`: A Python dictionary\n", "- `param.Array`: NumPy array\n", "- `param.Series`: A Pandas Series\n", "- `param.DataFrame`: A Pandas DataFrame\n", "\n", "A ClassSelector has a value that is either an instance or a subclass of a specified Python `class_`. By default, requires an instance of that class, but specifying `is_instance=False` means that a subclass must be selected instead. \n", "\n", "Like Selector types, all ClassSelector types implement `get_range()`, in this case providing an introspected list of all the concrete (not abstract) subclasses available for the given class. If you want a class to be treated as abstract so that it does not show up in such a list, you can have it declare `__abstract=True` as a class attribute. In a GUI, the range list allows a user to select a type of object they want to create, and they can then separately edit the new object's parameters (if any) to configure it appropriately." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "class C(param.Parameterized):\n", " e_instance = param.ClassSelector(class_=ArithmeticError, default=ZeroDivisionError(\"1/0\"))\n", " e_class = param.ClassSelector(class_=ArithmeticError, default=ZeroDivisionError, is_instance=False)\n", " \n", "c = C(e_class=OverflowError)\n", "c.e_class, c.e_instance" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "c.param.e_instance.get_range()" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "c.param.e_class.get_range()" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "with param.exceptions_summarized():\n", " c.e_class = Exception" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "with param.exceptions_summarized():\n", " c.e_instance = ArithmeticError" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "c.e_instance = ArithmeticError()\n", "c.e_instance" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Various types of ClassSelector are provided for specific data types:\n", "\n", "- `param.Dict`: `class_=dict`, accepting a Python dictionary\n", "- `param.Array`: `class=numpy.ndarray`, accepting a NumPy array\n", "- `param.Series`: `class_=pandas.Series`, a Pandas Series. Accepts constraints on the number of `rows`, either as an integer length (e.g. `rows=10`) or a range tuple `rows=(2,4)`).\n", "- `param.DataFrame`: `class_=pandas.DataFrame`, a Pandas DataFrame. Accepts constraints on the number of `rows` (as for `param.Series`) or `columns` (with numerical or range values as for `rows` or as a list of column names (which must appear in that order) or as a set of column names (which can appear in any order))." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import numpy as np, pandas as pd\n", "\n", "class D(param.Parameterized):\n", " d = param.Dict(dict(a=1, b=\"not set\", c=2.0))\n", " a = param.Array(np.array([1,-1]))\n", " s = param.Series(pd.Series([1,-1]))\n", " f = param.DataFrame(pd.DataFrame({'a':[1,2], 'b':[2,3], 'c':[4,5]}), rows=(2,None), columns=set(['a','b']))\n", "\n", "d = D()\n", "d.d = {5:np.nan}\n", "d.d" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "with param.exceptions_summarized():\n", " d.d=[(\"a\",1)]" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "df = pd.DataFrame({'b':[-2,-3], 'a':[-1,-2]})\n", "d.f = df\n", "d.f" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "with param.exceptions_summarized():\n", " df = pd.DataFrame({'a':[-2,-3], 'c':[-1,-2]})\n", " d.f = df" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "with param.exceptions_summarized():\n", " df = pd.DataFrame({'a':[-2], 'b':[-1]})\n", " d.f = df" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### `classlist` and `param.descendents`\n", "\n", "If you are building a GUI or some other mechanism allowing the user to choose a class or an instance of a specified class in a ClassSelector, you may want to construct a list of all subclasses or superclasses of the given class. To make it easy to traverse upwards or downwards in the inheritance hierarchy in this way, param provides the `classlist` and `param.descendents` functions. `classlist` provides a list of the superclasses of the provided object, including itself, in order from least to most specific:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from param.parameterized import classlist\n", "\n", "classlist(D)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "As you can see, `D` is a type of `Parameterized`, and a `Parameterized` is a type of Python object. Conversely (and typically more usefully), `param.descendents` provides a list of the subclasses of the provided object, including itself:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "param.descendents(param.SelectorBase)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "As you can see, there are many subtypes of SelectorBase. This list is calculated from whatever subtypes are currently defined in this Python session. If you derive an additional subtype or load code that defines an additional subtype, this list will get longer, so you need to make sure that all such code has been executed before letting the user make a selection of a subtype." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Invocations\n", "\n", "- `param.Callable`: A callable object, such as a function\n", "- `param.Action`: A callable with no arguments, ready to invoke\n", "- `param.Event`: Empty action, for use in triggering events for watchers\n", "- `param.Composite`: Wrapper around other parameters, letting them be set as a tuple\n", "\n", "Invocation parameters are a loose group of types that either contain an executable (callable) object, are invoked to execute some other code, or are set to change the value of some other parameter(s) or attribute(s).\n", "\n", "A Callable may be set to any callable object, typically either a function or else an instance of a class that provides a `__call__` method. At present, there is no validation that the provided callable takes any particular number or type of arguments. Lambdas can be provided, but note that the resulting parameter value will no longer be picklable, so if you need to use pickling (`setstate` and `getstate`), be sure to use a named function instead.\n", "\n", "An Action is the same as a Callable, but is expected to have no arguments. In a GUI an Action is typically mapped to a button whose name or label is the name of this parameter. \n", "\n", "An Event Parameter has a Boolean value but is primarily intended for triggering events on its watchers. See [Dependencies and Watchers](Dependencies_and_Watchers.ipynb) for the details.\n", "\n", "A Composite Parameter has a value that is looked up from the value of a list of attributes of this class (which may or may not be parameters) and that when set changes the values of those other attributes or parameters. This type of Parameter can be useful for treating a set of related values as a group for setting purposes, but as individual parameters for code that reads from them. As of Param 1.10, Composite parameters have not been tested with watchers and dependencies and may not behave appropriately for such uses." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def identity(x): return x\n", " \n", "def print_time_of_day():\n", " print(datetime.date.today())\n", "\n", "class A(param.Parameterized):\n", " transformer = param.Callable(identity)\n", " a = param.Action(print_time_of_day)\n", " \n", " def __call__(self, x):\n", " return self.transformer(x)\n", " \n", "a = A()\n", "a(5)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def double(x):\n", " return 2*x\n", " \n", "d = A(transformer=double)\n", "d(5)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "d.a()" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "with param.exceptions_summarized():\n", " d.a = 5" ] } ], "metadata": { "language_info": { "name": "python", "pygments_lexer": "ipython3" } }, "nbformat": 4, "nbformat_minor": 5 } param-1.12.3/examples/user_guide/ParameterizedFunctions.ipynb000066400000000000000000000076071434441564000244160ustar00rootroot00000000000000{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# ParameterizedFunctions\n", "\n", "`Parameterized` classes and objects are full-featured substitues for Python objects, providing rich support and control for how attributes behave. What if you need similar features, but for functions rather than objects?\n", "\n", "Python functions don't directly support the various [language features like descriptors that Param builds on](How_Param_Works.ipynb), but you can instead make a Python class or object that _behaves_ like a function, while still supporting Parameters. To make that easier, Param provides an abstract class `ParameterizedFunction` that you can use as a superclass for any function-like object you want to write. A ParameterizedFunction automatically invokes its `__call__` method whenever it is instantiated. So, all you need to do is implement the `__call__` method with the implementation of your function. For example:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from param import Parameter, ParameterizedFunction, ParamOverrides\n", "\n", "class multiply(ParameterizedFunction):\n", " \"Function to multiply two arguments.\"\n", "\n", " left = Parameter(2, doc=\"Left-hand-side argument\")\n", " right = Parameter(4, doc=\"Right-hand-side argument\")\n", "\n", " def __call__(self, **params):\n", " p = ParamOverrides(self, params)\n", " return p.left * p.right\n", " \n", "multiply()" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "multiply(left=3, right=7)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "multiply.left = 7\n", "multiply(right = 10)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Here you can see that multiply acts like any other function that takes keyword arguments, but the arguments are now documented, potentially type checked, and have default values. \n", "\n", "This implementation depends on the separate object {py:class}`param.ParamOverrides`, which provides two-level lookup of parameter values: first on the arguments provided to the call, and then (if not provided) on the ParameterizedFunction instance. This way a user can choose to provide any or none of the arguments when the function (really, function object) is invoked. \n", "\n", "The `__call__` method can also take positional arguments, but in that case the class author would need to handle any mapping from those arguments to parameters there might be. `__call__` can also take extra keyword arguments beyond parameter values, but if so, you'll need to construct ParamOverrides as `p = ParamOverrides(self, params, allow_extra_keywords=True)`, then access the extra (non-Parameter) keywords in `p.extra_keywords` and process those explicitly." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## .instance()\n", "\n", "Usually, with a Parameterized object, you can modify values on the instance level, in addition to the class level shown above. Here, however, there is no instance to grab, because the ParameterizedFunction is called and evaluated, returning a value rather than the function object. If you want to grab an instance where you can set a value and then call the instance, you can use the `.instance()` method of a ParameterizedFunction:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "multiply_by_10 = multiply.instance(right=10)\n", "multiply_by_10(left=8)" ] } ], "metadata": { "language_info": { "name": "python", "pygments_lexer": "ipython3" } }, "nbformat": 4, "nbformat_minor": 5 } param-1.12.3/examples/user_guide/Parameters.ipynb000066400000000000000000000723001434441564000220240ustar00rootroot00000000000000{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Parameters and Parameterized objects\n", "\n", "Fundamentally, what Param does is allow you to control how certain user-visible attributes (\"parameters\") of a Python class or instance will behave when their value is get or set. A user of that class can set those attributes to control the class, but only if the mechanisms provided by Param and configured by the programmer allow it. In this way, Param allows the author of a class to implement and constrain what a user can do with that class or an instance of it, setting up a clear contract of what is and is not allowed, and how that attribute will behave. To do this, Param provides two main new types of Python object: `Parameter` objects, and `Parameterized` objects.\n", "\n", "A parameter is a special kind of Python class attribute. Setting a `Parameterized` class attribute to be a Parameter instance causes that attribute of the class (and the class's instances) to be treated as a parameter, not just an ordinary attribute. Parameters support special behavior, including dynamically generated parameter values, documentation strings, constant and read-only parameters, type or range checking at assignment time, and values dependent on those of other parameters.\n", "\n", "More concretely, a Python `Parameter` object inherits from `param.Parameter` and stores various metadata attributes describing how a corresponding Python attribute of a `Parameterized` object should behave. By convention, we will use a capital 'P' Parameter to refer to the Parameter object itself, and a lower-case 'p' parameter to refer to the Python attribute it controls (i.e., the Parameter's \"value\"). \n", "\n", "A `Parameterized` class is a Python class that inherits from `param.Parameterized` and can accept `Parameter` objects as class attributes. A `Parameterized` class or instance uses the `Parameter` objects to determine how the corresponding attribute should behave.\n", "\n", "There are many specific types of `Parameter` with different behavior, discussed in [Parameter Types](Parameter_Types.ipynb), but here we will cover the common behavior between _all_ Parameter types when used in a `Parameterized` object." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Parameter metadata\n", "\n", "Each Parameter type can define additional behavior and associated metadata, but the metadata supported for all Parameter types includes:\n", "\n", "- **default**: Default value for this parameter at the class level, which will also be the value at the Parameterized instance level if it hasn't been set separately on the instance.\n", "- **name**: String **name** of this parameter, which is typically determined by the attribute name of this Parameter in the owning Parameterized object, and is not set directly by a programmer.\n", "- **label**: Optional long name used for a verbose listing; defaults to the **name**.\n", "- **allow_None**: Whether this parameter accepts None as an allowed value, in addition to whatever other types it accepts. Automatically set to True if the default value of this Parameter is None.\n", "- **doc**: Docstring describing this parameter, which can be used by automatic documentation systems.\n", "- **constant**: Parameter whose value can only be changed at the class level or in a Parameterized constructor. Once the Parameterized instance has been created, the value is constant except in the context of `with param.edit_constant(obj)` (see below).\n", "- **readonly**: Parameter whose value cannot be set by a user either on an instance or at the class level. Can still be changed inside a codebase by temporarily overriding this value, e.g. to report internal state.\n", "- **instantiate**: Whether to deepcopy the default value into a Parameterized instance when it is created. False by default for Parameter and most of its subtypes, but some Parameter types commonly used with mutable containers default to `instantiate=True` to avoid interaction between separate Parameterized instances, and users can control this when declaring the Parameter (see below). \n", "- **per_instance**: whether a separate Parameter instance will be created for every Parameterized instance created. Similar to `instantiate`, but applies to the Parameter object rather than to its value.\n", "- **precedence**: Optional numeric value controlling whether this parameter is visible in a listing and if so in what order.\n", "\n", "Most of these settings (apart from **name**) are accepted as keyword arguments to the Parameter's constructor, with `default` also accepted as a positional argument:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import param\n", "from param import Parameter, Parameterized\n", "\n", "p = Parameter(42, doc=\"The answer\", constant=True)\n", "p.default" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "p.allow_None" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "p.doc" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Parameter objects and instances\n", "\n", "In most cases, a Parameter will not be declared on its own as above; the Parameter object by itself is little more than a container for the metadata above. Until it is put into a class, most of those declarations are not meaningful, because what the Parameter object does is to specify how the corresponding Python attribute of that class should be handled. For example, we can define a Parameterized class with a couple of Parameter objects, and we'll then be able to access the corresponding attributes of that class:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "class A(Parameterized):\n", " question = Parameter(\"What is it?\", doc=\"The question\")\n", " answer = Parameter(2, constant=True, doc=\"The answer\")\n", " ultimate_answer = Parameter(42, readonly=True, doc=\"The real answer\")\n", "\n", "a = A(question=\"How is it?\", answer=\"6\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Here, we created a Parameterized class `A`, with parameters `question` and `answer`, each with default values. We then instantiated a Python object `a` of type `A`. Without having to write a constructor for `A`, we were able to provide our own values for `question` and `answer`, while inheriting the default value of `ultimate_answer`. This approach gives a lot of (but not too much!) configurability to the user of this class, without much effort by the class author. Any values we provide at instantiation need to be allowed by the `Parameter` declaration; e.g. here we could not provide a value for `ultimate_answer` when declaring `a`, because that parameter is declared read only:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "with param.exceptions_summarized():\n", " A(ultimate_answer=\"no\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now that we have a Parameterized instance `a`, we can access the attributes we defined just as if they were normal Python instance attributes, and we'll get the values we provided:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "a.question" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "a.answer" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Meanwhile, the `Parameterized` _class_ `A` (not the instance `a`) still has the default values, accessible as class attributes and used for any future objects instantiated of type `A`:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "A.question" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "A.answer" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "b=A()\n", "b.answer" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "If accessing the attribute always gives us a value whether on the instance or the class, what happened to the `Parameter` objects? They are stored on the Parameterized instance or class, and are accessible via a special `param` accessor object at either the instance or class levels:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "a.param.question" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "a.param.question.name" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "a.param.question.default" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "A.param.question.default" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Once the Parameterized instance is created, the attributes can continue to be modified on it as often as you like, as long as the value is allowed by the `Parameter` object involved. E.g. `question` can still be changed, while `answer` is constant and cannot be changed after the `Parameterized` object has been instantiated:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "with param.exceptions_summarized():\n", " a.question=True\n", " a.answer=5" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "a.question" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Note that if for some reason you do need to change the value of a constant parameter (typically inside of your Parameterized object's own code), you can do so using the `param.edit_constant` context manager:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "with param.edit_constant(a):\n", " a.answer=30\n", "a.answer" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "In most cases, the only time you need to worry about the difference between a Parameter and a regular Python attribute is when you first declare it; after that it will sit there happily behaving as instructed, noticeable only when a user attempts something the declarer of that Parameter has not allowed. You can safely leave the various metadata items at their defaults most of the time, but they are all there for when your particular application requires a certain behavior. " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Parameter inheritance and instantiation\n", "\n", "Much of the parameter metadata is there to help you control whether and how the parameter value is instantiated on Parameterized objects as they are created or new Parameterized subclasses as they are defined. Depending on how you want to use that Parameter and what values it might take, controlling instantiation can be very important when mutable values are involved. First, let's look at the default behavior, which is appropriate for immutable attributes:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "class B(A):\n", " ultimate_answer = Parameter(84, readonly=True)\n", "\n", "b = B()\n", "b.question" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "A.question=\"How are you?\"" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "b.question" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Here you can see that B inherits the `question` parameter from A, and as long as `question` has not been set explicitly on `b`, `b.question` will report the value from where that Parameter was defined, i.e. A in this case. If `question` is subsequently set on `b`, `b.question` will no longer be affected by the value in `A`:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "b.question=\"Why?\"\n", "A.question=\"Who?\"\n", "b.question" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "As you can see, parameters not specified in B are still fully usable in it, if they were declared in a superclass. Metadata associated with that parameter is also inherited if not explicitly overidden in `B`. E.g. `help(b)` or `help(B)` will list all parameters:" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\"Param" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Parameter inheritance like this lets you (a) use a parameter in many subclasses without having to define it more than once, and (b) control the value of that parameter conveniently across the entire set of subclasses and instances, as long as that attribute has not been set on those objects already. Using inheritance in this way is a very convenient mechanism for setting default values and other \"global\" parameters, whether before a program starts executing or during it.\n", "\n", "However, what happens if the value (unlike Python strings) is mutable? Things get a lot more complex:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "s = [1,2,3]\n", "\n", "class C(Parameterized):\n", " s1 = param.Parameter(s, doc=\"A sequence\")\n", " s2 = param.Parameter(s, doc=\"Another sequence\")\n", "\n", "c = C()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Here, both parameters `s1` and `s2` effectively point to the same underlying sequence `s`:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "c.s1 is c.s2" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "s[1]*=5" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "s" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "c.s1" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "c.s1[2]='a'" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "c.s1" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "c.s2" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "As you can see, there is only one actual sequence here, and `s`, `s1`, and `s2` all point to it. In some cases such behavior is desirable, e.g. if the mutable object is a specific global list (e.g. a set of search paths) with a unique identity and all of the parameters are meant to point to that specific item. In other cases, it's the contents of the mutable item that are important, and no sharing of contents is intended. Luckily, Param supports that case as well, if you provide `instantiate=True`:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "s = [1,2,3]\n", "\n", "class D(Parameterized):\n", " s1 = Parameter(s, doc=\"A sequence\", instantiate=True)\n", " s2 = Parameter(s, doc=\"Another sequence\", instantiate=True)\n", "\n", "d = D()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now, parameters `s1` and `s2` point to their own copies of the sequence, independent of each other and of the original argument `s`:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "d.s1 is d.s2" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "s*=2" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "s" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "d.s1" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "d.s1[2]='a'" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "d.s2" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Of course, copying the data into each instance like that costs memory, and moreover prevents controlling all instances at once by setting a class attribute as we saw earlier, which is why `instantiate` is not True by default. As a rule of thumb, set `instantiate=True` if and only if (a) your Parameter can take mutable values, and (b) you want those values to be independent between Parameterized instances." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Parameter metadata inheritance and instantiation\n", "\n", "`instantiate` controls how parameter _values_ behave, but similar issues arise for Parameter _objects_, which offer similar control via the `per_instance` metadata declaration. `per_instance` (True by default) provides a logically distinct Parameter object for every Parameterized instance, allowing each such instance to have different metadata for that parameter. For example, we can set the label separately for each instance without clobbering each other:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "d1 = D()\n", "d2 = D()\n", "d1.param.s1.label=\"sequence 1\"\n", "d2.param.s1.label=\"Sequence 1\"\n", "d2.param.s1.label" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "d1.param.s1.label" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This capability is useful for situations with dynamically updated metadata, e.g. if you need setting one parameter's value (e.g. 'Continent') to change the allowed values of another parameter (e.g. 'Country'). The underlying Parameter objects are copied lazily (only when actually changed), so that objects are not actually multiplied unless necessary. If you do want parameters to share a single Parameter object so that you can control its behavior globally, you can achieve that with `per_instance=False`, though the effects can be confusing in the same way as `instantiate=True` for mutable objects (above):" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "class E(Parameterized):\n", " a = param.Parameter(3.14, label=\"pi\", per_instance=False)\n", "\n", "e1 = E()\n", "e2 = E()\n", "e2.param.a.label=\"Pie\"\n", "e1.param.a.label" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Instantiating with shared parameters\n", "\n", "When creating a large collection of Parameterized objects of the same type, the overhead of having separate parameters for each object can be significant. If you want, you can create the objects to share parameter values for efficiency, and also so that you can easily change a value on all such objects at the same time. \n", "\n", "As an example, let's say you've defined a Parameter value to be independent, such that changing one instance's value will not affect the others:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "class S(param.Parameterized):\n", " l = param.Parameter([1,2,3], instantiate=True)\n", "\n", "ss = [S() for i in range(10)]\n", "ss[0].l[2]=5\n", "ss[1].l" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Here changing the value of `l` on `ss[0]` doesn't affect `ss[1]` or any other instances.\n", "\n", "What if you as a user of this class are creating a very large number of similar objects and actually do want them to share the same parameter value, either to save memory or to make it easy to change all of their values at once? In that case you can use the context manager `shared_parameters`, and any Parameterized objects created within that context will share parameter values, such that changing one of them will affect all of them:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "with param.shared_parameters():\n", " ps = [S() for i in range(10)]\n", " \n", "ps[0].l[2]=5\n", "ps[1].l" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This approach can provide significant speedup and memory savings in certain cases, but should only be used for good reasons, since it can cause confusion for any code expecting instances to be independent as they have been declared." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Displaying Parameterized objects\n", "\n", "Most of the important behavior of Parameterized is to do with instantiation, getting, and setting, as described above. Parameterized also provides a few public methods for creating a string representation of the Parameterized object and its parameters:\n", "\n", "- `Parameterized.__str__()`: A concise, non-executable representation of the name and class of this object\n", "- `Parameterized.__repr__()`: A representation of this object and its parameter values as if it were Python code calling the constructor (`classname(parameter1=x,parameter2=y,...)`)\n", "- `Parameterized.param.pprint()`: Customizable, hierarchical pretty-printed representation of this Parameterized and (recursively) any of its parameters that are Parameterized objects. See [Serialization and Persistence](Serialization_and_Persistence.ipynb) for details on customizing `pprint`." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import param\n", "\n", "class Q(param.Parameterized):\n", " a = param.Number(39, bounds=(0,50))\n", " b = param.String(\"str\")\n", "\n", "class P(Q):\n", " c = param.ClassSelector(Q, Q())\n", " e = param.ClassSelector(param.Parameterized, param.Parameterized())\n", " f = param.Range((0,1))\n", "\n", "p = P(f=(2,3), c=P(f=(42,43)), name=\"demo\")" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "p.__str__()" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "p.__repr__()" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "p.param.pprint(separator=\"\\n\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Notice that in the case of a circular reference (`p.c = P(c=p)`) the representation will show an ellipsis (`...`) rather than recursively printing the subobject:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "p.c=P(c=p)\n", "p.param.pprint()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Other Parameterized methods\n", "\n", "Like `.param.pprint`, the remaining \"utility\" or convenience methods available for a `Parameterized` class or object are provided via a subobject called `param` that helps keep the namespace clean and disambiguate between Parameter objects and parameter values:\n", "\n", "- `.param.add_parameter(param_name,param_obj)`: Dynamically add a new Parameter to this object's class\n", "- `.param.update(**kwargs)`: Set parameter values from the given `param=value` keyword arguments (or a dict or iterable), delaying watching and dependency handling until all have been updated\n", "- `.param.values(onlychanged=False)`: A dict of name,value pairs for all parameters of this object\n", "- `.param.objects(instance=True)`: Parameter objects of this instance or class\n", "- `.param.get_value_generator(name)`: Returns the underlying value-generating callable for this parameter, or the underlying static value if none\n", "- `.param.force_new_dynamic_value(name)`: For a Dynamic parameter, generate a new value and return it\n", "- `.param.inspect_value(name)`: For a Dynamic parameter, return the current value of the named attribute without modifying it.\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Specialized Parameter types\n", "\n", "As you can see above, a `Parameter` provides a lot of power already on its own, but in practice you will want to use much more specific parameter types that reject invalid inputs and keep your code clean and simple. A specialized Parameter acts as a \"contract\" with the users of the code you write, declaring and defending precisely what configuration is allowed and how to achieve it. If you need to accept specific inputs like that but don't add an appropriate Parameter type, you'll be stuck adding exceptions and validation code throughout your codebase, whereas anything you can express at the Parameter level will be enforced automatically without any further checks or code.\n", "\n", "For instance, what if you want to accept a numeric parameter, but (for some reason) can only accept numbers that are even integers? You'll need a custom Parameter class to express a restriction like that. In this case you can do it by overriding the `_validate_value` method of the `Parameter` class:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import numbers\n", "\n", "class EvenInteger(param.Parameter):\n", " \"\"\"Integer Parameter that must be even\"\"\"\n", "\n", " def _validate_value(self, val, allow_None):\n", " super(EvenInteger, self)._validate_value(val, allow_None)\n", " if not isinstance(val, numbers.Number):\n", " raise ValueError(\"EvenInteger parameter %r must be a number, \"\n", " \"not %r.\" % (self.name, val))\n", " \n", " if not (val % 2 == 0):\n", " raise ValueError(\"EvenInteger parameter %r must be even, \"\n", " \"not %r.\" % (self.name, val))\n", "\n", "class P(param.Parameterized):\n", " n = param.Number()\n", " b = EvenInteger()\n", " \n", "p=P()\n", "P(n=5, b=4)\n", "P(b=4, n=5, name='P00003')" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "with param.exceptions_summarized():\n", " P(n=5, b=\"four\")" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "with param.exceptions_summarized():\n", " P(n=5, b=5)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Luckily, you don't often need to write a custom Parameter class like this, because the most common cases are already provided in Param, as listed in the [Parameter Types](Parameter_Types.ipynb) manual. If you need something more specific than the existing types, start with the one that comes closest to restricting its value to the desired set of values without excluding any allowable values. In this case all integer powers of 2 are also integers, so you'd start with `param.Integer` rather than `param.Parameterized` as above. You can then make a new subclass and add validation as above to further restrict the values to precisely what you allow. Here if you inherited from `param.Integer` you would no longer need to check if the input is a number, as `param.Integer` already does that as long as you call `super` as above. Your custom type can override any aspects of the Parameter if needed, e.g. to accept different items in the constructor, store additional data, add additional constraints, and so on. The existing Parameter types in `param/__init__.py` act as a rich source of examples for you to start with and crib from." ] } ], "metadata": { "language_info": { "name": "python", "pygments_lexer": "ipython3" } }, "nbformat": 4, "nbformat_minor": 5 } param-1.12.3/examples/user_guide/Serialization_and_Persistence.ipynb000066400000000000000000000637361434441564000257410ustar00rootroot00000000000000{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Serialization and Persistence\n", "\n", "Parameterized objects are declarative, explicitly defining a set of values for their parameters. This set of values constitutes the (parameter) state of the object, and this state can be saved (\"serialized\"), transmitted (if appropriate), and restored (\"deserialized\") in various ways, so that object state can be sent from one Python session to another, restored from disk, configured using a text file, and so on.\n", "\n", "Param offers several independent serialization mechanisms for a Parameterized object, each used for very different purposes:\n", "- **Pickle**: creates a Python [pickle](https://docs.python.org/3/library/pickle.html) file containing not just the Parameters, but potentially any other state of the object. A pickle file is not human readable, and is not always portable between different python versions, but it is highly complete, capturing both parameter values and also non-Parameter attributes of an object. Useful for saving the entire state of a complex object and restoring it. All objects used in pickling need to be restorable, which puts some restrictions on Parameter values (e.g. requiring named functions, not lambdas).\n", "- **JSON**: captures the state as a JSON text string. Currently and probably always limited in what can be represented, but human readable and easily exchanged with other languages. Useful for sending over a network connection, saving simple state to disk for restoring later, etc.\n", "- **script_repr**: generates a string representation in the form of Python code that, when executed, will instantiate Parameterized objects having similar state. Useful for capturing the current state in a compact, human-readable form suitable for manual editing to create a Python file. Not all Parameters will have values representable in this way (e.g. functions defined in the current namespace will not show their function definition), but this representation is generally a reasonable human-readable starting point for hand editing. " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Pickling Parameterized objects\n", "\n", "Param supports Python's native [pickle](https://docs.python.org/3/library/pickle.html) serialization format. Pickling converts a Python object into a binary stream of bytes that can be stored on disk, and unpickling converts a previously pickled byte stream into an instantiated Python object in the same or a new Python session. Pickling does not capture the actual Python source code or bytecode for functions or classes; instead, it assumes you will have the same Python source tree available for importing those definitions during unpickling and only stores the fully qualified path to those definitions. Thus pickling requires that you use named functions defined in separate importable modules rather than lambdas (unnamed functions) or other objects whose code is defined only in the main namespace or in a non-importable python script. \n", "\n", "Apart from such limitations, pickling is the most rich and _fully featured_ serialization option, capable of capturing the full state of an object even beyond its Parameter values. Pickling is also inherently the _least portable_ option, because it does include all the details of this internal state. The resulting .pkl files are not human readable and are not normally usable outside of Python or even across Python versions in some cases. Pickling is thus most useful for \"snapshots\" (e.g. for checkpoint-and-restore support) for a particular software installation, rather than for exporting, archiving, or configuration. See the [comparison with JSON](https://docs.python.org/3/library/pickle.html#comparison-with-json) to help understand some of the tradeoffs involved in using pickles. \n", "\n", "### Using pickling\n", "\n", "Let's look at an example of pickling and unpickling a Parameterized object:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import param, pickle, time\n", "from param.parameterized import default_label_formatter\n", "\n", "class A(param.Parameterized):\n", " n = param.Number(39)\n", " l = param.List([\"a\",\"b\"])\n", " o = param.ClassSelector(class_=param.Parameterized)\n", " \n", " def __init__(self, **params):\n", " super(A,self).__init__(**params)\n", " self.timestamp = time.time()\n", " \n", "a = A(n=5, l=[1,\"e\",[2]], o=default_label_formatter.instance())\n", "a, a.timestamp" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Here we created a Parameterized object `a` containing another Parameterized object nested in parameter `o`, with state in `self.timestamp` and not just in the Parameter values. To save this state to a file on disk, we can do a pickle \"dump\" and then delete the object so that we are sure it's no longer around:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "with open('data.pickle', 'wb') as f:\n", " pickle.dump(a, f)\n", " \n", "del a" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "To reload the state of `a` from disk, we do a pickle \"load\":" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import pickle\n", "\n", "with open('data.pickle', 'rb') as f:\n", " a = pickle.load(f)\n", " \n", "a, a.timestamp" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "As you can see, it restored not just the Parameter values, but the timestamp (stored in the object's dictionary) as well. \n", "\n", "Here we are depending on the class definition of `A` actually being in memory. If we delete that definition and try to unpickle the object again, it will fail:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "del A\n", "\n", "with param.exceptions_summarized():\n", " with open('data.pickle', 'rb') as f:\n", " a = pickle.load(f)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Notice how the pickle has stored the fact that class `A` is defined in the main namespace, but because `__main__` is not an importable module, unpickling fails. Had `A` been defined in a module available for importing, unpickling would have succeeded here even if A had never previously been loaded.\n", "\n", "To use pickling in practice, you'll need to ensure that all functions and classes are named (not lambdas) and defined in some importable module, not just inline here in a notebook or script or command prompt. Even so, pickling can be very useful as a way to save and restore state of complex Parameterized objects." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Pickling limitations and workarounds\n", "\n", "As you develop a module using Param, you'll need to pay attention to a few technical issues if you want to support pickling:\n", "\n", "1. **Callable parameter values**: If you provide any `param.Callable`, `param.Hooklist`, or other parameters that can accept callable objects to your users, you will need to warn them that none of those can be set to unnamed (lambda) functions or to one-off functions defined in the main namespace if they want to use pickling. Of course, you can accept such values during initial development when you may not care about pickling, but once things are working, move the one-off function to a proper importable module and then it will be safe to use as a picklable value. One way to make this work smoothly is to create `param.ParameterizedFunction` objects or other \"function object\" classes (classes whose instances are callable like functions but which may have state and are fully picklable); see e.g. the `numbergen` module for examples.\n", "\n", "2. **Skipping Parameters that should not be pickled**: In some cases, you may not _want_ the value of a given Parameter to be pickled and restored even while other state is being serialized. For instance, a Parameter whose value is set to a particular file path might cause errors if that path is restored when the pickle is loaded on a different system or once the file no longer exists. To cover such rare but potentially important cases, the Parameter can be defined with `pickle_default_value=False` (normally `True`), so that the instantaneous value is usable but won't be saved and restored with pickle.\n", "\n", "3. **Customizing settting and getting state**: You may find that your Parameter or Parameterized objects have other state that you need to handle specially, whether that's to save and restore data that isn't otherwise picklable, or to ignore state that should _not_ be pickled. For instance, if your object's dictionary contains some object that doesn't support pickling, then you can add code to omit that or to serialize it in some special way that allows it to be restored, e.g. by extracting a state dictionary fom it and then restoring it from the dictionary later. See the [pickle docs](https://docs.python.org/3/library/pickle.html#pickle-state) for the `__getstate__` and `__setstate__` methods that you can implement on your Parameter or Parameterized objects to override this behavior. Be sure to call `super(YourClass,self).__setstate__(state)` or the getstate equivalent so that you also store parameters and dictionary values as usual, if desired.\n", "\n", "4. **Loading old pickle files**: If you use pickles extensively, you may find yourself wanting to support pickle files generated by an older version of your own code, even though your code has since changed (with renamed modules, classes, or parameters, or options that are no longer supported, etc.). By default, unpickling will raise an exception if it finds information in your pickle file that does not match the current Python source code, but it is possible to add custom handling to translate old definitions to match current code, discard no-longer-used options, map from a previous approach into the current approach, etc. You can use `__getstate__` and `__setstate__` on your top-level object or on specific other classes to do just about anything like this, though it can get complicated to reason about. Best practice is to store the module version number or other suitable identifier as an attribute or Parameter on the top-level object to declare what version of the code was used to create the file, and you can then read this identifier later to determine whether you need to apply such conversions on reloading." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Serializing with JSON\n", "\n", "JSON is a human-readable string representation for nested dictionaries of key-value pairs. Compared to pickle, JSON is a much more limited representation, using a fixed set of types mapped to string values, and not natively supporting Python-specific types like tuples or custom Python objects. However, it is widely accepted across computer languages, and because it is human readable and editable and omits the detailed internal state of objects (unlike pickle), JSON works well as an interchange or configuration format.\n", "\n", "Param's JSON support is currently fairly limited, with support for serializing and deserializing individual (not nested) Parameterized objects. It is currently primarily used for synchronizing state \"across the wire\", e.g. between multiple apps running on different machines that communicate changes to shared state (e.g. for a remote GUI), but as proposed in [issue#520](https://github.com/holoviz/param/issues/520) it could be extended to be a general configuration and specification mechanism by adding conventions for specifying a Parameterized type for an object and its nested objects.\n", "\n", "To see how it currently works, let's start with a Parameterized object containing Parameters of different types:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import param, datetime, numpy as np, pandas as pd\n", "\n", "ndarray = np.array([[1,2,3],[4,5,6]])\n", "df = pd.DataFrame({'A':[1,2,3], 'B':[1.1,2.2,3.3]})\n", "\n", "simple_list = [1]\n", "\n", "class P(param.Parameterized):\n", " a = param.Integer(default=5, doc='Int', bounds=(2,30), inclusive_bounds=(True, False))\n", " e = param.List([1,2,3], class_=int)\n", " g = param.Date(default=datetime.datetime.now())\n", " l = param.Range(default=(1.1,2.3), bounds=(1,3))\n", " m = param.String(default='baz', allow_None=True)\n", " s = param.DataFrame(default=df, columns=(1,4), rows=(2,5))\n", "\n", "p = P(a=29)\n", "p" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "To serialize this Parameterized object to a JSON string, call `.serialize_parameters()` on it:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "s = p.param.serialize_parameters()\n", "s" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Notice that the serialization includes not just the values set specifically on this instance (`a=29`), but also all the default values inherited from the class definition.\n", "\n", "You can easily select only a subset to serialize, if you wish:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "p.param.serialize_parameters(subset=['a','m'])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The JSON string can be saved to disk, sent via a network connection, stored in a database, or for any other usage suitable for a string.\n", "\n", "Once you are ready to deserialize the string into a Parameterized object, you'll need to know the class it came from (here `P`) and can then call its `deserialize_parameters` method to get parameter values to use in `P`'s constructor:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "p2 = P(**P.param.deserialize_parameters(s))\n", "p2" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "As you can see, we have successfully serialized our original object `p` into a new object `p2`, which could be in a different Python process on a different machine or at a different date.\n", "\n", "### JSON limitations and workarounds\n", "\n", "To see the limitations on Param's JSON support, let's look at how it works in more detail. Because the result of serialization (`s` above) is a valid JSON string, we can use the `json` library to unpack it without any knowledge of what Parameterized class it came from:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import json\n", "dj = json.loads(s)\n", "dj" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The result is a Python dictionary of name:value pairs, some of which you can recognize as the original type (e.g. `a=29`), others that have changed type (e.g. `l=(1.1,2.3)` or `s=pd.DataFrame({'A':[1,2,3], 'B':[1.1,2.2,3.3]})`), and others that are still a string encoding of that type (e.g. `g=datetime.datetime(...)`)). If you try to pass this dictionary to your Parameterized constructor, any such value will be rejected as invalid by the corresponding Parameter:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "with param.exceptions_summarized():\n", " P(**dj)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "That's why instead of simply `json.loads(s)`, we do `P.param.deserialize_parameters(s)`, which uses the knowledge that `P.l` is a tuple parameter to convert the resulting list `[1.1, 2.3]` into a Python tuple `(1.1, 2.3)` as required for such a parameter:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "print(dj['l'])\n", "print(p2.l)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Similarly, parameters of type `param.Array` will unpack the list representation into a NumPy array, `param.DataFrame` unpacks the list of dicts of list into a Pandas DataFrame, etc. So, the encoding for your Parameterized object will always be standard JSON, but to _deserialize_ it fully into a Parameterized, you'll need to know the class it came from, or Param will not know that the list it finds was originally a tuple, dataframe, etc. \n", "\n", "For this reason, any Parameter that itself contains a Parameterized object will not be able to be JSON deserialized, since even if we knew what class it was (e.g. for `param.ClassSelector(class_=param.Number)`, it could be some subclass of that class. Because the class name is not currently stored in the JSON serialization, there is no way to restore it. Thus there is currently no support for JSON serializing or deserializing nested Parameterized objects.\n", "\n", "We do expect to add support for nested objects using something like the convention for datetime objects; see [issue#520](https://github.com/holoviz/param/issues/520)." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### JSON Schemas \n", "\n", "If you want to use your JSON representation in a separate process where Param is not available or perhaps in a different language altogether, Param can provide a [JSON schema](https://json-schema.org/) that specifies what type you are expecting for each Parameter. The schema for a given Parameterized can be obtained using the `schema` method:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "p.param.schema()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Once you have the schema, you can validate that a given JSON string matches the schema, i.e. that all values included therein match the constraints listed in the schema:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from jsonschema import validate\n", "d = json.loads(s)\n", "full_schema = {\"type\" : \"object\", \"properties\" : p.param.schema()}\n", "validate(instance=d, schema=full_schema)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "If one of the parameter values fails to match the provided schema, you'll get an exception:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "d2 = d.copy()\n", "d2['a']='astring'\n", "\n", "with param.exceptions_summarized():\n", " validate(instance=d2, schema=full_schema)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The `param.schema()` call accepts the same `subset` argument as `.param.serialize_parameters()`, letting you serialize and check only a subset of the parameters if appropriate. \n", "\n", "You can also supply a `safe=True` argument that checks that all parameter values are _guaranteed_ to be serializable and follow the given schema. This lets you detect if there are any containers or parameters whose type is not fully specified:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "with param.exceptions_summarized():\n", " full2 = {\"type\" : \"object\", \"properties\" : p.param.schema(safe=True)}\n", " validate(instance=d, schema=full2)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## script_repr\n", "\n", "Parameterized objects can be constructed through a series of interactive actions, either in a GUI or command line, or as the result of automated scripts and object-construction functions. Any parameter values can also be changed at any moment once that object has been created. If you want to capture the resulting Parameterized object with any such additions and changes, you can use the `param.script_repr()` function. `script_repr` returns a representation of that object and all nested Parameterized or other supported objects as Python code that can recreate the full object later. This approach lets you go flexibly from an interactive or indirect way of creating or modifying objects, to being able to recreate that specific object again for later use. Programs with a GUI interface can use `script_repr()` as a way of exporting a runnable version of what a user created interactively in the GUI.\n", "\n", "For example, let's construct a Parameterized object `p` containing Parameters whose values are themselves Parameterized objects with their own Parameters:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import param\n", "\n", "class Q(param.Parameterized):\n", " a = param.Number(39, bounds=(0,50))\n", " b = param.String(\"str\")\n", "\n", "class P(param.Parameterized):\n", " c = param.ClassSelector(Q, Q())\n", " d = param.ClassSelector(param.Parameterized, param.Parameterized())\n", " e = param.Range((0,1))\n", " \n", "q = Q(b=\"new\")\n", "p=P(c=q, e=(2,3))\n", "p" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We can get a script representation for this object by calling `script_repr(p)`:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "print(param.script_repr(p))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "As you can see, this representation encodes the fact that `P` was defined in the main namespace, generated inside this notebook. As you might expect, this representation has the same limitation as for `pickle` -- only classes that are in importable modules will be runnable; you'll need to save the source code to your classes in a proper Python module if you want the resulting script to be runnable. But once you have done that, you can use the `script_repr` to get a runnable version of your Parameterized object no matter how you created it, whether it was by selecting options in a GUI, adding items via a loop in a script, and so on." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### script_repr limitations and workarounds\n", "\n", "Apart from making sure your functions and classes are all defined in their own importable modules, there are various considerations and limitations to keep in mind if you want to support using `script_repr`. " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Normally, script_repr prints only parameter values that have changed from their defaults; it is designed to generate a script as close as is practical to one that a user would have typed to create the given object. If you want a record of the _complete_ set of parameter values, including all defaults, you can enable that behavior:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import param.parameterized\n", "param.parameterized.script_repr_suppress_defaults=True" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The resulting output is then suitable for archiving the full parameter state of that object, even if some default later gets changed in the source code. Note that Param is not able to detect all cases where a default value is unchanged, e.g. for Parameters with `instantiate=True`, which will always be treated as changed since each instance has a copy of that Parameter value independent of the original default value.\n", "\n", "You can control `script_repr` with keyword arguments:\n", "\n", "- `imports=[]`: If desired, a list of imports that can be built up over multiple script_repr calls to collect a full set of imports required for a script. Useful with `show_imports=False` except on the last script_repr call. Can be an empty list or a list containing some hard-coded imports needed.\n", "- `prefix=\"\\n \"`: Optional prefix to use before a nested object.\n", "- `qualify=True`: Whether the class's path will be included (e.g. \"a.b.C()\"), otherwise only the class will appear (\"C()\").\n", "- `unknown_value=None`: determines what to do where a representation cannot be generated for something required to recreate the object. Such things include non-parameter positional and keyword arguments, and certain values of parameters (e.g. some random state objects). Supplying an `unknown_value` of `None` causes unrepresentable things to be silently ignored. If `unknown_value` is a string, that string will appear in place of any unrepresentable things. If `unknown_value` is `False`, an Exception will be raised if an unrepresentable value is encountered. \n", "- `separator=\"\\n\"`: Separator to use between parameters.\n", "- `show_imports=True`: Whether to include import statements in the output.\n", "\n", "\n", "The `script_repr` behavior for a particular type, whether it's a Parameterized object or not, can be overridden to provide any functionality needed. Such overrides are stored in `param.parameterized.script_repr_reg`, which already contains handling for list and tuple containers, various objects with random state, functions, and modules. See examples in \n", "`param.parameterized`." ] } ], "metadata": { "language_info": { "name": "python", "pygments_lexer": "ipython3" } }, "nbformat": 4, "nbformat_minor": 5 } param-1.12.3/examples/user_guide/Simplifying_Codebases.ipynb000066400000000000000000000351461434441564000241720ustar00rootroot00000000000000{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Simplifying Codebases\n", "\n", "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 param.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 param.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 param.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 param.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 param.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": 4 } param-1.12.3/numbergen/000077500000000000000000000000001434441564000146655ustar00rootroot00000000000000param-1.12.3/numbergen/__init__.py000066400000000000000000000670461434441564000170130ustar00rootroot00000000000000""" Callable objects that generate numbers according to different distributions. """ import random import operator import hashlib import struct import fractions from ctypes import c_size_t from math import e,pi import param from param import __version__ # noqa: API import class TimeAware(param.Parameterized): """ Class of objects that have access to a global time function and have the option of using it to generate time-dependent values as necessary. In the simplest case, an object could act as a strict function of time, returning the current time transformed according to a fixed equation. Other objects may support locking their results to a timebase, but also work without time. For instance, objects with random state could return a new random value for every call, with no notion of time, or could always return the same value until the global time changes. Subclasses should thus provide an ability to return a time-dependent value, but may not always do so. """ time_dependent = param.Boolean(default=False, doc=""" Whether the given time_fn should be used to constrain the results generated.""") time_fn = param.Callable(default=param.Dynamic.time_fn, doc=""" Callable used to specify the time that determines the state and return value of the object, if time_dependent=True.""") def __init__(self, **params): super(TimeAware, self).__init__(**params) self._check_time_fn() def _check_time_fn(self, time_instance=False): """ If time_fn is the global time function supplied by param.Dynamic.time_fn, make sure Dynamic parameters are using this time function to control their behaviour. If time_instance is True, time_fn must be a param.Time instance. """ if time_instance and not isinstance(self.time_fn, param.Time): raise AssertionError("%s requires a Time object" % self.__class__.__name__) if self.time_dependent: global_timefn = self.time_fn is param.Dynamic.time_fn if global_timefn and not param.Dynamic.time_dependent: raise AssertionError("Cannot use Dynamic.time_fn as" " parameters are ignoring time.") class TimeDependent(TimeAware): """ Objects that have access to a time function that determines the output value. As a function of time, this type of object should allow time values to be randomly jumped forwards or backwards, but for a given time point, the results should remain constant. The time_fn must be an instance of param.Time, to ensure all the facilities necessary for safely navigating the timeline are available. """ time_dependent = param.Boolean(default=True, readonly=True, doc=""" Read-only parameter that is always True.""") def _check_time_fn(self): super(TimeDependent,self)._check_time_fn(time_instance=True) class NumberGenerator(param.Parameterized): """ Abstract base class for any object that when called produces a number. Primarily provides support for using NumberGenerators in simple arithmetic expressions, such as abs((x+y)/z), where x,y,z are NumberGenerators or numbers. """ def __call__(self): raise NotImplementedError # Could define any of Python's operators here, esp. if they have operator or ufunc equivalents def __add__ (self,operand): return BinaryOperator(self,operand,operator.add) def __sub__ (self,operand): return BinaryOperator(self,operand,operator.sub) def __mul__ (self,operand): return BinaryOperator(self,operand,operator.mul) def __mod__ (self,operand): return BinaryOperator(self,operand,operator.mod) def __pow__ (self,operand): return BinaryOperator(self,operand,operator.pow) def __div__ (self,operand): return BinaryOperator(self,operand,operator.div) def __truediv__ (self,operand): return BinaryOperator(self,operand,operator.truediv) def __floordiv__ (self,operand): return BinaryOperator(self,operand,operator.floordiv) def __radd__ (self,operand): return BinaryOperator(self,operand,operator.add,True) def __rsub__ (self,operand): return BinaryOperator(self,operand,operator.sub,True) def __rmul__ (self,operand): return BinaryOperator(self,operand,operator.mul,True) def __rmod__ (self,operand): return BinaryOperator(self,operand,operator.mod,True) def __rpow__ (self,operand): return BinaryOperator(self,operand,operator.pow,True) def __rdiv__ (self,operand): return BinaryOperator(self,operand,operator.div,True) def __rtruediv__ (self,operand): return BinaryOperator(self,operand,operator.truediv,True) def __rfloordiv__(self,operand): return BinaryOperator(self,operand,operator.floordiv,True) def __neg__ (self): return UnaryOperator(self,operator.neg) def __pos__ (self): return UnaryOperator(self,operator.pos) def __abs__ (self): return UnaryOperator(self,operator.abs) operator_symbols = { operator.add:'+', operator.sub:'-', operator.mul:'*', operator.mod:'%', operator.pow:'**', operator.truediv:'/', operator.floordiv:'//', operator.neg:'-', operator.pos:'+', operator.abs:'abs', } def pprint(x, *args, **kwargs): "Pretty-print the provided item, translating operators to their symbols" return x.pprint(*args, **kwargs) if hasattr(x,'pprint') else operator_symbols.get(x, repr(x)) class BinaryOperator(NumberGenerator): """Applies any binary operator to NumberGenerators or numbers to yield a NumberGenerator.""" def __init__(self,lhs,rhs,operator,reverse=False,**args): """ Accepts two NumberGenerator operands, an operator, and optional arguments to be provided to the operator when calling it on the two operands. """ # Note that it's currently not possible to set # parameters in the superclass when creating an instance, # because **args is used by this class itself. super(BinaryOperator,self).__init__() if reverse: self.lhs=rhs self.rhs=lhs else: self.lhs=lhs self.rhs=rhs self.operator=operator self.args=args def __call__(self): return self.operator(self.lhs() if callable(self.lhs) else self.lhs, self.rhs() if callable(self.rhs) else self.rhs, **self.args) def pprint(self, *args, **kwargs): return (pprint(self.lhs, *args, **kwargs) + pprint(self.operator, *args, **kwargs) + pprint(self.rhs, *args, **kwargs)) class UnaryOperator(NumberGenerator): """Applies any unary operator to a NumberGenerator to yield another NumberGenerator.""" def __init__(self,operand,operator,**args): """ Accepts a NumberGenerator operand, an operator, and optional arguments to be provided to the operator when calling it on the operand. """ # Note that it's currently not possible to set # parameters in the superclass when creating an instance, # because **args is used by this class itself. super(UnaryOperator,self).__init__() self.operand=operand self.operator=operator self.args=args def __call__(self): return self.operator(self.operand(),**self.args) def pprint(self, *args, **kwargs): return (pprint(self.operator, *args, **kwargs) + '(' + pprint(self.operand, *args, **kwargs) + ')') class Hash(object): """ A platform- and architecture-independent hash function (unlike Python's inbuilt hash function) for use with an ordered collection of rationals or integers. The supplied name sets the initial hash state. The output from each call is a 32-bit integer to ensure the value is a regular Python integer (and not a Python long) on both 32-bit and 64-bit platforms. This can be important to seed Numpy's random number generator safely (a bad Numpy bug!). The number of inputs (integer or rational numbers) to be supplied for __call__ must be specified in the constructor and must stay constant across calls. """ def __init__(self, name, input_count): self.name = name self.input_count = input_count self._digest = hashlib.md5() self._digest.update(name.encode()) self._hash_struct = struct.Struct( "!" +" ".join(["I"] * (input_count * 2))) def _rational(self, val): """Convert the given value to a rational, if necessary.""" I32 = 4294967296 # Maximum 32 bit unsigned int (i.e. 'I') value if isinstance(val, int): numer, denom = val, 1 elif isinstance(val, fractions.Fraction): numer, denom = val.numerator, val.denominator elif hasattr(val, 'numer'): (numer, denom) = (int(val.numer()), int(val.denom())) else: param.main.param.log(param.WARNING, "Casting type '%s' to Fraction.fraction" % type(val).__name__) frac = fractions.Fraction(str(val)) numer, denom = frac.numerator, frac.denominator return numer % I32, denom % I32 def __getstate__(self): """ Avoid Hashlib.md5 TypeError in deepcopy (hashlib issue) """ d = self.__dict__.copy() d.pop('_digest') d.pop('_hash_struct') return d def __setstate__(self, d): self._digest = hashlib.md5() name, input_count = d['name'], d['input_count'] self._digest.update(name.encode()) self._hash_struct = struct.Struct( "!" +" ".join(["I"] * (input_count * 2))) self.__dict__.update(d) def __call__(self, *vals): """ Given integer or rational inputs, generate a cross-platform, architecture-independent 32-bit integer hash. """ # Convert inputs to (numer, denom) pairs with integers # becoming (int, 1) pairs to match gmpy.mpqs for int values. pairs = [self._rational(val) for val in vals] # Unpack pairs and fill struct with ints to update md5 hash ints = [el for pair in pairs for el in pair] digest = self._digest.copy() digest.update(self._hash_struct.pack(*ints)) # Convert from hex string to 32 bit int return int(digest.hexdigest()[:7], 16) class TimeAwareRandomState(TimeAware): """ Generic base class to enable time-dependent random streams. Although this class is the basis of all random numbergen classes, it is designed to be useful whenever time-dependent randomness is needed using param's notion of time. For instance, this class is used by the imagen package to define time-dependent, random distributions over 2D arrays. For generality, this class may use either the Random class from Python's random module or numpy.random.RandomState. Either of these random state objects may be used to generate numbers from any of several different random distributions (e.g. uniform, Gaussian). The latter offers the ability to generate multi-dimensional random arrays and more random distributions but requires numpy as a dependency. If declared time_dependent, the random state is fully determined by a hash value per call. The hash is initialized once with the object name and then per call using a tuple consisting of the time (via time_fn) and the global param.random_seed. As a consequence, for a given name and fixed value of param.random_seed, the random values generated will be a fixed function of time. If the object name has not been set and time_dependent is True, a message is generated warning that the default object name is dependent on the order of instantiation. To ensure that the random number stream will remain constant even if other objects are added or reordered in your file, supply a unique name explicitly when you construct the RandomDistribution object. """ # Historically, the default random state was seeded with the tuple # (500, 500). The CPython implementation implicitly formed an unsigned # integer seed using the hash of the tuple as in the expression below. Note # that the resulting integer, and therefore the default initial random # state, varies across CPython versions (as the hash algorithm has changed) # and also between 32-bit and 64-bit interpreters. # # Seeding based on hashing is deprecated since Python 3.9 and removed in # Python 3.11; we explicitly continue the historical behavior for the time # being. random_generator = param.Parameter( default=random.Random(c_size_t(hash((500,500))).value), doc= """ Random state used by the object. This may may be an instance of random.Random from the Python standard library or an instance of numpy.random.RandomState. This random state may be exclusively owned by the object or may be shared by all instance of the same class. It is always possible to give an object its own unique random state by setting this parameter with a new random state instance. """) __abstract = True def _initialize_random_state(self, seed=None, shared=True, name=None): """ Initialization method to be called in the constructor of subclasses to initialize the random state correctly. If seed is None, there is no control over the random stream (no reproducibility of the stream). If shared is True (and not time-dependent), the random state is shared across all objects of the given class. This can be overridden per object by creating new random state to assign to the random_generator parameter. """ if seed is None: # Equivalent to an uncontrolled seed. seed = random.Random().randint(0, 1000000) suffix = '' else: suffix = str(seed) # If time_dependent, independent state required: otherwise # time-dependent seeding (via hash) will affect shared # state. Note that if all objects have time_dependent=True # shared random state is safe and more memory efficient. if self.time_dependent or not shared: self.random_generator = type(self.random_generator)(seed) # Seed appropriately (if not shared) if not shared: self.random_generator.seed(seed) if name is None: self._verify_constrained_hash() hash_name = name if name else self.name if not shared: hash_name += suffix self._hashfn = Hash(hash_name, input_count=2) if self.time_dependent: self._hash_and_seed() def _verify_constrained_hash(self): """ Warn if the object name is not explicitly set. """ changed_params = self.param.values(onlychanged=True) if self.time_dependent and ('name' not in changed_params): self.param.log(param.WARNING, "Default object name used to set the seed: " "random values conditional on object instantiation order.") def _hash_and_seed(self): """ To be called between blocks of random number generation. A 'block' can be an unbounded sequence of random numbers so long as the time value (as returned by time_fn) is guaranteed not to change within the block. If this condition holds, each block of random numbers is time-dependent. Note: param.random_seed is assumed to be integer or rational. """ hashval = self._hashfn(self.time_fn(), param.random_seed) self.random_generator.seed(hashval) class RandomDistribution(NumberGenerator, TimeAwareRandomState): """ The base class for all Numbergenerators using random state. Numbergen provides a hierarchy of classes to make it easier to use the random distributions made available in Python's random module, where each class is tied to a particular random distribution. RandomDistributions support setting parameters on creation rather than passing them each call, and allow pickling to work properly. Code that uses these classes will be independent of how many parameters are used by the underlying distribution, and can simply treat them as a generic source of random numbers. RandomDistributions are examples of TimeAwareRandomState, and thus can be locked to a global time if desired. By default, time_dependent=False, and so a new random value will be generated each time these objects are called. If you have a global time function, you can set time_dependent=True, so that the random values will instead be constant at any given time, changing only when the time changes. Using time_dependent values can help you obtain fully reproducible streams of random numbers, even if you e.g. move time forwards and backwards for testing. Note: Each RandomDistribution object has independent random state. """ seed = param.Integer(default=None, allow_None=True, doc=""" Sets the seed of the random number generator and can be used to randomize time dependent streams. If seed is None, there is no control over the random stream (i.e. no reproducibility of the stream).""") __abstract = True def __init__(self,**params): """ Initialize a new Random() instance and store the supplied positional and keyword arguments. If seed=X is specified, sets the Random() instance's seed. Otherwise, calls creates an unseeded Random instance which is likely to result in a state very different from any just used. """ super(RandomDistribution,self).__init__(**params) self._initialize_random_state(seed=self.seed, shared=False) def __call__(self): if self.time_dependent: self._hash_and_seed() class UniformRandom(RandomDistribution): """ Specified with lbound and ubound; when called, return a random number in the range [lbound, ubound). See the random module for further details. """ lbound = param.Number(default=0.0,doc="inclusive lower bound") ubound = param.Number(default=1.0,doc="exclusive upper bound") def __call__(self): super(UniformRandom, self).__call__() return self.random_generator.uniform(self.lbound,self.ubound) class UniformRandomOffset(RandomDistribution): """ Identical to UniformRandom, but specified by mean and range. When called, return a random number in the range [mean - range/2, mean + range/2). See the random module for further details. """ mean = param.Number(default=0.0, doc="""Mean value""") range = param.Number(default=1.0, bounds=(0.0,None), doc=""" Difference of maximum and minimum value""") def __call__(self): super(UniformRandomOffset, self).__call__() return self.random_generator.uniform( self.mean - self.range / 2.0, self.mean + self.range / 2.0) class UniformRandomInt(RandomDistribution): """ Specified with lbound and ubound; when called, return a random number in the inclusive range [lbound, ubound]. See the randint function in the random module for further details. """ lbound = param.Number(default=0,doc="inclusive lower bound") ubound = param.Number(default=1000,doc="inclusive upper bound") def __call__(self): super(UniformRandomInt, self).__call__() x = self.random_generator.randint(self.lbound,self.ubound) return x class Choice(RandomDistribution): """ Return a random element from the specified list of choices. Accepts items of any type, though they are typically numbers. See the choice() function in the random module for further details. """ choices = param.List(default=[0,1], doc="List of items from which to select.") def __call__(self): super(Choice, self).__call__() return self.random_generator.choice(self.choices) class NormalRandom(RandomDistribution): """ Normally distributed (Gaussian) random number. Specified with mean mu and standard deviation sigma. See the random module for further details. """ mu = param.Number(default=0.0,doc="Mean value.") sigma = param.Number(default=1.0,bounds=(0.0,None),doc="Standard deviation.") def __call__(self): super(NormalRandom, self).__call__() return self.random_generator.normalvariate(self.mu,self.sigma) class VonMisesRandom(RandomDistribution): """ Circularly normal distributed random number. If kappa is zero, this distribution reduces to a uniform random angle over the range 0 to 2*pi. Otherwise, it is concentrated to a greater or lesser degree (determined by kappa) around the mean mu. For large kappa (narrow peaks), this distribution approaches the Gaussian (normal) distribution with variance 1/kappa. See the random module for further details. """ mu = param.Number(default=0.0,softbounds=(0.0,2*pi),doc=""" Mean value, typically in the range 0 to 2*pi.""") kappa = param.Number(default=1.0,bounds=(0.0,None),softbounds=(0.0,50.0),doc=""" Concentration (inverse variance).""") def __call__(self): super(VonMisesRandom, self).__call__() return self.random_generator.vonmisesvariate(self.mu,self.kappa) class ScaledTime(NumberGenerator, TimeDependent): """ The current time multiplied by some conversion factor. """ factor = param.Number(default=1.0, doc=""" The factor to be multiplied by the current time value.""") def __call__(self): return float(self.time_fn() * self.factor) class BoxCar(NumberGenerator, TimeDependent): """ The boxcar function over the specified time interval. The bounds are exclusive: zero is returned at the onset time and at the offset (onset+duration). If duration is None, then this reduces to a step function around the onset value with no offset. See http://en.wikipedia.org/wiki/Boxcar_function """ onset = param.Number(0.0, doc="Time of onset.") duration = param.Number(None, allow_None=True, bounds=(0.0,None), doc=""" Duration of step value.""") def __call__(self): if self.time_fn() <= self.onset: return 0.0 elif (self.duration is not None) and (self.time_fn() > self.onset + self.duration): return 0.0 else: return 1.0 class SquareWave(NumberGenerator, TimeDependent): """ Generate a square wave with 'on' periods returning 1.0 and 'off'periods returning 0.0 of specified duration(s). By default the portion of time spent in the high state matches the time spent in the low state (a duty cycle of 50%), but the duty cycle can be controlled if desired. The 'on' state begins after a time specified by the 'onset' parameter. The onset duration supplied must be less than the off duration. """ onset = param.Number(0.0, doc="""Time of onset of the first 'on' state relative to time 0. Must be set to a value less than the 'off_duration' parameter.""") duration = param.Number(1.0, allow_None=False, bounds=(0.0,None), doc=""" Duration of the 'on' state during which a value of 1.0 is returned.""") off_duration = param.Number(default=None, allow_None=True, bounds=(0.0,None), doc=""" Duration of the 'off' value state during which a value of 0.0 is returned. By default, this duration matches the value of the 'duration' parameter.""") def __init__(self, **params): super(SquareWave,self).__init__(**params) if self.off_duration is None: self.off_duration = self.duration if self.onset > self.off_duration: raise AssertionError("Onset value needs to be less than %s" % self.onset) def __call__(self): phase_offset = (self.time_fn() - self.onset) % (self.duration + self.off_duration) if phase_offset < self.duration: return 1.0 else: return 0.0 class ExponentialDecay(NumberGenerator, TimeDependent): """ Function object that provides a value that decays according to an exponential function, based on a given time function. Returns starting_value*base^(-time/time_constant). See http://en.wikipedia.org/wiki/Exponential_decay. """ starting_value = param.Number(1.0, doc="Value used for time zero.") ending_value = param.Number(0.0, doc="Value used for time infinity.") time_constant = param.Number(10000,doc=""" Time scale for the exponential; large values give slow decay.""") base = param.Number(e, doc=""" Base of the exponent; the default yields starting_value*exp(-t/time_constant). Another popular choice of base is 2, which allows the time_constant to be interpreted as a half-life.""") def __call__(self): Vi = self.starting_value Vm = self.ending_value exp = -1.0*float(self.time_fn())/float(self.time_constant) return Vm + (Vi - Vm) * self.base**exp class TimeSampledFn(NumberGenerator, TimeDependent): """ Samples the values supplied by a time_dependent callable at regular intervals of duration 'period', with the sampled value held constant within each interval. """ period = param.Number(default=1.0, bounds=(0.0,None), inclusive_bounds=(False,True), softbounds=(0.0,5.0), doc=""" The periodicity with which the values of fn are sampled.""") offset = param.Number(default=0.0, bounds=(0.0,None), softbounds=(0.0,5.0), doc=""" The offset from time 0.0 at which the first sample will be drawn. Must be less than the value of period.""") fn = param.Callable(doc=""" The time-dependent function used to generate the sampled values.""") def __init__(self, **params): super(TimeSampledFn, self).__init__(**params) if not getattr(self.fn,'time_dependent', False): raise Exception("The function 'fn' needs to be time dependent.") if self.time_fn != self.fn.time_fn: raise Exception("Objects do not share the same time_fn") if self.offset >= self.period: raise Exception("The onset value must be less than the period.") def __call__(self): current_time = self.time_fn() current_time += self.offset difference = current_time % self.period with self.time_fn as t: t(current_time - difference - self.offset) value = self.fn() return value class BoundedNumber(NumberGenerator): """ Function object that silently enforces numeric bounds on values returned by a callable object. """ generator = param.Callable(None, doc="Object to call to generate values.") bounds = param.Parameter((None,None), doc=""" Legal range for the value returned, as a pair. The default bounds are (None,None), meaning there are actually no bounds. One or both bounds can be set by specifying a value. For instance, bounds=(None,10) means there is no lower bound, and an upper bound of 10.""") def __call__(self): val = self.generator() min_, max_ = self.bounds if min_ is not None and val < min_: return min_ elif max_ is not None and val > max_: return max_ else: return val _public = list(set([_k for _k,_v in locals().items() if isinstance(_v,type) and issubclass(_v,NumberGenerator)])) __all__ = _public param-1.12.3/param/000077500000000000000000000000001434441564000140035ustar00rootroot00000000000000param-1.12.3/param/__init__.py000066400000000000000000002413111434441564000161160ustar00rootroot00000000000000from __future__ import print_function """ Parameters are a kind of class attribute allowing special behavior, including dynamically generated parameter values, documentation strings, constant and read-only parameters, and type or range checking at assignment time. Potentially useful for any large Python program that needs user-modifiable object attributes; see the Parameter and Parameterized classes for more information. If you do not want to add a dependency on external code by importing from a separately installed param package, you can simply save this file as param.py and copy it and parameterized.py directly into your own package. This file contains subclasses of Parameter, implementing specific parameter types (e.g. Number), and also imports the definition of Parameters and Parameterized classes. """ import os.path import sys import copy import glob import re import datetime as dt import collections from .parameterized import ( Parameterized, Parameter, String, ParameterizedFunction, ParamOverrides, descendents, get_logger, instance_descriptor, basestring, dt_types) from .parameterized import (batch_watch, depends, output, script_repr, # noqa: api import discard_events, edit_constant, instance_descriptor) from .parameterized import shared_parameters # noqa: api import from .parameterized import logging_level # noqa: api import from .parameterized import DEBUG, VERBOSE, INFO, WARNING, ERROR, CRITICAL # noqa: api import from collections import OrderedDict from numbers import Real # Determine up-to-date version information, if possible, but with a # safe fallback to ensure that this file and parameterized.py are the # only two required files. try: from .version import Version __version__ = str(Version(fpath=__file__, archive_commit="897687f71", reponame="param")) except: __version__ = "0.0.0+unknown" try: import collections.abc as collections_abc except ImportError: collections_abc = collections if sys.version_info[0] >= 3: unicode = str #: Top-level object to allow messaging not tied to a particular #: Parameterized object, as in 'param.main.warning("Invalid option")'. main=Parameterized(name="main") # A global random seed (integer or rational) available for controlling # the behaviour of Parameterized objects with random state. random_seed = 42 def produce_value(value_obj): """ A helper function that produces an actual parameter from a stored object: if the object is callable, call it, otherwise return the object. """ if callable(value_obj): return value_obj() else: return value_obj def as_unicode(obj): """ Safely casts any object to unicode including regular string (i.e. bytes) types in python 2. """ if sys.version_info.major < 3 and isinstance(obj, str): obj = obj.decode('utf-8') return unicode(obj) def is_ordered_dict(d): """ Predicate checking for ordered dictionaries. OrderedDict is always ordered, and vanilla Python dictionaries are ordered for Python 3.6+ """ py3_ordered_dicts = (sys.version_info.major == 3) and (sys.version_info.minor >= 6) vanilla_odicts = (sys.version_info.major > 3) or py3_ordered_dicts return isinstance(d, (OrderedDict))or (vanilla_odicts and isinstance(d, dict)) def hashable(x): """ Return a hashable version of the given object x, with lists and dictionaries converted to tuples. Allows mutable objects to be used as a lookup key in cases where the object has not actually been mutated. Lookup will fail (appropriately) in cases where some part of the object has changed. Does not (currently) recursively replace mutable subobjects. """ if isinstance(x, collections_abc.MutableSequence): return tuple(x) elif isinstance(x, collections_abc.MutableMapping): return tuple([(k,v) for k,v in x.items()]) else: return x def named_objs(objlist, namesdict=None): """ Given a list of objects, returns a dictionary mapping from string name for the object to the object itself. Accepts an optional name,obj dictionary, which will override any other name if that item is present in the dictionary. """ objs = OrderedDict() objtoname = {} unhashables = [] if namesdict is not None: for k, v in namesdict.items(): try: objtoname[hashable(v)] = k except TypeError: unhashables.append((k, v)) for obj in objlist: if objtoname and hashable(obj) in objtoname: k = objtoname[hashable(obj)] elif any(obj is v for (_, v) in unhashables): k = [k for (k, v) in unhashables if v is obj][0] elif hasattr(obj, "name"): k = obj.name elif hasattr(obj, '__name__'): k = obj.__name__ else: k = as_unicode(obj) objs[k] = obj return objs def param_union(*parameterizeds, **kwargs): """ Given a set of Parameterized objects, returns a dictionary with the union of all param name,value pairs across them. If warn is True (default), warns if the same parameter has been given multiple values; otherwise uses the last value """ warn = kwargs.pop('warn', True) if len(kwargs): raise TypeError( "param_union() got an unexpected keyword argument '{}'".format( kwargs.popitem()[0])) d = dict() for o in parameterizeds: for k in o.param: if k != 'name': if k in d and warn: get_logger().warning("overwriting parameter {}".format(k)) d[k] = getattr(o, k) return d def guess_param_types(**kwargs): """ Given a set of keyword literals, promote to the appropriate parameter type based on some simple heuristics. """ params = {} for k, v in kwargs.items(): kws = dict(default=v, constant=True) if isinstance(v, Parameter): params[k] = v elif isinstance(v, dt_types): params[k] = Date(**kws) elif isinstance(v, bool): params[k] = Boolean(**kws) elif isinstance(v, int): params[k] = Integer(**kws) elif isinstance(v, float): params[k] = Number(**kws) elif isinstance(v, str): params[k] = String(**kws) elif isinstance(v, dict): params[k] = Dict(**kws) elif isinstance(v, tuple): if all(_is_number(el) for el in v): params[k] = NumericTuple(**kws) elif all(isinstance(el, dt_types) for el in v) and len(v)==2: params[k] = DateRange(**kws) else: params[k] = Tuple(**kws) elif isinstance(v, list): params[k] = List(**kws) else: if 'numpy' in sys.modules: from numpy import ndarray if isinstance(v, ndarray): params[k] = Array(**kws) continue if 'pandas' in sys.modules: from pandas import ( DataFrame as pdDFrame, Series as pdSeries ) if isinstance(v, pdDFrame): params[k] = DataFrame(**kws) continue elif isinstance(v, pdSeries): params[k] = Series(**kws) continue params[k] = Parameter(**kws) return params def parameterized_class(name, params, bases=Parameterized): """ Dynamically create a parameterized class with the given name and the supplied parameters, inheriting from the specified base(s). """ if not (isinstance(bases, list) or isinstance(bases, tuple)): bases=[bases] return type(name, tuple(bases), params) def guess_bounds(params, **overrides): """ Given a dictionary of Parameter instances, return a corresponding set of copies with the bounds appropriately set. If given a set of override keywords, use those numeric tuple bounds. """ guessed = {} for name, p in params.items(): new_param = copy.copy(p) if isinstance(p, (Integer, Number)): if name in overrides: minv,maxv = overrides[name] else: minv, maxv, _ = _get_min_max_value(None, None, value=p.default) new_param.bounds = (minv, maxv) guessed[name] = new_param return guessed def _get_min_max_value(min, max, value=None, step=None): """Return min, max, value given input values with possible None.""" # Either min and max need to be given, or value needs to be given if value is None: if min is None or max is None: raise ValueError('unable to infer range, value ' 'from: ({0}, {1}, {2})'.format(min, max, value)) diff = max - min value = min + (diff / 2) # Ensure that value has the same type as diff if not isinstance(value, type(diff)): value = min + (diff // 2) else: # value is not None if not isinstance(value, Real): raise TypeError('expected a real number, got: %r' % value) # Infer min/max from value if value == 0: # This gives (0, 1) of the correct type vrange = (value, value + 1) elif value > 0: vrange = (-value, 3*value) else: vrange = (3*value, -value) if min is None: min = vrange[0] if max is None: max = vrange[1] if step is not None: # ensure value is on a step tick = int((value - min) / step) value = min + tick * step if not min <= value <= max: raise ValueError('value must be between min and max (min={0}, value={1}, max={2})'.format(min, value, max)) return min, max, value class Infinity(object): """ An instance of this class represents an infinite value. Unlike Python's float('inf') value, this object can be safely compared with gmpy numeric types across different gmpy versions. All operators on Infinity() return Infinity(), apart from the comparison and equality operators. Equality works by checking whether the two objects are both instances of this class. """ def __eq__ (self,other): return isinstance(other,self.__class__) def __ne__ (self,other): return not self==other def __lt__ (self,other): return False def __le__ (self,other): return False def __gt__ (self,other): return True def __ge__ (self,other): return True def __add__ (self,other): return self def __radd__(self,other): return self def __ladd__(self,other): return self def __sub__ (self,other): return self def __iadd_ (self,other): return self def __isub__(self,other): return self def __repr__(self): return "Infinity()" def __str__ (self): return repr(self) class Time(Parameterized): """ A callable object returning a number for the current time. Here 'time' is an abstract concept that can be interpreted in any useful way. For instance, in a simulation, it would be the current simulation time, while in a turn-taking game it could be the number of moves so far. The key intended usage is to allow independent Parameterized objects with Dynamic parameters to remain consistent with a global reference. The time datatype (time_type) is configurable, but should typically be an exact numeric type like an integer or a rational, so that small floating-point errors do not accumulate as time is incremented repeatedly. When used as a context manager using the 'with' statement (implemented by the __enter__ and __exit__ special methods), entry into a context pushes the state of the Time object, allowing the effect of changes to the time value to be explored by setting, incrementing or decrementing time as desired. This allows the state of time-dependent objects to be modified temporarily as a function of time, within the context's block. For instance, you could use the context manager to "see into the future" to collect data over multiple times, without affecting the global time state once exiting the context. Of course, you need to be careful not to do anything while in context that would affect the lasting state of your other objects, if you want things to return to their starting state when exiting the context. The starting time value of a new Time object is 0, converted to the chosen time type. Here is an illustration of how time can be manipulated using a Time object: >>> time = Time(until=20, timestep=1) >>> 'The initial time is %s' % time() 'The initial time is 0' >>> 'Setting the time to %s' % time(5) 'Setting the time to 5' >>> time += 5 >>> 'After incrementing by 5, the time is %s' % time() 'After incrementing by 5, the time is 10' >>> with time as t: # Entering a context ... 'Time before iteration: %s' % t() ... 'Iteration: %s' % [val for val in t] ... 'Time after iteration: %s' % t() ... t += 2 ... 'The until parameter may be exceeded outside iteration: %s' % t() 'Time before iteration: 10' 'Iteration: [10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]' 'Time after iteration: 20' 'The until parameter may be exceeded outside iteration: 22' >>> 'After exiting the context the time is back to %s' % time() 'After exiting the context the time is back to 10' """ _infinitely_iterable = True forever = Infinity() label= String(default='Time', doc=""" The label given to the Time object. Can be used to convey more specific notions of time as appropriate. For instance, the label could be 'Simulation Time' or 'Duration'.""") time_type = Parameter(default=int, constant=True, doc=""" Callable that Time will use to convert user-specified time values into the current time; all times will be of the resulting numeric type. By default, time is of integer type, but you can supply any arbitrary-precision type like a fixed-point decimal or a rational, to allow fractional times. Floating-point times are also allowed, but are not recommended because they will suffer from accumulated rounding errors. For instance, incrementing a floating-point value 0.0 by 0.05, 20 times, will not reach 1.0 exactly. Instead, it will be slightly higher than 1.0, because 0.05 cannot be represented exactly in a standard floating point numeric type. Fixed-point or rational types should be able to handle such computations exactly, avoiding accumulation issues over long time intervals. Some potentially useful exact number classes: - int: Suitable if all times can be expressed as integers. - Python's decimal.Decimal and fractions.Fraction classes: widely available but slow and also awkward to specify times (e.g. cannot simply type 0.05, but have to use a special constructor or a string). - fixedpoint.FixedPoint: Allows a natural representation of times in decimal notation, but very slow and needs to be installed separately. - gmpy.mpq: Allows a natural representation of times in decimal notation, and very fast because it uses the GNU Multi-Precision library, but needs to be installed separately and depends on a non-Python library. gmpy.mpq is gmpy's rational type. """) timestep = Parameter(default=1.0,doc=""" Stepsize to be used with the iterator interface. Time can be advanced or decremented by any value, not just those corresponding to the stepsize, and so this value is only a default.""") until = Parameter(default=forever,doc=""" Declaration of an expected end to time values, if any. When using the iterator interface, iteration will end before this value is exceeded.""") unit = String(default=None, doc=""" The units of the time dimensions. The default of None is set as the global time function may on an arbitrary time base. Typical values for the parameter are 'seconds' (the SI unit for time) or subdivisions thereof (e.g. 'milliseconds').""") def __init__(self, **params): super(Time, self).__init__(**params) self._time = self.time_type(0) self._exhausted = None self._pushed_state = [] def __eq__(self, other): if not isinstance(other, Time): return False self_params = (self.timestep,self.until) other_params = (other.timestep,other.until) if self_params != other_params: return False return True def __ne__(self, other): return not (self == other) def __iter__(self): return self def __next__(self): timestep = self.time_type(self.timestep) if self._exhausted is None: self._exhausted = False elif (self._time + timestep) <= self.until: self._time += timestep else: self._exhausted = None raise StopIteration return self._time # PARAM2_DEPRECATION: For Python 2 compatibility; can be removed for Python 3. next = __next__ def __call__(self, val=None, time_type=None): """ When called with no arguments, returns the current time value. When called with a specified val, sets the time to it. When called with a specified time_type, changes the time_type and sets the current time to the given val (which *must* be specified) converted to that time type. To ensure that the current state remains consistent, this is normally the only way to change the time_type of an existing Time instance. """ if time_type and val is None: raise Exception("Please specify a value for the new time_type.") if time_type: type_param = self.param.objects('existing').get('time_type') type_param.constant = False self.time_type = time_type type_param.constant = True if val is not None: self._time = self.time_type(val) return self._time def advance(self, val): self += val def __iadd__(self, other): self._time = self._time + self.time_type(other) return self def __isub__(self, other): self._time = self._time - self.time_type(other) return self def __enter__(self): """Enter the context and push the current state.""" self._pushed_state.append((self._time, self.timestep, self.until)) self.in_context = True return self def __exit__(self, exc, *args): """ Exit from the current context, restoring the previous state. The StopIteration exception raised in context will force the context to exit. Any other exception exc that is raised in the block will not be caught. """ (self._time, self.timestep, self.until) = self._pushed_state.pop() self.in_context = len(self._pushed_state) != 0 if exc is StopIteration: return True class Dynamic(Parameter): """ Parameter whose value can be generated dynamically by a callable object. If a Parameter is declared as Dynamic, it can be set a callable object (such as a function or callable class), and getting the parameter's value will call that callable. Note that at present, the callable object must allow attributes to be set on itself. [Python 2.4 limitation: the callable object must be an instance of a callable class, rather than a named function or a lambda function, otherwise the object will not be picklable or deepcopyable.] If set as time_dependent, setting the Dynamic.time_fn allows the production of dynamic values to be controlled: a new value will be produced only if the current value of time_fn is different from what it was the last time the parameter value was requested. By default, the Dynamic parameters are not time_dependent so that new values are generated on every call regardless of the time. The default time_fn used when time_dependent is a single Time instance that allows general manipulations of time. It may be set to some other callable as required so long as a number is returned on each call. """ time_fn = Time() time_dependent = False def __init__(self,**params): """ Call the superclass's __init__ and set instantiate=True if the default is dynamic. """ super(Dynamic,self).__init__(**params) if callable(self.default): self._set_instantiate(True) self._initialize_generator(self.default) def _initialize_generator(self,gen,obj=None): """ Add 'last time' and 'last value' attributes to the generator. """ # Could use a dictionary to hold these things. if hasattr(obj,"_Dynamic_time_fn"): gen._Dynamic_time_fn = obj._Dynamic_time_fn gen._Dynamic_last = None # Would have usede None for this, but can't compare a fixedpoint # number with None (e.g. 1>None but FixedPoint(1)>None can't be done) gen._Dynamic_time = -1 gen._saved_Dynamic_last = [] gen._saved_Dynamic_time = [] def __get__(self,obj,objtype): """ Call the superclass's __get__; if the result is not dynamic return that result, otherwise ask that result to produce a value and return it. """ gen = super(Dynamic,self).__get__(obj,objtype) if not hasattr(gen,'_Dynamic_last'): return gen else: return self._produce_value(gen) @instance_descriptor def __set__(self,obj,val): """ Call the superclass's set and keep this parameter's instantiate value up to date (dynamic parameters must be instantiated). If val is dynamic, initialize it as a generator. """ super(Dynamic,self).__set__(obj,val) dynamic = callable(val) if dynamic: self._initialize_generator(val,obj) if obj is None: self._set_instantiate(dynamic) def _produce_value(self,gen,force=False): """ Return a value from gen. If there is no time_fn, then a new value will be returned (i.e. gen will be asked to produce a new value). If force is True, or the value of time_fn() is different from what it was was last time produce_value was called, a new value will be produced and returned. Otherwise, the last value gen produced will be returned. """ if hasattr(gen,"_Dynamic_time_fn"): time_fn = gen._Dynamic_time_fn else: time_fn = self.time_fn if (time_fn is None) or (not self.time_dependent): value = produce_value(gen) gen._Dynamic_last = value else: time = time_fn() if force or time!=gen._Dynamic_time: value = produce_value(gen) gen._Dynamic_last = value gen._Dynamic_time = time else: value = gen._Dynamic_last return value def _value_is_dynamic(self,obj,objtype=None): """ Return True if the parameter is actually dynamic (i.e. the value is being generated). """ return hasattr(super(Dynamic,self).__get__(obj,objtype),'_Dynamic_last') def _inspect(self,obj,objtype=None): """Return the last generated value for this parameter.""" gen=super(Dynamic,self).__get__(obj,objtype) if hasattr(gen,'_Dynamic_last'): return gen._Dynamic_last else: return gen def _force(self,obj,objtype=None): """Force a new value to be generated, and return it.""" gen=super(Dynamic,self).__get__(obj,objtype) if hasattr(gen,'_Dynamic_last'): return self._produce_value(gen,force=True) else: return gen import numbers def _is_number(obj): if isinstance(obj, numbers.Number): return True # The extra check is for classes that behave like numbers, such as those # found in numpy, gmpy, etc. elif (hasattr(obj, '__int__') and hasattr(obj, '__add__')): return True # This is for older versions of gmpy elif hasattr(obj, 'qdiv'): return True else: return False def identity_hook(obj,val): return val def get_soft_bounds(bounds, softbounds): """ For each soft bound (upper and lower), if there is a defined bound (not equal to None) and does not exceed the hard bound, then it is returned. Otherwise it defaults to the hard bound. The hard bound could still be None. """ if bounds is None: hl, hu = (None, None) else: hl, hu = bounds if softbounds is None: sl, su = (None, None) else: sl, su = softbounds if sl is None or (hl is not None and slhu): u = hu else: u = su return (l, u) class Number(Dynamic): """ A numeric Dynamic Parameter, with a default value and optional bounds. There are two types of bounds: ``bounds`` and ``softbounds``. ``bounds`` are hard bounds: the parameter must have a value within the specified range. The default bounds are (None,None), meaning there are actually no hard bounds. One or both bounds can be set by specifying a value (e.g. bounds=(None,10) means there is no lower bound, and an upper bound of 10). Bounds are inclusive by default, but exclusivity can be specified for each bound by setting inclusive_bounds (e.g. inclusive_bounds=(True,False) specifies an exclusive upper bound). Number is also a type of Dynamic parameter, so its value can be set to a callable to get a dynamically generated number (see Dynamic). When not being dynamically generated, bounds are checked when a Number is created or set. Using a default value outside the hard bounds, or one that is not numeric, results in an exception. When being dynamically generated, bounds are checked when the value of a Number is requested. A generated value that is not numeric, or is outside the hard bounds, results in an exception. As a special case, if allow_None=True (which is true by default if the parameter has a default of None when declared) then a value of None is also allowed. A separate function set_in_bounds() is provided that will silently crop the given value into the legal range, for use in, for instance, a GUI. ``softbounds`` are present to indicate the typical range of the parameter, but are not enforced. Setting the soft bounds allows, for instance, a GUI to know what values to display on sliders for the Number. Example of creating a Number:: AB = Number(default=0.5, bounds=(None,10), softbounds=(0,1), doc='Distance from A to B.') """ __slots__ = ['bounds', 'softbounds', 'inclusive_bounds', 'set_hook', 'step'] def __init__(self, default=0.0, bounds=None, softbounds=None, inclusive_bounds=(True,True), step=None, **params): """ Initialize this parameter object and store the bounds. Non-dynamic default values are checked against the bounds. """ super(Number,self).__init__(default=default, **params) self.set_hook = identity_hook self.bounds = bounds self.inclusive_bounds = inclusive_bounds self.softbounds = softbounds self.step = step self._validate(default) def __get__(self, obj, objtype): """ Same as the superclass's __get__, but if the value was dynamically generated, check the bounds. """ result = super(Number, self).__get__(obj, objtype) # Should be able to optimize this commonly used method by # avoiding extra lookups (e.g. _value_is_dynamic() is also # looking up 'result' - should just pass it in). if self._value_is_dynamic(obj, objtype): self._validate(result) return result def set_in_bounds(self,obj,val): """ Set to the given value, but cropped to be within the legal bounds. All objects are accepted, and no exceptions will be raised. See crop_to_bounds for details on how cropping is done. """ if not callable(val): bounded_val = self.crop_to_bounds(val) else: bounded_val = val super(Number, self).__set__(obj, bounded_val) def crop_to_bounds(self, val): """ Return the given value cropped to be within the hard bounds for this parameter. If a numeric value is passed in, check it is within the hard bounds. If it is larger than the high bound, return the high bound. If it's smaller, return the low bound. In either case, the returned value could be None. If a non-numeric value is passed in, set to be the default value (which could be None). In no case is an exception raised; all values are accepted. As documented in https://github.com/holoviz/param/issues/80, currently does not respect exclusive bounds, which would strictly require setting to one less for integer values or an epsilon less for floats. """ # Values outside the bounds are silently cropped to # be inside the bounds. if _is_number(val): if self.bounds is None: return val vmin, vmax = self.bounds if vmin is not None: if val < vmin: return vmin if vmax is not None: if val > vmax: return vmax elif self.allow_None and val is None: return val else: # non-numeric value sent in: reverts to default value return self.default return val def _validate_bounds(self, val, bounds, inclusive_bounds): if bounds is None or (val is None and self.allow_None) or callable(val): return vmin, vmax = bounds incmin, incmax = inclusive_bounds if vmax is not None: if incmax is True: if not val <= vmax: raise ValueError("Parameter %r must be at most %s, " "not %s." % (self.name, vmax, val)) else: if not val < vmax: raise ValueError("Parameter %r must be less than %s, " "not %s." % (self.name, vmax, val)) if vmin is not None: if incmin is True: if not val >= vmin: raise ValueError("Parameter %r must be at least %s, " "not %s." % (self.name, vmin, val)) else: if not val > vmin: raise ValueError("Parameter %r must be greater than %s, " "not %s." % (self.name, vmin, val)) def _validate_value(self, val, allow_None): if (allow_None and val is None) or callable(val): return if not _is_number(val): raise ValueError("Parameter %r only takes numeric values, " "not type %r." % (self.name, type(val))) def _validate_step(self, val, step): if step is not None and not _is_number(step): raise ValueError("Step can only be None or a " "numeric value, not type %r." % type(step)) def _validate(self, val): """ Checks that the value is numeric and that it is within the hard bounds; if not, an exception is raised. """ self._validate_value(val, self.allow_None) self._validate_step(val, self.step) self._validate_bounds(val, self.bounds, self.inclusive_bounds) def get_soft_bounds(self): return get_soft_bounds(self.bounds, self.softbounds) def __setstate__(self,state): if 'step' not in state: state['step'] = None super(Number, self).__setstate__(state) class Integer(Number): """Numeric Parameter required to be an Integer""" def __init__(self, default=0, **params): Number.__init__(self, default=default, **params) def _validate_value(self, val, allow_None): if callable(val): return if allow_None and val is None: return if not isinstance(val, int): raise ValueError("Integer parameter %r must be an integer, " "not type %r." % (self.name, type(val))) def _validate_step(self, val, step): if step is not None and not isinstance(step, int): raise ValueError("Step can only be None or an " "integer value, not type %r" % type(step)) class Magnitude(Number): """Numeric Parameter required to be in the range [0.0-1.0].""" def __init__(self, default=1.0, softbounds=None, **params): Number.__init__(self, default=default, bounds=(0.0,1.0), softbounds=softbounds, **params) class Boolean(Parameter): """Binary or tristate Boolean Parameter.""" __slots__ = ['bounds'] # Bounds are set for consistency and are arguably accurate, but have # no effect since values are either False, True, or None (if allowed). def __init__(self, default=False, bounds=(0,1), **params): self.bounds = bounds super(Boolean, self).__init__(default=default, **params) def _validate_value(self, val, allow_None): if allow_None: if not isinstance(val, bool) and val is not None: raise ValueError("Boolean parameter %r only takes a " "Boolean value or None, not %s." % (self.name, val)) elif not isinstance(val, bool): raise ValueError("Boolean parameter %r must be True or False, " "not %s." % (self.name, val)) class Tuple(Parameter): """A tuple Parameter (e.g. ('a',7.6,[3,5])) with a fixed tuple length.""" __slots__ = ['length'] def __init__(self, default=(0,0), length=None, **params): """ Initialize a tuple parameter with a fixed length (number of elements). The length is determined by the initial default value, if any, and must be supplied explicitly otherwise. The length is not allowed to change after instantiation. """ super(Tuple,self).__init__(default=default, **params) if length is None and default is not None: self.length = len(default) elif length is None and default is None: raise ValueError("%s: length must be specified if no default is supplied." % (self.name)) else: self.length = length self._validate(default) def _validate_value(self, val, allow_None): if val is None and allow_None: return if not isinstance(val, tuple): raise ValueError("Tuple parameter %r only takes a tuple value, " "not %r." % (self.name, type(val))) def _validate_length(self, val, length): if val is None and self.allow_None: return if not len(val) == length: raise ValueError("Tuple parameter %r is not of the correct " "length (%d instead of %d)." % (self.name, len(val), length)) def _validate(self, val): self._validate_value(val, self.allow_None) self._validate_length(val, self.length) @classmethod def serialize(cls, value): if value is None: return 'null' return list(value) # As JSON has no tuple representation @classmethod def deserialize(cls, value): if value == 'null': return None return tuple(value) # As JSON has no tuple representation class NumericTuple(Tuple): """A numeric tuple Parameter (e.g. (4.5,7.6,3)) with a fixed tuple length.""" def _validate_value(self, val, allow_None): super(NumericTuple, self)._validate_value(val, allow_None) if allow_None and val is None: return for n in val: if _is_number(n): continue raise ValueError("NumericTuple parameter %r only takes numeric " "values, not type %r." % (self.name, type(n))) class XYCoordinates(NumericTuple): """A NumericTuple for an X,Y coordinate.""" def __init__(self, default=(0.0, 0.0), **params): super(XYCoordinates,self).__init__(default=default, length=2, **params) class Callable(Parameter): """ Parameter holding a value that is a callable object, such as a function. A keyword argument instantiate=True should be provided when a function object is used that might have state. On the other hand, regular standalone functions cannot be deepcopied as of Python 2.4, so instantiate must be False for those values. """ def _validate_value(self, val, allow_None): if (allow_None and val is None) or callable(val): return raise ValueError("Callable parameter %r only takes a callable object, " "not objects of type %r." % (self.name, type(val))) class Action(Callable): """ A user-provided function that can be invoked like a class or object method using (). In a GUI, this might be mapped to a button, but it can be invoked directly as well. """ # Currently same implementation as Callable, but kept separate to allow different handling in GUIs def _is_abstract(class_): try: return class_.abstract except AttributeError: return False # Could be a method of ClassSelector. def concrete_descendents(parentclass): """ Return a dictionary containing all subclasses of the specified parentclass, including the parentclass. Only classes that are defined in scripts that have been run or modules that have been imported are included, so the caller will usually first do ``from package import *``. Only non-abstract classes will be included. """ return dict((c.__name__,c) for c in descendents(parentclass) if not _is_abstract(c)) class Composite(Parameter): """ A Parameter that is a composite of a set of other attributes of the class. The constructor argument 'attribs' takes a list of attribute names, which may or may not be Parameters. Getting the parameter returns a list of the values of the constituents of the composite, in the order specified. Likewise, setting the parameter takes a sequence of values and sets the value of the constituent attributes. This Parameter type has not been tested with watchers and dependencies, and may not support them properly. """ __slots__ = ['attribs', 'objtype'] def __init__(self, attribs=None, **kw): if attribs is None: attribs = [] super(Composite, self).__init__(default=None, **kw) self.attribs = attribs def __get__(self, obj, objtype): """ Return the values of all the attribs, as a list. """ if obj is None: return [getattr(objtype, a) for a in self.attribs] else: return [getattr(obj, a) for a in self.attribs] def _validate_attribs(self, val, attribs): if len(val) == len(attribs): return raise ValueError("Compound parameter %r got the wrong number " "of values (needed %d, but got %d)." % (self.name, len(attribs), len(val))) def _validate(self, val): self._validate_attribs(val, self.attribs) def _post_setter(self, obj, val): if obj is None: for a, v in zip(self.attribs, val): setattr(self.objtype, a, v) else: for a, v in zip(self.attribs, val): setattr(obj, a, v) class SelectorBase(Parameter): """ Parameter whose value must be chosen from a list of possibilities. Subclasses must implement get_range(). """ __abstract = True def get_range(self): raise NotImplementedError("get_range() must be implemented in subclasses.") class Selector(SelectorBase): """ Parameter whose value must be one object from a list of possible objects. By default, if no default is specified, picks the first object from the provided set of objects, as long as the objects are in an ordered data collection. check_on_set restricts the value to be among the current list of objects. By default, if objects are initially supplied, check_on_set is True, whereas if no objects are initially supplied, check_on_set is False. This can be overridden by explicitly specifying check_on_set initially. If check_on_set is True (either because objects are supplied initially, or because it is explicitly specified), the default (initial) value must be among the list of objects (unless the default value is None). The list of objects can be supplied as a list (appropriate for selecting among a set of strings, or among a set of objects with a "name" parameter), or as a (preferably ordered) dictionary from names to objects. If a dictionary is supplied, the objects will need to be hashable so that their names can be looked up from the object value. """ __slots__ = ['objects', 'compute_default_fn', 'check_on_set', 'names'] # Selector is usually used to allow selection from a list of # existing objects, therefore instantiate is False by default. def __init__(self, objects=None, default=None, instantiate=False, compute_default_fn=None, check_on_set=None, allow_None=None, empty_default=False, **params): autodefault = None if objects: if is_ordered_dict(objects): autodefault = list(objects.values())[0] elif isinstance(objects, dict): main.param.warning("Parameter default value is arbitrary due to " "dictionaries prior to Python 3.6 not being " "ordered; should use an ordered dict or " "supply an explicit default value.") autodefault = list(objects.values())[0] elif isinstance(objects, list): autodefault = objects[0] default = autodefault if (not empty_default and default is None) else default if objects is None: objects = [] if isinstance(objects, collections_abc.Mapping): self.names = objects self.objects = list(objects.values()) else: self.names = None self.objects = objects self.compute_default_fn = compute_default_fn if check_on_set is not None: self.check_on_set = check_on_set elif len(objects) == 0: self.check_on_set = False else: self.check_on_set = True super(Selector,self).__init__( default=default, instantiate=instantiate, **params) # Required as Parameter sets allow_None=True if default is None self.allow_None = allow_None if default is not None and self.check_on_set is True: self._validate(default) # Note that if the list of objects is changed, the current value for # this parameter in existing POs could be outside of the new range. def compute_default(self): """ If this parameter's compute_default_fn is callable, call it and store the result in self.default. Also removes None from the list of objects (if the default is no longer None). """ if self.default is None and callable(self.compute_default_fn): self.default = self.compute_default_fn() if self.default not in self.objects: self.objects.append(self.default) def _validate(self, val): """ val must be None or one of the objects in self.objects. """ if not self.check_on_set: self._ensure_value_is_in_objects(val) return if not (val in self.objects or (self.allow_None and val is None)): # This method can be called before __init__ has called # super's __init__, so there may not be any name set yet. if (hasattr(self, "name") and self.name): attrib_name = " " + self.name else: attrib_name = "" items = [] limiter = ']' length = 0 for item in self.objects: string = str(item) length += len(string) if length < 200: items.append(string) else: limiter = ', ...]' break items = '[' + ', '.join(items) + limiter raise ValueError("%s not in parameter%s's list of possible objects, " "valid options include %s" % (val, attrib_name, items)) def _ensure_value_is_in_objects(self,val): """ Make sure that the provided value is present on the objects list. Subclasses can override if they support multiple items on a list, to check each item instead. """ if not (val in self.objects): self.objects.append(val) def get_range(self): """ Return the possible objects to which this parameter could be set. (Returns the dictionary {object.name:object}.) """ return named_objs(self.objects, self.names) class ObjectSelector(Selector): """ Deprecated. Same as Selector, but with a different constructor for historical reasons. """ def __init__(self, default=None, objects=None, **kwargs): super(ObjectSelector,self).__init__(objects=objects, default=default, empty_default=True, **kwargs) class ClassSelector(SelectorBase): """ Parameter allowing selection of either a subclass or an instance of a given set of classes. By default, requires an instance, but if is_instance=False, accepts a class instead. Both class and instance values respect the instantiate slot, though it matters only for is_instance=True. """ __slots__ = ['class_', 'is_instance'] def __init__(self,class_,default=None,instantiate=True,is_instance=True,**params): self.class_ = class_ self.is_instance = is_instance super(ClassSelector,self).__init__(default=default,instantiate=instantiate,**params) self._validate(default) def _validate(self, val): super(ClassSelector, self)._validate(val) self._validate_class_(val, self.class_, self.is_instance) def _validate_class_(self, val, class_, is_instance): if (val is None and self.allow_None): return if isinstance(class_, tuple): class_name = ('(%s)' % ', '.join(cl.__name__ for cl in class_)) else: class_name = class_.__name__ param_cls = self.__class__.__name__ if is_instance: if not (isinstance(val, class_)): raise ValueError( "%s parameter %r value must be an instance of %s, not %r." % (param_cls, self.name, class_name, val)) else: if not (issubclass(val, class_)): raise ValueError( "%s parameter %r must be a subclass of %s, not %r." % (param_cls, self.name, class_name, val.__name__)) def get_range(self): """ Return the possible types for this parameter's value. (I.e. return `{name: }` for all classes that are concrete_descendents() of `self.class_`.) Only classes from modules that have been imported are added (see concrete_descendents()). """ classes = self.class_ if isinstance(self.class_, tuple) else (self.class_,) all_classes = {} for cls in classes: all_classes.update(concrete_descendents(cls)) d = OrderedDict((name, class_) for name,class_ in all_classes.items()) if self.allow_None: d['None'] = None return d class List(Parameter): """ Parameter whose value is a list of objects, usually of a specified type. The bounds allow a minimum and/or maximum length of list to be enforced. If the item_type is non-None, all items in the list are checked to be of that type. `class_` is accepted as an alias for `item_type`, but is deprecated due to conflict with how the `class_` slot is used in Selector classes. """ __slots__ = ['bounds', 'item_type', 'class_'] def __init__(self, default=[], class_=None, item_type=None, instantiate=True, bounds=(0, None), **params): self.item_type = item_type or class_ self.class_ = self.item_type self.bounds = bounds Parameter.__init__(self, default=default, instantiate=instantiate, **params) self._validate(default) def _validate(self, val): """ Checks that the value is numeric and that it is within the hard bounds; if not, an exception is raised. """ self._validate_value(val, self.allow_None) self._validate_bounds(val, self.bounds) self._validate_item_type(val, self.item_type) def _validate_bounds(self, val, bounds): "Checks that the list is of the right length and has the right contents." if bounds is None or (val is None and self.allow_None): return min_length, max_length = bounds l = len(val) if min_length is not None and max_length is not None: if not (min_length <= l <= max_length): raise ValueError("%s: list length must be between %s and %s (inclusive)"%(self.name,min_length,max_length)) elif min_length is not None: if not min_length <= l: raise ValueError("%s: list length must be at least %s." % (self.name, min_length)) elif max_length is not None: if not l <= max_length: raise ValueError("%s: list length must be at most %s." % (self.name, max_length)) def _validate_value(self, val, allow_None): if allow_None and val is None: return if not isinstance(val, list): raise ValueError("List parameter %r must be a list, not an object of type %s." % (self.name, type(val))) def _validate_item_type(self, val, item_type): if item_type is None or (self.allow_None and val is None): return for v in val: if isinstance(v, item_type): continue raise TypeError("List parameter %r items must be instances " "of type %r, not %r." % (self.name, item_type, val)) class HookList(List): """ Parameter whose value is a list of callable objects. This type of List Parameter is typically used to provide a place for users to register a set of commands to be called at a specified place in some sequence of processing steps. """ __slots__ = ['class_', 'bounds'] def _validate_value(self, val, allow_None): super(HookList, self)._validate_value(val, allow_None) if allow_None and val is None: return for v in val: if callable(v): continue raise ValueError("HookList parameter %r items must be callable, " "not %r." % (self.name, v)) class Dict(ClassSelector): """ Parameter whose value is a dictionary. """ def __init__(self, default=None, **params): super(Dict, self).__init__(dict, default=default, **params) class Array(ClassSelector): """ Parameter whose value is a numpy array. """ def __init__(self, default=None, **params): from numpy import ndarray super(Array, self).__init__(ndarray, allow_None=True, default=default, **params) @classmethod def serialize(cls, value): if value is None: return 'null' return value.tolist() @classmethod def deserialize(cls, value): if value == 'null': return None from numpy import asarray return asarray(value) class DataFrame(ClassSelector): """ Parameter whose value is a pandas DataFrame. The structure of the DataFrame can be constrained by the rows and columns arguments: rows: If specified, may be a number or an integer bounds tuple to constrain the allowable number of rows. columns: If specified, may be a number, an integer bounds tuple, a list or a set. If the argument is numeric, constrains the number of columns using the same semantics as used for rows. If either a list or set of strings, the column names will be validated. If a set is used, the supplied DataFrame must contain the specified columns and if a list is given, the supplied DataFrame must contain exactly the same columns and in the same order and no other columns. """ __slots__ = ['rows', 'columns', 'ordered'] def __init__(self, default=None, rows=None, columns=None, ordered=None, **params): from pandas import DataFrame as pdDFrame self.rows = rows self.columns = columns self.ordered = ordered super(DataFrame,self).__init__(pdDFrame, default=default, **params) self._validate(self.default) def _length_bounds_check(self, bounds, length, name): message = '{name} length {length} does not match declared bounds of {bounds}' if not isinstance(bounds, tuple): if (bounds != length): raise ValueError(message.format(name=name, length=length, bounds=bounds)) else: return (lower, upper) = bounds failure = ((lower is not None and (length < lower)) or (upper is not None and length > upper)) if failure: raise ValueError(message.format(name=name,length=length, bounds=bounds)) def _validate(self, val): super(DataFrame, self)._validate(val) if isinstance(self.columns, set) and self.ordered is True: raise ValueError('Columns cannot be ordered when specified as a set') if self.allow_None and val is None: return if self.columns is None: pass elif (isinstance(self.columns, tuple) and len(self.columns)==2 and all(isinstance(v, (type(None), numbers.Number)) for v in self.columns)): # Numeric bounds tuple self._length_bounds_check(self.columns, len(val.columns), 'Columns') elif isinstance(self.columns, (list, set)): self.ordered = isinstance(self.columns, list) if self.ordered is None else self.ordered difference = set(self.columns) - set([str(el) for el in val.columns]) if difference: msg = 'Provided DataFrame columns {found} does not contain required columns {expected}' raise ValueError(msg.format(found=list(val.columns), expected=sorted(self.columns))) else: self._length_bounds_check(self.columns, len(val.columns), 'Column') if self.ordered: if list(val.columns) != list(self.columns): msg = 'Provided DataFrame columns {found} must exactly match {expected}' raise ValueError(msg.format(found=list(val.columns), expected=self.columns)) if self.rows is not None: self._length_bounds_check(self.rows, len(val), 'Row') @classmethod def serialize(cls, value): if value is None: return 'null' return value.to_dict('records') @classmethod def deserialize(cls, value): if value == 'null': return None from pandas import DataFrame as pdDFrame return pdDFrame(value) class Series(ClassSelector): """ Parameter whose value is a pandas Series. The structure of the Series can be constrained by the rows argument which may be a number or an integer bounds tuple to constrain the allowable number of rows. """ __slots__ = ['rows'] def __init__(self, default=None, rows=None, allow_None=False, **params): from pandas import Series as pdSeries self.rows = rows super(Series,self).__init__(pdSeries, default=default, allow_None=allow_None, **params) self._validate(self.default) def _length_bounds_check(self, bounds, length, name): message = '{name} length {length} does not match declared bounds of {bounds}' if not isinstance(bounds, tuple): if (bounds != length): raise ValueError(message.format(name=name, length=length, bounds=bounds)) else: return (lower, upper) = bounds failure = ((lower is not None and (length < lower)) or (upper is not None and length > upper)) if failure: raise ValueError(message.format(name=name,length=length, bounds=bounds)) def _validate(self, val): super(Series, self)._validate(val) if self.allow_None and val is None: return if self.rows is not None: self._length_bounds_check(self.rows, len(val), 'Row') # For portable code: # - specify paths in unix (rather than Windows) style; # - use resolve_path(path_to_file=True) for paths to existing files to be read, # - use resolve_path(path_to_file=False) for paths to existing folders to be read, # and normalize_path() for paths to new files to be written. class resolve_path(ParameterizedFunction): """ Find the path to an existing file, searching the paths specified in the search_paths parameter if the filename is not absolute, and converting a UNIX-style path to the current OS's format if necessary. To turn a supplied relative path into an absolute one, the path is appended to paths in the search_paths parameter, in order, until the file is found. An IOError is raised if the file is not found. Similar to Python's os.path.abspath(), except more search paths than just os.getcwd() can be used, and the file must exist. """ search_paths = List(default=[os.getcwd()], pickle_default_value=False, doc=""" Prepended to a non-relative path, in order, until a file is found.""") path_to_file = Boolean(default=True, pickle_default_value=False, allow_None=True, doc=""" String specifying whether the path refers to a 'File' or a 'Folder'. If None, the path may point to *either* a 'File' *or* a 'Folder'.""") def __call__(self, path, **params): p = ParamOverrides(self, params) path = os.path.normpath(path) ftype = "File" if p.path_to_file is True \ else "Folder" if p.path_to_file is False else "Path" if not p.search_paths: p.search_paths = [os.getcwd()] if os.path.isabs(path): if ((p.path_to_file is None and os.path.exists(path)) or (p.path_to_file is True and os.path.isfile(path)) or (p.path_to_file is False and os.path.isdir( path))): return path raise IOError("%s '%s' not found." % (ftype,path)) else: paths_tried = [] for prefix in p.search_paths: try_path = os.path.join(os.path.normpath(prefix), path) if ((p.path_to_file is None and os.path.exists(try_path)) or (p.path_to_file is True and os.path.isfile(try_path)) or (p.path_to_file is False and os.path.isdir( try_path))): return try_path paths_tried.append(try_path) raise IOError(ftype + " " + os.path.split(path)[1] + " was not found in the following place(s): " + str(paths_tried) + ".") class normalize_path(ParameterizedFunction): """ Convert a UNIX-style path to the current OS's format, typically for creating a new file or directory. If the path is not already absolute, it will be made absolute (using the prefix parameter). Should do the same as Python's os.path.abspath(), except using prefix rather than os.getcwd). """ prefix = String(default=os.getcwd(),pickle_default_value=False,doc=""" Prepended to the specified path, if that path is not absolute.""") def __call__(self,path="",**params): p = ParamOverrides(self,params) if not os.path.isabs(path): path = os.path.join(os.path.normpath(p.prefix),path) return os.path.normpath(path) class Path(Parameter): """ Parameter that can be set to a string specifying the path of a file or folder. The string should be specified in UNIX style, but it will be returned in the format of the user's operating system. Please use the Filename or Foldername classes if you require discrimination between the two possibilities. The specified path can be absolute, or relative to either: * any of the paths specified in the search_paths attribute (if search_paths is not None); or * any of the paths searched by resolve_path() (if search_paths is None). """ __slots__ = ['search_paths'] def __init__(self, default=None, search_paths=None, **params): if search_paths is None: search_paths = [] self.search_paths = search_paths super(Path,self).__init__(default,**params) def _resolve(self, path): return resolve_path(path, path_to_file=None, search_paths=self.search_paths) def _validate(self, val): if val is None: if not self.allow_None: Parameterized(name="%s.%s"%(self.owner.name,self.name)).param.warning('None is not allowed') else: try: self._resolve(val) except IOError as e: Parameterized(name="%s.%s"%(self.owner.name,self.name)).param.warning('%s',e.args[0]) def __get__(self, obj, objtype): """ Return an absolute, normalized path (see resolve_path). """ raw_path = super(Path,self).__get__(obj,objtype) return None if raw_path is None else self._resolve(raw_path) def __getstate__(self): # don't want to pickle the search_paths state = super(Path,self).__getstate__() if 'search_paths' in state: state['search_paths'] = [] return state class Filename(Path): """ Parameter that can be set to a string specifying the path of a file. The string should be specified in UNIX style, but it will be returned in the format of the user's operating system. The specified path can be absolute, or relative to either: * any of the paths specified in the search_paths attribute (if search_paths is not None); or * any of the paths searched by resolve_path() (if search_paths is None). """ def _resolve(self, path): return resolve_path(path, path_to_file=True, search_paths=self.search_paths) class Foldername(Path): """ Parameter that can be set to a string specifying the path of a folder. The string should be specified in UNIX style, but it will be returned in the format of the user's operating system. The specified path can be absolute, or relative to either: * any of the paths specified in the search_paths attribute (if search_paths is not None); or * any of the paths searched by resolve_dir_path() (if search_paths is None). """ def _resolve(self, path): return resolve_path(path, path_to_file=False, search_paths=self.search_paths) def abbreviate_paths(pathspec,named_paths): """ Given a dict of (pathname,path) pairs, removes any prefix shared by all pathnames. Helps keep menu items short yet unambiguous. """ from os.path import commonprefix, dirname, sep prefix = commonprefix([dirname(name)+sep for name in named_paths.keys()]+[pathspec]) return OrderedDict([(name[len(prefix):],path) for name,path in named_paths.items()]) class FileSelector(Selector): """ Given a path glob, allows one file to be selected from those matching. """ __slots__ = ['path'] def __init__(self, default=None, path="", **kwargs): self.default = default self.path = path self.update() super(FileSelector, self).__init__(default=default, objects=self.objects, empty_default=True, **kwargs) def _on_set(self, attribute, old, new): super(FileSelector, self)._on_set(attribute, new, old) if attribute == 'path': self.update() def update(self): self.objects = sorted(glob.glob(self.path)) if self.default in self.objects: return self.default = self.objects[0] if self.objects else None def get_range(self): return abbreviate_paths(self.path,super(FileSelector, self).get_range()) class ListSelector(Selector): """ Variant of Selector where the value can be multiple objects from a list of possible objects. """ def __init__(self, default=None, objects=None, **kwargs): super(ListSelector,self).__init__( objects=objects, default=default, empty_default=True, **kwargs) def compute_default(self): if self.default is None and callable(self.compute_default_fn): self.default = self.compute_default_fn() for o in self.default: if o not in self.objects: self.objects.append(o) def _validate(self, val): if (val is None and self.allow_None): return for o in val: super(ListSelector, self)._validate(o) class MultiFileSelector(ListSelector): """ Given a path glob, allows multiple files to be selected from the list of matches. """ __slots__ = ['path'] def __init__(self, default=None, path="", **kwargs): self.default = default self.path = path self.update() super(MultiFileSelector, self).__init__(default=default, objects=self.objects, **kwargs) def _on_set(self, attribute, old, new): super(MultiFileSelector, self)._on_set(attribute, new, old) if attribute == 'path': self.update() def update(self): self.objects = sorted(glob.glob(self.path)) if self.default and all([o in self.objects for o in self.default]): return self.default = self.objects def get_range(self): return abbreviate_paths(self.path,super(MultiFileSelector, self).get_range()) class Date(Number): """ Date parameter of datetime or date type. """ def __init__(self, default=None, **kwargs): super(Date, self).__init__(default=default, **kwargs) def _validate_value(self, val, allow_None): """ Checks that the value is numeric and that it is within the hard bounds; if not, an exception is raised. """ if self.allow_None and val is None: return if not isinstance(val, dt_types) and not (allow_None and val is None): raise ValueError( "Date parameter %r only takes datetime and date types, " "not type %r." % (self.name, type(val)) ) def _validate_step(self, val, step): if step is not None and not isinstance(step, dt_types): raise ValueError( "Step can only be None, a datetime " "or datetime type, not type %r." % type(val) ) @classmethod def serialize(cls, value): if value is None: return 'null' if not isinstance(value, (dt.datetime, dt.date)): # i.e np.datetime64 value = value.astype(dt.datetime) return value.strftime("%Y-%m-%dT%H:%M:%S.%f") @classmethod def deserialize(cls, value): if value == 'null': return None return dt.datetime.strptime(value, "%Y-%m-%dT%H:%M:%S.%f") class CalendarDate(Number): """ Parameter specifically allowing dates (not datetimes). """ def __init__(self, default=None, **kwargs): super(CalendarDate, self).__init__(default=default, **kwargs) def _validate_value(self, val, allow_None): """ Checks that the value is numeric and that it is within the hard bounds; if not, an exception is raised. """ if self.allow_None and val is None: return if (not isinstance(val, dt.date) or isinstance(val, dt.datetime)) and not (allow_None and val is None): raise ValueError("CalendarDate parameter %r only takes date types." % self.name) def _validate_step(self, val, step): if step is not None and not isinstance(step, dt.date): raise ValueError("Step can only be None or a date type.") @classmethod def serialize(cls, value): if value is None: return 'null' return value.strftime("%Y-%m-%d") @classmethod def deserialize(cls, value): if value == 'null': return None return dt.datetime.strptime(value, "%Y-%m-%d").date() class Color(Parameter): """ Color parameter defined as a hex RGB string with an optional # prefix or (optionally) as a CSS3 color name. """ # CSS3 color specification https://www.w3.org/TR/css-color-3/#svg-color _named_colors = [ 'aliceblue', 'antiquewhite', 'aqua', 'aquamarine', 'azure', 'beige', 'bisque', 'black', 'blanchedalmond', 'blue', 'blueviolet', 'brown', 'burlywood', 'cadetblue', 'chartreuse', 'chocolate', 'coral', 'cornflowerblue', 'cornsilk', 'crimson', 'cyan', 'darkblue', 'darkcyan', 'darkgoldenrod', 'darkgray', 'darkgrey', 'darkgreen', 'darkkhaki', 'darkmagenta', 'darkolivegreen', 'darkorange', 'darkorchid', 'darkred', 'darksalmon', 'darkseagreen', 'darkslateblue', 'darkslategray', 'darkslategrey', 'darkturquoise', 'darkviolet', 'deeppink', 'deepskyblue', 'dimgray', 'dimgrey', 'dodgerblue', 'firebrick', 'floralwhite', 'forestgreen', 'fuchsia', 'gainsboro', 'ghostwhite', 'gold', 'goldenrod', 'gray', 'grey', 'green', 'greenyellow', 'honeydew', 'hotpink', 'indianred', 'indigo', 'ivory', 'khaki', 'lavender', 'lavenderblush', 'lawngreen', 'lemonchiffon', 'lightblue', 'lightcoral', 'lightcyan', 'lightgoldenrodyellow', 'lightgray', 'lightgrey', 'lightgreen', 'lightpink', 'lightsalmon', 'lightseagreen', 'lightskyblue', 'lightslategray', 'lightslategrey', 'lightsteelblue', 'lightyellow', 'lime', 'limegreen', 'linen', 'magenta', 'maroon', 'mediumaquamarine', 'mediumblue', 'mediumorchid', 'mediumpurple', 'mediumseagreen', 'mediumslateblue', 'mediumspringgreen', 'mediumturquoise', 'mediumvioletred', 'midnightblue', 'mintcream', 'mistyrose', 'moccasin', 'navajowhite', 'navy', 'oldlace', 'olive', 'olivedrab', 'orange', 'orangered', 'orchid', 'palegoldenrod', 'palegreen', 'paleturquoise', 'palevioletred', 'papayawhip', 'peachpuff', 'peru', 'pink', 'plum', 'powderblue', 'purple', 'red', 'rosybrown', 'royalblue', 'saddlebrown', 'salmon', 'sandybrown', 'seagreen', 'seashell', 'sienna', 'silver', 'skyblue', 'slateblue', 'slategray', 'slategrey', 'snow', 'springgreen', 'steelblue', 'tan', 'teal', 'thistle', 'tomato', 'turquoise', 'violet', 'wheat', 'white', 'whitesmoke', 'yellow', 'yellowgreen'] __slots__ = ['allow_named'] def __init__(self, default=None, allow_named=True, **kwargs): super(Color, self).__init__(default=default, **kwargs) self.allow_named = allow_named self._validate(default) def _validate(self, val): self._validate_value(val, self.allow_None) self._validate_allow_named(val, self.allow_named) def _validate_value(self, val, allow_None): if (allow_None and val is None): return if not isinstance(val, basestring): raise ValueError("Color parameter %r expects a string value, " "not an object of type %s." % (self.name, type(val))) def _validate_allow_named(self, val, allow_named): if (val is None and self.allow_None): return is_hex = re.match('^#?(([0-9a-fA-F]{2}){3}|([0-9a-fA-F]){3})$', val) if self.allow_named: if not is_hex and val not in self._named_colors: raise ValueError("Color '%s' only takes RGB hex codes " "or named colors, received '%s'." % (self.name, val)) elif not is_hex: raise ValueError("Color '%s' only accepts valid RGB hex " "codes, received '%s'." % (self.name, val)) class Range(NumericTuple): """ A numeric range with optional bounds and softbounds. """ __slots__ = ['bounds', 'inclusive_bounds', 'softbounds', 'step'] def __init__(self,default=None, bounds=None, softbounds=None, inclusive_bounds=(True,True), step=None, **params): self.bounds = bounds self.inclusive_bounds = inclusive_bounds self.softbounds = softbounds self.step = step super(Range,self).__init__(default=default,length=2,**params) def _validate(self, val): super(Range, self)._validate(val) self._validate_bounds(val, self.bounds, self.inclusive_bounds) def _validate_bounds(self, val, bounds, inclusive_bounds): if bounds is None or (val is None and self.allow_None): return vmin, vmax = bounds incmin, incmax = inclusive_bounds for bound, v in zip(['lower', 'upper'], val): too_low = (vmin is not None) and (v < vmin if incmin else v <= vmin) too_high = (vmax is not None) and (v > vmax if incmax else v >= vmax) if too_low or too_high: raise ValueError("Range parameter %r's %s bound must be in range %s." % (self.name, bound, self.rangestr())) def get_soft_bounds(self): return get_soft_bounds(self.bounds, self.softbounds) def rangestr(self): vmin, vmax = self.bounds incmin, incmax = self.inclusive_bounds incmin = '[' if incmin else '(' incmax = ']' if incmax else ')' return '%s%s, %s%s' % (incmin, vmin, vmax, incmax) class DateRange(Range): """ A datetime or date range specified as (start, end). Bounds must be specified as datetime or date types (see param.dt_types). """ def _validate_value(self, val, allow_None): # Cannot use super()._validate_value as DateRange inherits from # NumericTuple which check that the tuple values are numbers and # datetime objects aren't numbers. if allow_None and val is None: return if not isinstance(val, tuple): raise ValueError("DateRange parameter %r only takes a tuple value, " "not %s." % (self.name, type(val).__name__)) for n in val: if isinstance(n, dt_types): continue raise ValueError("DateRange parameter %r only takes date/datetime " "values, not type %s." % (self.name, type(n).__name__)) start, end = val if not end >= start: raise ValueError("DateRange parameter %r's end datetime %s " "is before start datetime %s." % (self.name, val[1], val[0])) @classmethod def serialize(cls, value): if value is None: return 'null' # List as JSON has no tuple representation serialized = [] for v in value: if not isinstance(v, (dt.datetime, dt.date)): # i.e np.datetime64 v = v.astype(dt.datetime) # Separate date and datetime to deserialize to the right type. if type(v) == dt.date: v = v.strftime("%Y-%m-%d") else: v = v.strftime("%Y-%m-%dT%H:%M:%S.%f") serialized.append(v) return serialized def deserialize(cls, value): if value == 'null': return None deserialized = [] for v in value: # Date if len(v) == 10: v = dt.datetime.strptime(v, "%Y-%m-%d").date() # Datetime else: v = dt.datetime.strptime(v, "%Y-%m-%dT%H:%M:%S.%f") deserialized.append(v) # As JSON has no tuple representation return tuple(deserialized) class CalendarDateRange(Range): """ A date range specified as (start_date, end_date). """ def _validate_value(self, val, allow_None): if allow_None and val is None: return for n in val: if not isinstance(n, dt.date): raise ValueError("CalendarDateRange parameter %r only " "takes date types, not %s." % (self.name, val)) start, end = val if not end >= start: raise ValueError("CalendarDateRange parameter %r's end date " "%s is before start date %s." % (self.name, val[1], val[0])) @classmethod def serialize(cls, value): if value is None: return 'null' # As JSON has no tuple representation return [v.strftime("%Y-%m-%d") for v in value] @classmethod def deserialize(cls, value): if value == 'null': return None # As JSON has no tuple representation return tuple([dt.datetime.strptime(v, "%Y-%m-%d").date() for v in value]) class Event(Boolean): """ An Event Parameter is one whose value is intimately linked to the triggering of events for watchers to consume. Event has a Boolean value, which when set to True triggers the associated watchers (as any Parameter does) and then is automatically set back to False. Conversely, if events are triggered directly via `.trigger`, the value is transiently set to True (so that it's clear which of many parameters being watched may have changed), then restored to False when the triggering completes. An Event parameter is thus like a momentary switch or pushbutton with a transient True value that serves only to launch some other action (e.g. via a param.depends decorator), rather than encapsulating the action itself as param.Action does. """ # _autotrigger_value specifies the value used to set the parameter # to when the parameter is supplied to the trigger method. This # value change is then what triggers the watcher callbacks. __slots__ = ['_autotrigger_value', '_mode', '_autotrigger_reset_value'] def __init__(self,default=False,bounds=(0,1),**params): self._autotrigger_value = True self._autotrigger_reset_value = False self._mode = 'set-reset' # Mode can be one of 'set', 'set-reset' or 'reset' # 'set' is normal Boolean parameter behavior when set with a value. # 'set-reset' temporarily sets the parameter (which triggers # watching callbacks) but immediately resets the value back to # False. # 'reset' applies the reset from True to False without # triggering watched callbacks # This _mode attribute is one of the few places where a specific # parameter has a special behavior that is relied upon by the # core functionality implemented in # parameterized.py. Specifically, the set_param method # temporarily sets this attribute in order to disable resetting # back to False while triggered callbacks are executing super(Event, self).__init__(default=default,**params) def _reset_event(self, obj, val): val = False if obj is None: self.default = val else: obj.__dict__[self._internal_name] = val self._post_setter(obj, val) @instance_descriptor def __set__(self, obj, val): if self._mode in ['set-reset', 'set']: super(Event, self).__set__(obj, val) if self._mode in ['set-reset', 'reset']: self._reset_event(obj, val) from contextlib import contextmanager @contextmanager def exceptions_summarized(): """Useful utility for writing docs that need to show expected errors. Shows exception only, concisely, without a traceback. """ try: yield except Exception: import sys etype, value, tb = sys.exc_info() print("{}: {}".format(etype.__name__,value), file=sys.stderr) param-1.12.3/param/_async.py000066400000000000000000000017441434441564000156370ustar00rootroot00000000000000""" Module that implements async/def function wrappers to be used by param internal callbacks. These are defined in a separate file due to py2 incompatibility with both `async/await` and `yield from` syntax. """ def generate_depends(func): async def _depends(*args, **kw): # noqa: E999 await func(*args, **kw) # noqa: E999 return _depends def generate_caller(function, what='value', changed=None, callback=None, skip_event=None): async def caller(*events): # noqa: E999 if callback: callback(*events) if not skip_event or not skip_event(*events, what=what, changed=changed): await function() # noqa: E999 return caller def generate_callback(func, dependencies, kw): async def cb(*events): # noqa: E999 args = (getattr(dep.owner, dep.name) for dep in dependencies) dep_kwargs = {n: getattr(dep.owner, dep.name) for n, dep in kw.items()} await func(*args, **dep_kwargs) # noqa: E999 return cb param-1.12.3/param/ipython.py000066400000000000000000000326331434441564000160560ustar00rootroot00000000000000""" Optional IPython extension for working with Parameters. This extension offers extended but completely optional functionality for IPython users. From within IPython, it may be loaded using: %load_ext param.ipython This will register the %params line magic to allow easy inspection of all the parameters defined on a parameterized class or object: %params All parameters of the class or object will be listed in the IPython pager together with all their corresponding attributes and docstrings. Note that the class or object to be inspected must already exist in the active namespace. """ __author__ = "Jean-Luc Stevens" import re import sys import itertools import textwrap import param # Whether to generate warnings when misformatted docstrings are found WARN_MISFORMATTED_DOCSTRINGS = False # ANSI color codes for the IPython pager red = '\x1b[1;31m%s\x1b[0m' blue = '\x1b[1;34m%s\x1b[0m' green = '\x1b[1;32m%s\x1b[0m' cyan = '\x1b[1;36m%s\x1b[0m' class ParamPager(object): """ Callable class that displays information about the supplied Parameterized object or class in the IPython pager. """ def __init__(self, metaclass=False): """ If metaclass is set to True, the checks for Parameterized classes objects are disabled. This option is for use in ParameterizedMetaclass for automatic docstring generation. """ # Order of the information to be listed in the table (left to right) self.order = ['name', 'changed', 'value', 'type', 'bounds', 'mode'] self.metaclass = metaclass def get_param_info(self, obj, include_super=True): """ Get the parameter dictionary, the list of modifed parameters and the dictionary of parameter values. If include_super is True, parameters are also collected from the super classes. """ params = dict(obj.param.objects('existing')) if isinstance(obj,type): changed = [] val_dict = dict((k,p.default) for (k,p) in params.items()) self_class = obj else: changed = list(obj.param.values(onlychanged=True).keys()) val_dict = obj.param.values() self_class = obj.__class__ if not include_super: params = dict((k,v) for (k,v) in params.items() if k in self_class.__dict__) params.pop('name') # Already displayed in the title. return (params, val_dict, changed) def param_docstrings(self, info, max_col_len=100, only_changed=False): """ Build a string to that presents all of the parameter docstrings in a clean format (alternating red and blue for readability). """ (params, val_dict, changed) = info contents = [] displayed_params = [] for name in self.sort_by_precedence(params): if only_changed and not (name in changed): continue displayed_params.append((name, params[name])) right_shift = max(len(name) for name, _ in displayed_params)+2 for i, (name, p) in enumerate(displayed_params): heading = "%s: " % name unindented = textwrap.dedent("< No docstring available >" if p.doc is None else p.doc) if (WARN_MISFORMATTED_DOCSTRINGS and not unindented.startswith("\n") and len(unindented.splitlines()) > 1): param.main.warning("Multi-line docstring for %r is incorrectly formatted " " (should start with newline)", name) # Strip any starting newlines while unindented.startswith("\n"): unindented = unindented[1:] lines = unindented.splitlines() if len(lines) > 1: tail = ['%s%s' % (' ' * right_shift, line) for line in lines[1:]] all_lines = [ heading.ljust(right_shift) + lines[0]] + tail elif len(lines) == 1: all_lines = [ heading.ljust(right_shift) + lines[0]] else: all_lines = [] if i % 2: # Alternate red and blue for docstrings contents.extend([red %el for el in all_lines]) else: contents.extend([blue %el for el in all_lines]) return "\n".join(contents) def sort_by_precedence(self, parameters): """ Sort the provided dictionary of parameters by their precedence value. In Python 3, preserves the original ordering for parameters with the same precedence; for Python 2 sorts them lexicographically by name, unless explicit precedences are provided. """ params = [(p, pobj) for p, pobj in parameters.items()] key_fn = lambda x: x[1].precedence if x[1].precedence is not None else 1e-8 sorted_params = sorted(params, key=key_fn) groups = itertools.groupby(sorted_params, key=key_fn) # Params preserve definition order in Python 3.6+ dict_ordered = ( (sys.version_info.major == 3 and sys.version_info.minor >= 6) or (sys.version_info.major > 3) or all(p.precedence is not None for p in parameters.values()) ) ordered_groups = [list(grp) if dict_ordered else sorted(grp) for (_, grp) in groups] ordered_params = [el[0] for group in ordered_groups for el in group if (el[0] != 'name' or el[0] in parameters)] return ordered_params def _build_table(self, info, order, max_col_len=40, only_changed=False): """ Collect the information about parameters needed to build a properly formatted table and then tabulate it. """ info_list, bounds_dict = [], {} (params, val_dict, changed) = info col_widths = dict((k,0) for k in order) ordering = self.sort_by_precedence(params) for name in ordering: p = params[name] if only_changed and not (name in changed): continue constant = 'C' if p.constant else 'V' readonly = 'RO' if p.readonly else 'RW' allow_None = ' AN' if hasattr(p, 'allow_None') and p.allow_None else '' mode = '%s %s%s' % (constant, readonly, allow_None) value = repr(val_dict[name]) if len(value) > (max_col_len - 3): value = value[:max_col_len-3] + '...' p_dict = {'name': name, 'type': p.__class__.__name__, 'mode': mode, 'value': value} if hasattr(p, 'bounds'): lbound, ubound = (None,None) if p.bounds is None else p.bounds mark_lbound, mark_ubound = False, False # Use soft_bounds when bounds not defined. if hasattr(p, 'get_soft_bounds'): soft_lbound, soft_ubound = p.get_soft_bounds() if lbound is None and soft_lbound is not None: lbound = soft_lbound mark_lbound = True if ubound is None and soft_ubound is not None: ubound = soft_ubound mark_ubound = True if (lbound, ubound) != (None,None): bounds_dict[name] = (mark_lbound, mark_ubound) p_dict['bounds'] = '(%s, %s)' % (lbound, ubound) for col in p_dict: max_width = max([col_widths[col], len(p_dict[col])]) col_widths[col] = max_width info_list.append((name, p_dict)) return self._tabulate(info_list, col_widths, changed, order, bounds_dict) def _tabulate(self, info_list, col_widths, changed, order, bounds_dict): """ Returns the supplied information as a table suitable for printing or paging. info_list: List of the parameters name, type and mode. col_widths: Dictionary of column widths in characters changed: List of parameters modified from their defaults. order: The order of the table columns bound_dict: Dictionary of appropriately formatted bounds """ contents, tail = [], [] column_set = set(k for _, row in info_list for k in row) columns = [col for col in order if col in column_set] title_row = [] # Generate the column headings for i, col in enumerate(columns): width = col_widths[col]+2 col = col.capitalize() formatted = col.ljust(width) if i == 0 else col.center(width) title_row.append(formatted) contents.append(blue % ''.join(title_row)+"\n") # Format the table rows for row, info in info_list: row_list = [] for i,col in enumerate(columns): width = col_widths[col]+2 val = info[col] if (col in info) else '' formatted = val.ljust(width) if i==0 else val.center(width) if col == 'bounds' and bounds_dict.get(row,False): (mark_lbound, mark_ubound) = bounds_dict[row] lval, uval = formatted.rsplit(',') lspace, lstr = lval.rsplit('(') ustr, uspace = uval.rsplit(')') lbound = lspace + '('+(cyan % lstr) if mark_lbound else lval ubound = (cyan % ustr)+')'+uspace if mark_ubound else uval formatted = "%s,%s" % (lbound, ubound) row_list.append(formatted) row_text = ''.join(row_list) if row in changed: row_text = red % row_text contents.append(row_text) return '\n'.join(contents+tail) def __call__(self, param_obj): """ Given a Parameterized object or class, display information about the parameters in the IPython pager. """ title = None if not self.metaclass: parameterized_object = isinstance(param_obj, param.Parameterized) parameterized_class = (isinstance(param_obj,type) and issubclass(param_obj,param.Parameterized)) if not (parameterized_object or parameterized_class): print("Object is not a Parameterized class or object.") return if parameterized_object: # Only show the name if not autogenerated class_name = param_obj.__class__.__name__ default_name = re.match('^'+class_name+'[0-9]+$', param_obj.name) obj_name = '' if default_name else (' %r' % param_obj.name) title = 'Parameters of %r instance%s' % (class_name, obj_name) if title is None: title = 'Parameters of %r' % param_obj.name heading_line = '=' * len(title) heading_text = "%s\n%s\n" % (title, heading_line) param_info = self.get_param_info(param_obj, include_super=True) if not param_info[0]: return "%s\n%s" % ((green % heading_text), "Object has no parameters.") table = self._build_table(param_info, self.order, max_col_len=40, only_changed=False) docstrings = self.param_docstrings(param_info, max_col_len=100, only_changed=False) dflt_msg = "Parameters changed from their default values are marked in red." top_heading = (green % heading_text) top_heading += "\n%s" % (red % dflt_msg) top_heading += "\n%s" % (cyan % "Soft bound values are marked in cyan.") top_heading += '\nC/V= Constant/Variable, RO/RW = ReadOnly/ReadWrite, AN=Allow None' heading_text = 'Parameter docstrings:' heading_string = "%s\n%s" % (heading_text, '=' * len(heading_text)) docstring_heading = (green % heading_string) return "%s\n\n%s\n\n%s\n\n%s" % (top_heading, table, docstring_heading, docstrings) message = """Welcome to the param IPython extension! (https://param.holoviz.org/)""" message += '\nAvailable magics: %params' _loaded = False def load_ipython_extension(ip, verbose=True): from IPython.core.magic import Magics, magics_class, line_magic from IPython.core import page @magics_class class ParamMagics(Magics): """ Implements the %params line magic used to inspect the parameters of a parameterized class or object. """ def __init__(self, *args, **kwargs): super(ParamMagics, self).__init__(*args, **kwargs) self.param_pager = ParamPager() @line_magic def params(self, parameter_s='', namespaces=None): """ The %params line magic accepts a single argument which is a handle on the parameterized object to be inspected. If the object can be found in the active namespace, information about the object's parameters is displayed in the IPython pager. Usage: %params """ if parameter_s=='': print("Please specify an object to inspect.") return # Beware! Uses IPython internals that may change in future... obj = self.shell._object_find(parameter_s) if obj.found is False: print("Object %r not found in the namespace." % parameter_s) return page.page(self.param_pager(obj.obj)) if verbose: print(message) global _loaded if not _loaded: _loaded = True ip.register_magics(ParamMagics) param-1.12.3/param/parameterized.py000066400000000000000000004355001434441564000172200ustar00rootroot00000000000000""" Generic support for objects with full-featured Parameters and messaging. This file comes from the Param library (https://github.com/holoviz/param) but can be taken out of the param module and used on its own if desired, either alone (providing basic Parameter support) or with param's __init__.py (providing specialized Parameter types). """ import copy import datetime as dt import re import sys import inspect import random import numbers import operator # Allow this file to be used standalone if desired, albeit without JSON serialization try: from . import serializer except ImportError: serializer = None from collections import defaultdict, namedtuple, OrderedDict from functools import partial, wraps, reduce from operator import itemgetter,attrgetter from types import FunctionType import logging from contextlib import contextmanager from logging import DEBUG, INFO, WARNING, ERROR, CRITICAL try: # In case the optional ipython module is unavailable from .ipython import ParamPager param_pager = ParamPager(metaclass=True) # Generates param description except: param_pager = None try: from inspect import getfullargspec except: from inspect import getargspec as getfullargspec # python2 dt_types = (dt.datetime, dt.date) try: import numpy as np dt_types = dt_types + (np.datetime64,) except: pass basestring = basestring if sys.version_info[0]==2 else str # noqa: it is defined VERBOSE = INFO - 1 logging.addLevelName(VERBOSE, "VERBOSE") # Get the appropriate logging.Logger instance. If `logger` is None, a # logger named `"param"` will be instantiated. If `name` is set, a descendant # logger with the name ``"param."`` is returned (or # ``logger.name + "."``) logger = None def get_logger(name=None): if logger is None: root_logger = logging.getLogger('param') if not root_logger.handlers: root_logger.setLevel(logging.INFO) formatter = logging.Formatter( fmt='%(levelname)s:%(name)s: %(message)s') handler = logging.StreamHandler() handler.setFormatter(formatter) root_logger.addHandler(handler) else: root_logger = logger if name is None: return root_logger else: return logging.getLogger(root_logger.name + '.' + name) # Indicates whether warnings should be raised as errors, stopping # processing. warnings_as_exceptions = False docstring_signature = True # Add signature to class docstrings docstring_describe_params = True # Add parameter description to class # docstrings (requires ipython module) object_count = 0 warning_count = 0 class _Undefined: """ Dummy value to signal completely undefined values rather than simple None values. """ @contextmanager def logging_level(level): """ Temporarily modify param's logging level. """ level = level.upper() levels = [DEBUG, INFO, WARNING, ERROR, CRITICAL, VERBOSE] level_names = ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL', 'VERBOSE'] if level not in level_names: raise Exception("Level %r not in %r" % (level, levels)) param_logger = get_logger() logging_level = param_logger.getEffectiveLevel() param_logger.setLevel(levels[level_names.index(level)]) try: yield None finally: param_logger.setLevel(logging_level) @contextmanager def _batch_call_watchers(parameterized, enable=True, run=True): """ Internal version of batch_call_watchers, adding control over queueing and running. Only actually batches events if enable=True; otherwise a no-op. Only actually calls the accumulated watchers on exit if run=True; otherwise they remain queued. """ BATCH_WATCH = parameterized.param._BATCH_WATCH parameterized.param._BATCH_WATCH = enable or parameterized.param._BATCH_WATCH try: yield finally: parameterized.param._BATCH_WATCH = BATCH_WATCH if run and not BATCH_WATCH: parameterized.param._batch_call_watchers() batch_watch = _batch_call_watchers # PARAM2_DEPRECATION: Remove this compatibility alias for param 2.0 and later. @contextmanager def batch_call_watchers(parameterized): """ Context manager to batch events to provide to Watchers on a parameterized object. This context manager queues any events triggered by setting a parameter on the supplied parameterized object, saving them up to dispatch them all at once when the context manager exits. """ BATCH_WATCH = parameterized.param._BATCH_WATCH parameterized.param._BATCH_WATCH = True try: yield finally: parameterized.param._BATCH_WATCH = BATCH_WATCH if not BATCH_WATCH: parameterized.param._batch_call_watchers() @contextmanager def edit_constant(parameterized): """ Temporarily set parameters on Parameterized object to constant=False to allow editing them. """ params = parameterized.param.objects('existing').values() constants = [p.constant for p in params] for p in params: p.constant = False try: yield except: raise finally: for (p, const) in zip(params, constants): p.constant = const @contextmanager def discard_events(parameterized): """ Context manager that discards any events within its scope triggered on the supplied parameterized object. """ batch_watch = parameterized.param._BATCH_WATCH parameterized.param._BATCH_WATCH = True watchers, events = (list(parameterized.param._watchers), list(parameterized.param._events)) try: yield except: raise finally: parameterized.param._BATCH_WATCH = batch_watch parameterized.param._watchers = watchers parameterized.param._events = events # External components can register an async executor which will run # async functions async_executor = None def classlist(class_): """ Return a list of the class hierarchy above (and including) the given class. Same as `inspect.getmro(class_)[::-1]` """ return inspect.getmro(class_)[::-1] def descendents(class_): """ Return a list of the class hierarchy below (and including) the given class. The list is ordered from least- to most-specific. Can be useful for printing the contents of an entire class hierarchy. """ assert isinstance(class_,type) q = [class_] out = [] while len(q): x = q.pop(0) out.insert(0,x) for b in x.__subclasses__(): if b not in q and b not in out: q.append(b) return out[::-1] def get_all_slots(class_): """ Return a list of slot names for slots defined in `class_` and its superclasses. """ # A subclass's __slots__ attribute does not contain slots defined # in its superclass (the superclass' __slots__ end up as # attributes of the subclass). all_slots = [] parent_param_classes = [c for c in classlist(class_)[1::]] for c in parent_param_classes: if hasattr(c,'__slots__'): all_slots+=c.__slots__ return all_slots def get_occupied_slots(instance): """ Return a list of slots for which values have been set. (While a slot might be defined, if a value for that slot hasn't been set, then it's an AttributeError to request the slot's value.) """ return [slot for slot in get_all_slots(type(instance)) if hasattr(instance,slot)] def all_equal(arg1,arg2): """ Return a single boolean for arg1==arg2, even for numpy arrays using element-wise comparison. Uses all(arg1==arg2) for sequences, and arg1==arg2 otherwise. If both objects have an '_infinitely_iterable' attribute, they are not be zipped together and are compared directly instead. """ if all(hasattr(el, '_infinitely_iterable') for el in [arg1,arg2]): return arg1==arg2 try: return all(a1 == a2 for a1, a2 in zip(arg1, arg2)) except TypeError: return arg1==arg2 # PARAM2_DEPRECATION: For Python 2 compatibility only; can be removed in param2. # # The syntax to use a metaclass changed incompatibly between 2 and # 3. The add_metaclass() class decorator below creates a class using a # specified metaclass in a way that works on both 2 and 3. For 3, can # remove this decorator and specify metaclasses in a simpler way # (https://docs.python.org/3/reference/datamodel.html#customizing-class-creation) # # Code from six (https://bitbucket.org/gutworth/six; version 1.4.1). def add_metaclass(metaclass): """Class decorator for creating a class with a metaclass.""" def wrapper(cls): orig_vars = cls.__dict__.copy() orig_vars.pop('__dict__', None) orig_vars.pop('__weakref__', None) for slots_var in orig_vars.get('__slots__', ()): orig_vars.pop(slots_var) return metaclass(cls.__name__, cls.__bases__, orig_vars) return wrapper class bothmethod(object): # pylint: disable-msg=R0903 """ 'optional @classmethod' A decorator that allows a method to receive either the class object (if called on the class) or the instance object (if called on the instance) as its first argument. Code (but not documentation) copied from: http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/523033. """ # pylint: disable-msg=R0903 def __init__(self, func): self.func = func # i.e. this is also a non-data descriptor def __get__(self, obj, type_=None): if obj is None: return wraps(self.func)(partial(self.func, type_)) else: return wraps(self.func)(partial(self.func, obj)) def _getattrr(obj, attr, *args): def _getattr(obj, attr): return getattr(obj, attr, *args) return reduce(_getattr, [obj] + attr.split('.')) def accept_arguments(f): """ Decorator for decorators that accept arguments """ @wraps(f) def _f(*args, **kwargs): return lambda actual_f: f(actual_f, *args, **kwargs) return _f def no_instance_params(cls): """ Disables instance parameters on the class """ cls._disable_instance__params = True return cls def iscoroutinefunction(function): """ Whether the function is an asynchronous coroutine function. """ if not hasattr(inspect, 'iscoroutinefunction'): return False import asyncio try: return ( inspect.isasyncgenfunction(function) or asyncio.iscoroutinefunction(function) ) except AttributeError: return False def instance_descriptor(f): # If parameter has an instance Parameter, delegate setting def _f(self, obj, val): instance_param = getattr(obj, '_instance__params', {}).get(self.name) if instance_param is not None and self is not instance_param: instance_param.__set__(obj, val) return return f(self, obj, val) return _f def get_method_owner(method): """ Gets the instance that owns the supplied method """ if not inspect.ismethod(method): return None if isinstance(method, partial): method = method.func return method.__self__ if sys.version_info.major >= 3 else method.im_self @accept_arguments def depends(func, *dependencies, **kw): """ Annotates a function or Parameterized method to express its dependencies. The specified dependencies can be either be Parameter instances or if a method is supplied they can be defined as strings referring to Parameters of the class, or Parameters of subobjects (Parameterized objects that are values of this object's parameters). Dependencies can either be on Parameter values, or on other metadata about the Parameter. """ # PARAM2_DEPRECATION: python2 workaround; python3 allows kw-only args # (i.e. "func, *dependencies, watch=False" rather than **kw and the check below) watch = kw.pop("watch", False) on_init = kw.pop("on_init", False) if iscoroutinefunction(func): from ._async import generate_depends _depends = generate_depends(func) else: @wraps(func) def _depends(*args, **kw): return func(*args, **kw) deps = list(dependencies)+list(kw.values()) string_specs = False for dep in deps: if isinstance(dep, basestring): string_specs = True elif not isinstance(dep, Parameter): raise ValueError('The depends decorator only accepts string ' 'types referencing a parameter or parameter ' 'instances, found %s type instead.' % type(dep).__name__) elif not (isinstance(dep.owner, Parameterized) or (isinstance(dep.owner, ParameterizedMetaclass))): owner = 'None' if dep.owner is None else '%s class' % type(dep.owner).__name__ raise ValueError('Parameters supplied to the depends decorator, ' 'must be bound to a Parameterized class or ' 'instance not %s.' % owner) if (any(isinstance(dep, Parameter) for dep in deps) and any(isinstance(dep, basestring) for dep in deps)): raise ValueError('Dependencies must either be defined as strings ' 'referencing parameters on the class defining ' 'the decorated method or as parameter instances. ' 'Mixing of string specs and parameter instances ' 'is not supported.') elif string_specs and kw: raise AssertionError('Supplying keywords to the decorated method ' 'or function is not supported when referencing ' 'parameters by name.') if not string_specs and watch: # string_specs case handled elsewhere (later), in Parameterized.__init__ if iscoroutinefunction(func): from ._async import generate_callback cb = generate_callback(func, dependencies, kw) else: def cb(*events): args = (getattr(dep.owner, dep.name) for dep in dependencies) dep_kwargs = {n: getattr(dep.owner, dep.name) for n, dep in kw.items()} return func(*args, **dep_kwargs) grouped = defaultdict(list) for dep in deps: grouped[id(dep.owner)].append(dep) for group in grouped.values(): group[0].owner.param.watch(cb, [dep.name for dep in group]) _dinfo = getattr(func, '_dinfo', {}) _dinfo.update({'dependencies': dependencies, 'kw': kw, 'watch': watch, 'on_init': on_init}) _depends._dinfo = _dinfo return _depends @accept_arguments def output(func, *output, **kw): """ output allows annotating a method on a Parameterized class to declare that it returns an output of a specific type. The outputs of a Parameterized class can be queried using the Parameterized.param.outputs method. By default the output will inherit the method name but a custom name can be declared by expressing the Parameter type using a keyword argument. Declaring multiple return types using keywords is only supported in Python >= 3.6. The simplest declaration simply declares the method returns an object without any type guarantees, e.g.: @output() If a specific parameter type is specified this is a declaration that the method will return a value of that type, e.g.: @output(param.Number()) To override the default name of the output the type may be declared as a keyword argument, e.g.: @output(custom_name=param.Number()) Multiple outputs may be declared using keywords mapping from output name to the type for Python >= 3.6 or using tuples of the same format, which is supported for earlier versions, i.e. these two declarations are equivalent: @output(number=param.Number(), string=param.String()) @output(('number', param.Number()), ('string', param.String())) output also accepts Python object types which will be upgraded to a ClassSelector, e.g.: @output(int) """ if output: outputs = [] for i, out in enumerate(output): i = i if len(output) > 1 else None if isinstance(out, tuple) and len(out) == 2 and isinstance(out[0], str): outputs.append(out+(i,)) elif isinstance(out, str): outputs.append((out, Parameter(), i)) else: outputs.append((None, out, i)) elif kw: py_major = sys.version_info.major py_minor = sys.version_info.minor if (py_major < 3 or (py_major == 3 and py_minor < 6)) and len(kw) > 1: raise ValueError('Multiple output declaration using keywords ' 'only supported in Python >= 3.6.') # (requires keywords to be kept ordered, which was not true in previous versions) outputs = [(name, otype, i if len(kw) > 1 else None) for i, (name, otype) in enumerate(kw.items())] else: outputs = [(None, Parameter(), None)] names, processed = [], [] for name, otype, i in outputs: if isinstance(otype, type): if issubclass(otype, Parameter): otype = otype() else: from .import ClassSelector otype = ClassSelector(class_=otype) elif isinstance(otype, tuple) and all(isinstance(t, type) for t in otype): from .import ClassSelector otype = ClassSelector(class_=otype) if not isinstance(otype, Parameter): raise ValueError('output type must be declared with a Parameter class, ' 'instance or a Python object type.') processed.append((name, otype, i)) names.append(name) if len(set(names)) != len(names): raise ValueError('When declaring multiple outputs each value ' 'must be unique.') _dinfo = getattr(func, '_dinfo', {}) _dinfo.update({'outputs': processed}) @wraps(func) def _output(*args,**kw): return func(*args,**kw) _output._dinfo = _dinfo return _output def _parse_dependency_spec(spec): """ Parses param.depends specifications into three components: 1. The dotted path to the sub-object 2. The attribute being depended on, i.e. either a parameter or method 3. The parameter attribute being depended on """ assert spec.count(":")<=1 spec = spec.strip() m = re.match("(?P[^:]*):?(?P.*)", spec) what = m.group('what') path = "."+m.group('path') m = re.match(r"(?P.*)(\.)(?P.*)", path) obj = m.group('obj') attr = m.group("attr") return obj or None, attr, what or 'value' def _params_depended_on(minfo, dynamic=True, intermediate=True): """ Resolves dependencies declared on a Parameterized method. Dynamic dependencies, i.e. dependencies on sub-objects which may or may not yet be available, are only resolved if dynamic=True. By default intermediate dependencies, i.e. dependencies on the path to a sub-object are returned. For example for a dependency on 'a.b.c' dependencies on 'a' and 'b' are returned as long as intermediate=True. Returns lists of concrete dependencies on available parameters and dynamic dependencies specifications which have to resolved if the referenced sub-objects are defined. """ deps, dynamic_deps = [], [] dinfo = getattr(minfo.method, "_dinfo", {}) for d in dinfo.get('dependencies', list(minfo.cls.param)): ddeps, ddynamic_deps = (minfo.inst or minfo.cls).param._spec_to_obj(d, dynamic, intermediate) dynamic_deps += ddynamic_deps for dep in ddeps: if isinstance(dep, PInfo): deps.append(dep) else: method_deps, method_dynamic_deps = _params_depended_on(dep, dynamic, intermediate) deps += method_deps dynamic_deps += method_dynamic_deps return deps, dynamic_deps def _resolve_mcs_deps(obj, resolved, dynamic, intermediate=True): """ Resolves constant and dynamic parameter dependencies previously obtained using the _params_depended_on function. Existing resolved dependencies are updated with a supplied parameter instance while dynamic dependencies are resolved if possible. """ dependencies = [] for dep in resolved: if not issubclass(type(obj), dep.cls): dependencies.append(dep) continue inst = obj if dep.inst is None else dep.inst dep = PInfo(inst=inst, cls=dep.cls, name=dep.name, pobj=inst.param[dep.name], what=dep.what) dependencies.append(dep) for dep in dynamic: subresolved, _ = obj.param._spec_to_obj(dep.spec, intermediate=intermediate) for subdep in subresolved: if isinstance(subdep, PInfo): dependencies.append(subdep) else: dependencies += _params_depended_on(subdep, intermediate=intermediate)[0] return dependencies def _skip_event(*events, **kwargs): """ Checks whether a subobject event should be skipped. Returns True if all the values on the new subobject match the values on the previous subobject. """ what = kwargs.get('what', 'value') changed = kwargs.get('changed') if changed is None: return False for e in events: for p in changed: if what == 'value': old = _Undefined if e.old is None else _getattrr(e.old, p, None) new = _Undefined if e.new is None else _getattrr(e.new, p, None) else: old = _Undefined if e.old is None else _getattrr(e.old.param[p], what, None) new = _Undefined if e.new is None else _getattrr(e.new.param[p], what, None) if not Comparator.is_equal(old, new): return False return True def _m_caller(self, method_name, what='value', changed=None, callback=None): """ Wraps a method call adding support for scheduling a callback before it is executed and skipping events if a subobject has changed but its values have not. """ function = getattr(self, method_name) if iscoroutinefunction(function): from ._async import generate_caller caller = generate_caller(function, what=what, changed=changed, callback=callback, skip_event=_skip_event) else: def caller(*events): if callback: callback(*events) if not _skip_event(*events, what=what, changed=changed): return function() caller._watcher_name = method_name return caller def _add_doc(obj, docstring): """Add a docstring to a namedtuple, if on python3 where that's allowed""" if sys.version_info[0]>2: obj.__doc__ = docstring PInfo = namedtuple("PInfo", "inst cls name pobj what") _add_doc(PInfo, """ Object describing something being watched about a Parameter. `inst`: Parameterized instance owning the Parameter, or None `cls`: Parameterized class owning the Parameter `name`: Name of the Parameter being watched `pobj`: Parameter object being watched `what`: What is being watched on the Parameter (either 'value' or a slot name) """) MInfo = namedtuple("MInfo", "inst cls name method") _add_doc(MInfo, """ Object describing a Parameterized method being watched. `inst`: Parameterized instance owning the method, or None `cls`: Parameterized class owning the method `name`: Name of the method being watched `method`: bound method of the object being watched """) DInfo = namedtuple("DInfo", "spec") _add_doc(DInfo, """ Object describing dynamic dependencies. `spec`: Dependency specification to resolve """) Event = namedtuple("Event", "what name obj cls old new type") _add_doc(Event, """ Object representing an event that triggers a Watcher. `what`: What is being watched on the Parameter (either value or a slot name) `name`: Name of the Parameter that was set or triggered `obj`: Parameterized instance owning the watched Parameter, or None `cls`: Parameterized class owning the watched Parameter `old`: Previous value of the item being watched `new`: New value of the item being watched `type`: `triggered` if this event was triggered explicitly), `changed` if the item was set and watching for `onlychanged`, `set` if the item was set, or None if type not yet known """) _Watcher = namedtuple("Watcher", "inst cls fn mode onlychanged parameter_names what queued precedence") class Watcher(_Watcher): """ Object declaring a callback function to invoke when an Event is triggered on a watched item. `inst`: Parameterized instance owning the watched Parameter, or None `cls`: Parameterized class owning the watched Parameter `fn`: Callback function to invoke when triggered by a watched Parameter `mode`: 'args' for param.watch (call `fn` with PInfo object positional args), or 'kwargs' for param.watch_values (call `fn` with : keywords) `onlychanged`: If True, only trigger for actual changes, not setting to the current value `parameter_names`: List of Parameters to watch, by name `what`: What to watch on the Parameters (either 'value' or a slot name) `queued`: Immediately invoke callbacks triggered during processing of an Event (if False), or queue them up for processing later, after this event has been handled (if True) `precedence`: A numeric value which determines the precedence of the watcher. Lower precedence values are executed with higher priority. """ def __new__(cls_, *args, **kwargs): """ Allows creating Watcher without explicit precedence value. """ values = dict(zip(cls_._fields, args)) values.update(kwargs) if 'precedence' not in values: values['precedence'] = 0 return super(Watcher, cls_).__new__(cls_, **values) def __iter__(self): """ Backward compatibility layer to allow tuple unpacking without the precedence value. Important for Panel which creates a custom Watcher and uses tuple unpacking. Will be dropped in Param 3.x. """ return iter(self[:-1]) def __str__(self): cls = type(self) attrs = ', '.join(['%s=%r' % (f, getattr(self, f)) for f in cls._fields]) return "{cls}({attrs})".format(cls=cls.__name__, attrs=attrs) class ParameterMetaclass(type): """ Metaclass allowing control over creation of Parameter classes. """ def __new__(mcs,classname,bases,classdict): # store the class's docstring in __classdoc if '__doc__' in classdict: classdict['__classdoc']=classdict['__doc__'] # when asking for help on Parameter *object*, return the doc slot classdict['__doc__']=property(attrgetter('doc')) # To get the benefit of slots, subclasses must themselves define # __slots__, whether or not they define attributes not present in # the base Parameter class. That's because a subclass will have # a __dict__ unless it also defines __slots__. if '__slots__' not in classdict: classdict['__slots__']=[] # No special handling for a __dict__ slot; should there be? return type.__new__(mcs,classname,bases,classdict) def __getattribute__(mcs,name): if name=='__doc__': # when asking for help on Parameter *class*, return the # stored class docstring return type.__getattribute__(mcs,'__classdoc') else: return type.__getattribute__(mcs,name) @add_metaclass(ParameterMetaclass) class Parameter(object): """ An attribute descriptor for declaring parameters. Parameters are a special kind of class attribute. Setting a Parameterized class attribute to be a Parameter instance causes that attribute of the class (and the class's instances) to be treated as a Parameter. This allows special behavior, including dynamically generated parameter values, documentation strings, constant and read-only parameters, and type or range checking at assignment time. For example, suppose someone wants to define two new kinds of objects Foo and Bar, such that Bar has a parameter delta, Foo is a subclass of Bar, and Foo has parameters alpha, sigma, and gamma (and delta inherited from Bar). She would begin her class definitions with something like this:: class Bar(Parameterized): delta = Parameter(default=0.6, doc='The difference between steps.') ... class Foo(Bar): alpha = Parameter(default=0.1, doc='The starting value.') sigma = Parameter(default=0.5, doc='The standard deviation.', constant=True) gamma = Parameter(default=1.0, doc='The ending value.') ... Class Foo would then have four parameters, with delta defaulting to 0.6. Parameters have several advantages over plain attributes: 1. Parameters can be set automatically when an instance is constructed: The default constructor for Foo (and Bar) will accept arbitrary keyword arguments, each of which can be used to specify the value of a Parameter of Foo (or any of Foo's superclasses). E.g., if a script does this:: myfoo = Foo(alpha=0.5) myfoo.alpha will return 0.5, without the Foo constructor needing special code to set alpha. If Foo implements its own constructor, keyword arguments will still be accepted if the constructor accepts a dictionary of keyword arguments (as in ``def __init__(self,**params):``), and then each class calls its superclass (as in ``super(Foo,self).__init__(**params)``) so that the Parameterized constructor will process the keywords. 2. A Parameterized class need specify only the attributes of a Parameter whose values differ from those declared in superclasses; the other values will be inherited. E.g. if Foo declares:: delta = Parameter(default=0.2) the default value of 0.2 will override the 0.6 inherited from Bar, but the doc will be inherited from Bar. 3. The Parameter descriptor class can be subclassed to provide more complex behavior, allowing special types of parameters that, for example, require their values to be numbers in certain ranges, generate their values dynamically from a random distribution, or read their values from a file or other external source. 4. The attributes associated with Parameters provide enough information for automatically generating property sheets in graphical user interfaces, allowing Parameterized instances to be edited by users. Note that Parameters can only be used when set as class attributes of Parameterized classes. Parameters used as standalone objects, or as class attributes of non-Parameterized classes, will not have the behavior described here. """ # Because they implement __get__ and __set__, Parameters are known # as 'descriptors' in Python; see "Implementing Descriptors" and # "Invoking Descriptors" in the 'Customizing attribute access' # section of the Python reference manual: # http://docs.python.org/ref/attribute-access.html # # Overview of Parameters for programmers # ====================================== # # Consider the following code: # # # class A(Parameterized): # p = Parameter(default=1) # # a1 = A() # a2 = A() # # # * a1 and a2 share one Parameter object (A.__dict__['p']). # # * The default (class) value of p is stored in this Parameter # object (A.__dict__['p'].default). # # * If the value of p is set on a1 (e.g. a1.p=2), a1's value of p # is stored in a1 itself (a1.__dict__['_p_param_value']) # # * When a1.p is requested, a1.__dict__['_p_param_value'] is # returned. When a2.p is requested, '_p_param_value' is not # found in a2.__dict__, so A.__dict__['p'].default (i.e. A.p) is # returned instead. # # # Be careful when referring to the 'name' of a Parameter: # # * A Parameterized class has a name for the attribute which is # being represented by the Parameter ('p' in the example above); # in the code, this is called the 'attrib_name'. # # * When a Parameterized instance has its own local value for a # parameter, it is stored as '_X_param_value' (where X is the # attrib_name for the Parameter); in the code, this is called # the internal_name. # So that the extra features of Parameters do not require a lot of # overhead, Parameters are implemented using __slots__ (see # http://www.python.org/doc/2.4/ref/slots.html). Instead of having # a full Python dictionary associated with each Parameter instance, # Parameter instances have an enumerated list (named __slots__) of # attributes, and reserve just enough space to store these # attributes. Using __slots__ requires special support for # operations to copy and restore Parameters (e.g. for Python # persistent storage pickling); see __getstate__ and __setstate__. __slots__ = ['name', '_internal_name', 'default', 'doc', 'precedence', 'instantiate', 'constant', 'readonly', 'pickle_default_value', 'allow_None', 'per_instance', 'watchers', 'owner', '_label'] # Note: When initially created, a Parameter does not know which # Parameterized class owns it, nor does it know its names # (attribute name, internal name). Once the owning Parameterized # class is created, owner, name, and _internal_name are # set. _serializers = {'json': serializer.JSONSerialization} def __init__(self,default=None, doc=None, label=None, precedence=None, # pylint: disable-msg=R0913 instantiate=False, constant=False, readonly=False, pickle_default_value=True, allow_None=False, per_instance=True): """Initialize a new Parameter object and store the supplied attributes: default: the owning class's value for the attribute represented by this Parameter, which can be overridden in an instance. doc: docstring explaining what this parameter represents. label: optional text label to be used when this Parameter is shown in a listing. If no label is supplied, the attribute name for this parameter in the owning Parameterized object is used. precedence: a numeric value, usually in the range 0.0 to 1.0, which allows the order of Parameters in a class to be defined in a listing or e.g. in GUI menus. A negative precedence indicates a parameter that should be hidden in such listings. instantiate: controls whether the value of this Parameter will be deepcopied when a Parameterized object is instantiated (if True), or if the single default value will be shared by all Parameterized instances (if False). For an immutable Parameter value, it is best to leave instantiate at the default of False, so that a user can choose to change the value at the Parameterized instance level (affecting only that instance) or at the Parameterized class or superclass level (affecting all existing and future instances of that class or superclass). For a mutable Parameter value, the default of False is also appropriate if you want all instances to share the same value state, e.g. if they are each simply referring to a single global object like a singleton. If instead each Parameterized should have its own independently mutable value, instantiate should be set to True, but note that there is then no simple way to change the value of this Parameter at the class or superclass level, because each instance, once created, will then have an independently instantiated value. constant: if true, the Parameter value can be changed only at the class level or in a Parameterized constructor call. The value is otherwise constant on the Parameterized instance, once it has been constructed. readonly: if true, the Parameter value cannot ordinarily be changed by setting the attribute at the class or instance levels at all. The value can still be changed in code by temporarily overriding the value of this slot and then restoring it, which is useful for reporting values that the _user_ should never change but which do change during code execution. pickle_default_value: whether the default value should be pickled. Usually, you would want the default value to be pickled, but there are rare cases where that would not be the case (e.g. for file search paths that are specific to a certain system). per_instance: whether a separate Parameter instance will be created for every Parameterized instance. True by default. If False, all instances of a Parameterized class will share the same Parameter object, including all validation attributes (bounds, etc.). See also instantiate, which is conceptually similar but affects the Parameter value rather than the Parameter object. allow_None: if True, None is accepted as a valid value for this Parameter, in addition to any other values that are allowed. If the default value is defined as None, allow_None is set to True automatically. default, doc, and precedence all default to None, which allows inheritance of Parameter slots (attributes) from the owning-class' class hierarchy (see ParameterizedMetaclass). """ self.name = None self.owner = None self.precedence = precedence self.default = default self.doc = doc self.constant = constant or readonly # readonly => constant self.readonly = readonly self._label = label self._internal_name = None self._set_instantiate(instantiate) self.pickle_default_value = pickle_default_value self.allow_None = (default is None or allow_None) self.watchers = {} self.per_instance = per_instance @classmethod def serialize(cls, value): "Given the parameter value, return a Python value suitable for serialization" return value @classmethod def deserialize(cls, value): "Given a serializable Python value, return a value that the parameter can be set to" return value def schema(self, safe=False, subset=None, mode='json'): if serializer is None: raise ImportError('Cannot import serializer.py needed to generate schema') if mode not in self._serializers: raise KeyError('Mode %r not in available serialization formats %r' % (mode, list(self._serializers.keys()))) return self._serializers[mode].param_schema(self.__class__.__name__, self, safe=safe, subset=subset) @property def label(self): if self.name and self._label is None: return label_formatter(self.name) else: return self._label @label.setter def label(self, val): self._label = val def _set_instantiate(self,instantiate): """Constant parameters must be instantiated.""" # instantiate doesn't actually matter for read-only # parameters, since they can't be set even on a class. But # having this code avoids needless instantiation. if self.readonly: self.instantiate = False else: self.instantiate = instantiate or self.constant # pylint: disable-msg=W0201 def __setattr__(self, attribute, value): if attribute == 'name' and getattr(self, 'name', None) and value != self.name: raise AttributeError("Parameter name cannot be modified after " "it has been bound to a Parameterized.") implemented = (attribute != "default" and hasattr(self, 'watchers') and attribute in self.watchers) slot_attribute = attribute in self.__slots__ try: old = getattr(self, attribute) if implemented else NotImplemented if slot_attribute: self._on_set(attribute, old, value) except AttributeError as e: if slot_attribute: # If Parameter slot is defined but an AttributeError was raised # we are in __setstate__ and watchers should not be triggered old = NotImplemented else: raise e super(Parameter, self).__setattr__(attribute, value) if old is NotImplemented: return event = Event(what=attribute, name=self.name, obj=None, cls=self.owner, old=old, new=value, type=None) for watcher in self.watchers[attribute]: self.owner.param._call_watcher(watcher, event) if not self.owner.param._BATCH_WATCH: self.owner.param._batch_call_watchers() def _on_set(self, attribute, old, value): """ Can be overridden on subclasses to handle changes when parameter attribute is set. """ def __get__(self, obj, objtype): # pylint: disable-msg=W0613 """ Return the value for this Parameter. If called for a Parameterized class, produce that class's value (i.e. this Parameter object's 'default' attribute). If called for a Parameterized instance, produce that instance's value, if one has been set - otherwise produce the class's value (default). """ if obj is None: # e.g. when __get__ called for a Parameterized class result = self.default else: result = obj.__dict__.get(self._internal_name,self.default) return result @instance_descriptor def __set__(self, obj, val): """ Set the value for this Parameter. If called for a Parameterized class, set that class's value (i.e. set this Parameter object's 'default' attribute). If called for a Parameterized instance, set the value of this Parameter on that instance (i.e. in the instance's __dict__, under the parameter's internal_name). If the Parameter's constant attribute is True, only allows the value to be set for a Parameterized class or on uninitialized Parameterized instances. If the Parameter's readonly attribute is True, only allows the value to be specified in the Parameter declaration inside the Parameterized source code. A read-only parameter also cannot be set on a Parameterized class. Note that until we support some form of read-only object, it is still possible to change the attributes of the object stored in a constant or read-only Parameter (e.g. one item in a list). """ # PARAM2_DEPRECATION: For Python 2 compatibility only; # Deprecated Number set_hook called here to avoid duplicating setter if hasattr(self, 'set_hook'): val = self.set_hook(obj,val) self._validate(val) _old = NotImplemented # obj can be None if __set__ is called for a Parameterized class if self.constant or self.readonly: if self.readonly: raise TypeError("Read-only parameter '%s' cannot be modified" % self.name) elif obj is None: _old = self.default self.default = val elif not obj.initialized: _old = obj.__dict__.get(self._internal_name, self.default) obj.__dict__[self._internal_name] = val else: _old = obj.__dict__.get(self._internal_name, self.default) if val is not _old: raise TypeError("Constant parameter '%s' cannot be modified"%self.name) else: if obj is None: _old = self.default self.default = val else: _old = obj.__dict__.get(self._internal_name, self.default) obj.__dict__[self._internal_name] = val self._post_setter(obj, val) if obj is not None: if not getattr(obj, 'initialized', False): return obj.param._update_deps(self.name) if obj is None: watchers = self.watchers.get("value") elif hasattr(obj, '_param_watchers') and self.name in obj._param_watchers: watchers = obj._param_watchers[self.name].get('value') if watchers is None: watchers = self.watchers.get("value") else: watchers = None obj = self.owner if obj is None else obj if obj is None or not watchers: return event = Event(what='value', name=self.name, obj=obj, cls=self.owner, old=_old, new=val, type=None) # Copy watchers here since they may be modified inplace during iteration for watcher in sorted(watchers, key=lambda w: w.precedence): obj.param._call_watcher(watcher, event) if not obj.param._BATCH_WATCH: obj.param._batch_call_watchers() def _validate_value(self, value, allow_None): """Implements validation for parameter value""" def _validate(self, val): """Implements validation for the parameter value and attributes""" self._validate_value(val, self.allow_None) def _post_setter(self, obj, val): """Called after the parameter value has been validated and set""" def __delete__(self,obj): raise TypeError("Cannot delete '%s': Parameters deletion not allowed." % self.name) def _set_names(self, attrib_name): if None not in (self.owner, self.name) and attrib_name != self.name: raise AttributeError('The %s parameter %r has already been ' 'assigned a name by the %s class, ' 'could not assign new name %r. Parameters ' 'may not be shared by multiple classes; ' 'ensure that you create a new parameter ' 'instance for each new class.' % (type(self).__name__, self.name, self.owner.name, attrib_name)) self.name = attrib_name self._internal_name = "_%s_param_value" % attrib_name def __getstate__(self): """ All Parameters have slots, not a dict, so we have to support pickle and deepcopy ourselves. """ state = {} for slot in get_occupied_slots(self): state[slot] = getattr(self,slot) return state def __setstate__(self,state): # set values of __slots__ (instead of in non-existent __dict__) # Handle renamed slots introduced for instance params if '_attrib_name' in state: state['name'] = state.pop('_attrib_name') if '_owner' in state: state['owner'] = state.pop('_owner') if 'watchers' not in state: state['watchers'] = {} if 'per_instance' not in state: state['per_instance'] = False if '_label' not in state: state['_label'] = None for (k,v) in state.items(): setattr(self,k,v) # Define one particular type of Parameter that is used in this file class String(Parameter): r""" A String Parameter, with a default value and optional regular expression (regex) matching. Example of using a regex to implement IPv4 address matching:: class IPAddress(String): '''IPv4 address as a string (dotted decimal notation)''' def __init__(self, default="0.0.0.0", allow_None=False, **kwargs): ip_regex = r'^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$' super(IPAddress, self).__init__(default=default, regex=ip_regex, **kwargs) """ __slots__ = ['regex'] def __init__(self, default="", regex=None, allow_None=False, **kwargs): super(String, self).__init__(default=default, allow_None=allow_None, **kwargs) self.regex = regex self.allow_None = (default is None or allow_None) self._validate(default) def _validate_regex(self, val, regex): if (val is None and self.allow_None): return if regex is not None and re.match(regex, val) is None: raise ValueError("String parameter %r value %r does not match regex %r." % (self.name, val, regex)) def _validate_value(self, val, allow_None): if allow_None and val is None: return if not isinstance(val, basestring): raise ValueError("String parameter %r only takes a string value, " "not value of type %s." % (self.name, type(val))) def _validate(self, val): self._validate_value(val, self.allow_None) self._validate_regex(val, self.regex) class shared_parameters(object): """ Context manager to share parameter instances when creating multiple Parameterized objects of the same type. Parameter default values are instantiated once and cached to be reused when another Parameterized object of the same type is instantiated. Can be useful to easily modify large collections of Parameterized objects at once and can provide a significant speedup. """ _share = False _shared_cache = {} def __enter__(self): shared_parameters._share = True def __exit__(self, exc_type, exc_val, exc_tb): shared_parameters._share = False shared_parameters._shared_cache = {} def as_uninitialized(fn): """ Decorator: call fn with the parameterized_instance's initialization flag set to False, then revert the flag. (Used to decorate Parameterized methods that must alter a constant Parameter.) """ @wraps(fn) def override_initialization(self_,*args,**kw): parameterized_instance = self_.self original_initialized = parameterized_instance.initialized parameterized_instance.initialized = False fn(parameterized_instance, *args, **kw) parameterized_instance.initialized = original_initialized return override_initialization class Comparator(object): """ Comparator defines methods for determining whether two objects should be considered equal. It works by registering custom comparison functions, which may either be registed by type or with a predicate function. If no matching comparison can be found for the two objects the comparison will return False. If registered by type the Comparator will check whether both objects are of that type and apply the comparison. If the equality function is instead registered with a function it will call the function with each object individually to check if the comparison applies. This is useful for defining comparisons for objects without explicitly importing them. To use the Comparator simply call the is_equal function. """ equalities = { numbers.Number: operator.eq, basestring: operator.eq, bytes: operator.eq, type(None): operator.eq, } equalities.update({dtt: operator.eq for dtt in dt_types}) @classmethod def is_equal(cls, obj1, obj2): for eq_type, eq in cls.equalities.items(): if ((isinstance(eq_type, FunctionType) and eq_type(obj1) and eq_type(obj2)) or (isinstance(obj1, eq_type) and isinstance(obj2, eq_type))): return eq(obj1, obj2) if isinstance(obj2, (list, set, tuple)): return cls.compare_iterator(obj1, obj2) elif isinstance(obj2, dict): return cls.compare_mapping(obj1, obj2) return False @classmethod def compare_iterator(cls, obj1, obj2): if type(obj1) != type(obj2) or len(obj1) != len(obj2): return False for o1, o2 in zip(obj1, obj2): if not cls.is_equal(o1, o2): return False return True @classmethod def compare_mapping(cls, obj1, obj2): if type(obj1) != type(obj2) or len(obj1) != len(obj2): return False for k in obj1: if k in obj2: if not cls.is_equal(obj1[k], obj2[k]): return False else: return False return True class Parameters(object): """Object that holds the namespace and implementation of Parameterized methods as well as any state that is not in __slots__ or the Parameters themselves. Exists at both the metaclass level (instantiated by the metaclass) and at the instance level. Can contain state specific to either the class or the instance as necessary. """ _disable_stubs = False # Flag used to disable stubs in the API1 tests # None for no action, True to raise and False to warn. def __init__(self_, cls, self=None): """ cls is the Parameterized class which is always set. self is the instance if set. """ self_.cls = cls self_.self = self @property def _BATCH_WATCH(self_): return self_.self_or_cls._parameters_state['BATCH_WATCH'] @_BATCH_WATCH.setter def _BATCH_WATCH(self_, value): self_.self_or_cls._parameters_state['BATCH_WATCH'] = value @property def _TRIGGER(self_): return self_.self_or_cls._parameters_state['TRIGGER'] @_TRIGGER.setter def _TRIGGER(self_, value): self_.self_or_cls._parameters_state['TRIGGER'] = value @property def _events(self_): return self_.self_or_cls._parameters_state['events'] @_events.setter def _events(self_, value): self_.self_or_cls._parameters_state['events'] = value @property def _watchers(self_): return self_.self_or_cls._parameters_state['watchers'] @_watchers.setter def _watchers(self_, value): self_.self_or_cls._parameters_state['watchers'] = value @property def watchers(self): """Read-only list of watchers on this Parameterized""" return self._watchers @property def self_or_cls(self_): return self_.cls if self_.self is None else self_.self def __setstate__(self, state): # Set old parameters state on Parameterized._parameters_state self_or_cls = state.get('self', state.get('cls')) for k in self_or_cls._parameters_state: key = '_'+k if key in state: self_or_cls._parameters_state[k] = state.pop(key) for k, v in state.items(): setattr(self, k, v) def __getitem__(self_, key): """ Returns the class or instance parameter """ inst = self_.self parameters = self_.objects(False) if inst is None else inst.param.objects(False) p = parameters[key] if (inst is not None and getattr(inst, 'initialized', False) and p.per_instance and not getattr(inst, '_disable_instance__params', False)): if key not in inst._instance__params: try: # Do not copy watchers on class parameter watchers = p.watchers p.watchers = {} p = copy.copy(p) except: raise finally: p.watchers = {k: list(v) for k, v in watchers.items()} p.owner = inst inst._instance__params[key] = p else: p = inst._instance__params[key] return p def __dir__(self_): """ Adds parameters to dir """ return super(Parameters, self_).__dir__() + list(self_) def __iter__(self_): """ Iterates over the parameters on this object. """ for p in self_.objects(instance=False): yield p def __contains__(self_, param): return param in list(self_) def __getattr__(self_, attr): """ Extends attribute access to parameter objects. """ cls = self_.__dict__.get('cls') if cls is None: # Class not initialized raise AttributeError try: params = list(getattr(cls, '_%s__params' % cls.__name__)) except AttributeError: params = [n for class_ in classlist(cls) for n, v in class_.__dict__.items() if isinstance(v, Parameter)] if attr in params: return self_.__getitem__(attr) elif self_.self is None: raise AttributeError("type object '%s.param' has no attribute %r" % (self_.cls.__name__, attr)) else: raise AttributeError("'%s.param' object has no attribute %r" % (self_.cls.__name__, attr)) @as_uninitialized def _set_name(self_, name): self = self_.param.self self.name=name @as_uninitialized def _generate_name(self_): self = self_.param.self self.param._set_name('%s%05d' % (self.__class__.__name__ ,object_count)) @as_uninitialized def _setup_params(self_,**params): """ Initialize default and keyword parameter values. First, ensures that all Parameters with 'instantiate=True' (typically used for mutable Parameters) are copied directly into each object, to ensure that there is an independent copy (to avoid surprising aliasing errors). Then sets each of the keyword arguments, warning when any of them are not defined as parameters. Constant Parameters can be set during calls to this method. """ self = self_.param.self ## Deepcopy all 'instantiate=True' parameters # (building a set of names first to avoid redundantly # instantiating a later-overridden parent class's parameter) params_to_instantiate = {} for class_ in classlist(type(self)): if not issubclass(class_, Parameterized): continue for (k, v) in class_.param._parameters.items(): # (avoid replacing name with the default of None) if v.instantiate and k != "name": params_to_instantiate[k] = v for p in params_to_instantiate.values(): self.param._instantiate_param(p) ## keyword arg setting for name, val in params.items(): desc = self.__class__.get_param_descriptor(name)[0] # pylint: disable-msg=E1101 if not desc: self.param.warning("Setting non-parameter attribute %s=%s using a mechanism intended only for parameters", name, val) # i.e. if not desc it's setting an attribute in __dict__, not a Parameter setattr(self, name, val) # PARAM2_DEPRECATION: Backwards compatibilitity for param<1.12 @classmethod def deprecate(cls, fn): """ Decorator to issue warnings for API moving onto the param namespace and to add a docstring directing people to the appropriate method. """ def inner(*args, **kwargs): if cls._disable_stubs: raise AssertionError('Stubs supporting old API disabled') elif cls._disable_stubs is None: pass elif cls._disable_stubs is False: get_logger(name=args[0].__class__.__name__).log( WARNING, 'Use method %r via param namespace ' % fn.__name__) return fn(*args, **kwargs) inner.__doc__= "Inspect .param.%s method for the full docstring" % fn.__name__ return inner @classmethod def _changed(cls, event): """ Predicate that determines whether a Event object has actually changed such that old != new. """ return not Comparator.is_equal(event.old, event.new) def _instantiate_param(self_, param_obj, dict_=None, key=None): # deepcopy param_obj.default into self.__dict__ (or dict_ if supplied) # under the parameter's _internal_name (or key if supplied) self = self_.self dict_ = dict_ or self.__dict__ key = key or param_obj._internal_name if shared_parameters._share: param_key = (str(type(self)), param_obj.name) if param_key in shared_parameters._shared_cache: new_object = shared_parameters._shared_cache[param_key] else: new_object = copy.deepcopy(param_obj.default) shared_parameters._shared_cache[param_key] = new_object else: new_object = copy.deepcopy(param_obj.default) dict_[key] = new_object if isinstance(new_object, Parameterized): global object_count object_count += 1 # Writes over name given to the original object; # could instead have kept the same name new_object.param._generate_name() def _update_deps(self_, attribute=None, init=False): obj = self_.self init_methods = [] for method, queued, on_init, constant, dynamic in type(obj).param._depends['watch']: # On initialization set up constant watchers; otherwise # clean up previous dynamic watchers for the updated attribute dynamic = [d for d in dynamic if attribute is None or d.spec.split(".")[0] == attribute] if init: constant_grouped = defaultdict(list) for dep in _resolve_mcs_deps(obj, constant, []): constant_grouped[(id(dep.inst), id(dep.cls), dep.what)].append((None, dep)) for group in constant_grouped.values(): self_._watch_group(obj, method, queued, group) m = getattr(self_.self, method) if on_init and m not in init_methods: init_methods.append(m) elif dynamic: for w in obj._dynamic_watchers.pop(method, []): (w.inst or w.cls).param.unwatch(w) else: continue # Resolve dynamic dependencies one-by-one to be able to trace their watchers grouped = defaultdict(list) for ddep in dynamic: for dep in _resolve_mcs_deps(obj, [], [ddep]): grouped[(id(dep.inst), id(dep.cls), dep.what)].append((ddep, dep)) for group in grouped.values(): watcher = self_._watch_group(obj, method, queued, group, attribute) obj._dynamic_watchers[method].append(watcher) for m in init_methods: m() def _resolve_dynamic_deps(self, obj, dynamic_dep, param_dep, attribute): """ If a subobject whose parameters are being depended on changes we should only trigger events if the actual parameter values of the new object differ from those on the old subobject, therefore we accumulate parameters to compare on a subobject change event. Additionally we need to make sure to notify the parent object if a subobject changes so the dependencies can be reinitialized so we return a callback which updates the dependencies. """ subobj = obj subobjs = [obj] for subpath in dynamic_dep.spec.split('.')[:-1]: subobj = getattr(subobj, subpath.split(':')[0], None) subobjs.append(subobj) dep_obj = (param_dep.inst or param_dep.cls) if dep_obj not in subobjs[:-1]: return None, None, param_dep.what depth = subobjs.index(dep_obj) callback = None if depth > 0: def callback(*events): """ If a subobject changes, we need to notify the main object to update the dependencies. """ obj.param._update_deps(attribute) p = '.'.join(dynamic_dep.spec.split(':')[0].split('.')[depth+1:]) if p == 'param': subparams = [sp for sp in list(subobjs[-1].param)] else: subparams = [p] if ':' in dynamic_dep.spec: what = dynamic_dep.spec.split(':')[-1] else: what = param_dep.what return subparams, callback, what def _watch_group(self_, obj, name, queued, group, attribute=None): """ Sets up a watcher for a group of dependencies. Ensures that if the dependency was dynamically generated we check whether a subobject change event actually causes a value change and that we update the existing watchers, i.e. clean up watchers on the old subobject and create watchers on the new subobject. """ dynamic_dep, param_dep = group[0] dep_obj = (param_dep.inst or param_dep.cls) params = [] for _, g in group: if g.name not in params: params.append(g.name) if dynamic_dep is None: subparams, callback, what = None, None, param_dep.what else: subparams, callback, what = self_._resolve_dynamic_deps( obj, dynamic_dep, param_dep, attribute) mcaller = _m_caller(obj, name, what, subparams, callback) return dep_obj.param._watch( mcaller, params, param_dep.what, queued=queued, precedence=-1) # Classmethods # PARAM2_DEPRECATION: Backwards compatibilitity for param<1.12 def print_param_defaults(self_): """Print the default values of all cls's Parameters.""" cls = self_.cls for key,val in cls.__dict__.items(): if isinstance(val,Parameter): print(cls.__name__+'.'+key+ '='+ repr(val.default)) # PARAM2_DEPRECATION: Backwards compatibilitity for param<1.12 def set_default(self_,param_name,value): """ Set the default value of param_name. Equivalent to setting param_name on the class. """ cls = self_.cls setattr(cls,param_name,value) def add_parameter(self_, param_name, param_obj): """ Add a new Parameter object into this object's class. Should result in a Parameter equivalent to one declared in the class's source code. """ # Could have just done setattr(cls,param_name,param_obj), # which is supported by the metaclass's __setattr__ , but # would need to handle the params() cache as well # (which is tricky but important for startup speed). cls = self_.cls type.__setattr__(cls,param_name,param_obj) ParameterizedMetaclass._initialize_parameter(cls,param_name,param_obj) # delete cached params() try: delattr(cls,'_%s__params'%cls.__name__) except AttributeError: pass # PARAM2_DEPRECATION: Backwards compatibilitity for param<1.12 _add_parameter = add_parameter # PARAM2_DEPRECATION: Backwards compatibilitity for param<1.12 def params(self_, parameter_name=None): """ Return the Parameters of this class as the dictionary {name: parameter_object} Includes Parameters from this class and its superclasses. """ pdict = self_.objects(instance='existing') if parameter_name is None: return pdict else: return pdict[parameter_name] # Bothmethods def update(self_, *args, **kwargs): """ For the given dictionary or iterable or set of param=value keyword arguments, sets the corresponding parameter of this object or class to the given value. """ BATCH_WATCH = self_.self_or_cls.param._BATCH_WATCH self_.self_or_cls.param._BATCH_WATCH = True self_or_cls = self_.self_or_cls if args: if len(args) == 1 and not kwargs: kwargs = args[0] else: self_.self_or_cls.param._BATCH_WATCH = False raise ValueError("%s.update accepts *either* an iterable or key=value pairs, not both" % (self_or_cls.name)) trigger_params = [k for k in kwargs if ((k in self_.self_or_cls.param) and hasattr(self_.self_or_cls.param[k], '_autotrigger_value'))] for tp in trigger_params: self_.self_or_cls.param[tp]._mode = 'set' for (k, v) in kwargs.items(): if k not in self_or_cls.param: self_.self_or_cls.param._BATCH_WATCH = False raise ValueError("'%s' is not a parameter of %s" % (k, self_or_cls.name)) try: setattr(self_or_cls, k, v) except: self_.self_or_cls.param._BATCH_WATCH = False raise self_.self_or_cls.param._BATCH_WATCH = BATCH_WATCH if not BATCH_WATCH: self_._batch_call_watchers() for tp in trigger_params: p = self_.self_or_cls.param[tp] p._mode = 'reset' setattr(self_or_cls, tp, p._autotrigger_reset_value) p._mode = 'set-reset' # PARAM2_DEPRECATION: Could be removed post param 2.0; use update() instead. def set_param(self_, *args,**kwargs): """ For each param=value keyword argument, sets the corresponding parameter of this object or class to the given value. For backwards compatibility, also accepts set_param("param",value) for a single parameter value using positional arguments, but the keyword interface is preferred because it is more compact and can set multiple values. """ self_or_cls = self_.self_or_cls if args: if len(args) == 2 and not args[0] in kwargs and not kwargs: kwargs[args[0]] = args[1] else: raise ValueError("Invalid positional arguments for %s.set_param" % (self_or_cls.name)) return self_.update(kwargs) def objects(self_, instance=True): """ Returns the Parameters of this instance or class If instance=True and called on a Parameterized instance it will create instance parameters for all Parameters defined on the class. To force class parameters to be returned use instance=False. Since classes avoid creating instance parameters unless necessary you may also request only existing instance parameters to be returned by setting instance='existing'. """ cls = self_.cls # We cache the parameters because this method is called often, # and parameters are rarely added (and cannot be deleted) try: pdict = getattr(cls, '_%s__params' % cls.__name__) except AttributeError: paramdict = {} for class_ in classlist(cls): for name, val in class_.__dict__.items(): if isinstance(val, Parameter): paramdict[name] = val # We only want the cache to be visible to the cls on which # params() is called, so we mangle the name ourselves at # runtime (if we were to mangle it now, it would be # _Parameterized.__params for all classes). setattr(cls, '_%s__params' % cls.__name__, paramdict) pdict = paramdict if instance and self_.self is not None: if instance == 'existing': if getattr(self_.self, 'initialized', False) and self_.self._instance__params: return dict(pdict, **self_.self._instance__params) return pdict else: return {k: self_.self.param[k] for k in pdict} return pdict def trigger(self_, *param_names): """ Trigger watchers for the given set of parameter names. Watchers will be triggered whether or not the parameter values have actually changed. As a special case, the value will actually be changed for a Parameter of type Event, setting it to True so that it is clear which Event parameter has been triggered. """ trigger_params = [p for p in self_.self_or_cls.param if hasattr(self_.self_or_cls.param[p], '_autotrigger_value')] triggers = {p:self_.self_or_cls.param[p]._autotrigger_value for p in trigger_params if p in param_names} events = self_.self_or_cls.param._events watchers = self_.self_or_cls.param._watchers self_.self_or_cls.param._events = [] self_.self_or_cls.param._watchers = [] param_values = self_.values() params = {name: param_values[name] for name in param_names} self_.self_or_cls.param._TRIGGER = True self_.set_param(**dict(params, **triggers)) self_.self_or_cls.param._TRIGGER = False self_.self_or_cls.param._events += events self_.self_or_cls.param._watchers += watchers def _update_event_type(self_, watcher, event, triggered): """ Returns an updated Event object with the type field set appropriately. """ if triggered: event_type = 'triggered' else: event_type = 'changed' if watcher.onlychanged else 'set' return Event(what=event.what, name=event.name, obj=event.obj, cls=event.cls, old=event.old, new=event.new, type=event_type) def _execute_watcher(self, watcher, events): if watcher.mode == 'args': args, kwargs = events, {} else: args, kwargs = (), {event.name: event.new for event in events} if iscoroutinefunction(watcher.fn): if async_executor is None: raise RuntimeError("Could not execute %s coroutine function. " "Please register a asynchronous executor on " "param.parameterized.async_executor, which " "schedules the function on an event loop." % watcher.fn) async_executor(partial(watcher.fn, *args, **kwargs)) else: watcher.fn(*args, **kwargs) def _call_watcher(self_, watcher, event): """ Invoke the given watcher appropriately given an Event object. """ if self_.self_or_cls.param._TRIGGER: pass elif watcher.onlychanged and (not self_._changed(event)): return if self_.self_or_cls.param._BATCH_WATCH: self_._events.append(event) if not any(watcher is w for w in self_._watchers): self_._watchers.append(watcher) else: event = self_._update_event_type(watcher, event, self_.self_or_cls.param._TRIGGER) with _batch_call_watchers(self_.self_or_cls, enable=watcher.queued, run=False): self_._execute_watcher(watcher, (event,)) def _batch_call_watchers(self_): """ Batch call a set of watchers based on the parameter value settings in kwargs using the queued Event and watcher objects. """ while self_.self_or_cls.param._events: event_dict = OrderedDict([((event.name, event.what), event) for event in self_.self_or_cls.param._events]) watchers = self_.self_or_cls.param._watchers[:] self_.self_or_cls.param._events = [] self_.self_or_cls.param._watchers = [] for watcher in sorted(watchers, key=lambda w: w.precedence): events = [self_._update_event_type(watcher, event_dict[(name, watcher.what)], self_.self_or_cls.param._TRIGGER) for name in watcher.parameter_names if (name, watcher.what) in event_dict] with _batch_call_watchers(self_.self_or_cls, enable=watcher.queued, run=False): self_._execute_watcher(watcher, events) def set_dynamic_time_fn(self_,time_fn,sublistattr=None): """ Set time_fn for all Dynamic Parameters of this class or instance object that are currently being dynamically generated. Additionally, sets _Dynamic_time_fn=time_fn on this class or instance object, so that any future changes to Dynamic Parmeters can inherit time_fn (e.g. if a Number is changed from a float to a number generator, the number generator will inherit time_fn). If specified, sublistattr is the name of an attribute of this class or instance that contains an iterable collection of subobjects on which set_dynamic_time_fn should be called. If the attribute sublistattr is present on any of the subobjects, set_dynamic_time_fn() will be called for those, too. """ self_or_cls = self_.self_or_cls self_or_cls._Dynamic_time_fn = time_fn if isinstance(self_or_cls,type): a = (None,self_or_cls) else: a = (self_or_cls,) for n,p in self_or_cls.param.objects('existing').items(): if hasattr(p, '_value_is_dynamic'): if p._value_is_dynamic(*a): g = self_or_cls.param.get_value_generator(n) g._Dynamic_time_fn = time_fn if sublistattr: try: sublist = getattr(self_or_cls,sublistattr) except AttributeError: sublist = [] for obj in sublist: obj.param.set_dynamic_time_fn(time_fn,sublistattr) def serialize_parameters(self_, subset=None, mode='json'): self_or_cls = self_.self_or_cls if mode not in Parameter._serializers: raise ValueError('Mode %r not in available serialization formats %r' % (mode, list(Parameter._serializers.keys()))) serializer = Parameter._serializers[mode] return serializer.serialize_parameters(self_or_cls, subset=subset) def serialize_value(self_, pname, mode='json'): self_or_cls = self_.self_or_cls if mode not in Parameter._serializers: raise ValueError('Mode %r not in available serialization formats %r' % (mode, list(Parameter._serializers.keys()))) serializer = Parameter._serializers[mode] return serializer.serialize_parameter_value(self_or_cls, pname) def deserialize_parameters(self_, serialization, subset=None, mode='json'): self_or_cls = self_.self_or_cls serializer = Parameter._serializers[mode] return serializer.deserialize_parameters(self_or_cls, serialization, subset=subset) def deserialize_value(self_, pname, value, mode='json'): self_or_cls = self_.self_or_cls if mode not in Parameter._serializers: raise ValueError('Mode %r not in available serialization formats %r' % (mode, list(Parameter._serializers.keys()))) serializer = Parameter._serializers[mode] return serializer.deserialize_parameter_value(self_or_cls, pname, value) def schema(self_, safe=False, subset=None, mode='json'): """ Returns a schema for the parameters on this Parameterized object. """ self_or_cls = self_.self_or_cls if mode not in Parameter._serializers: raise ValueError('Mode %r not in available serialization formats %r' % (mode, list(Parameter._serializers.keys()))) serializer = Parameter._serializers[mode] return serializer.schema(self_or_cls, safe=safe, subset=subset) # PARAM2_DEPRECATION: Could be removed post param 2.0; same as values() but returns list, not dict def get_param_values(self_, onlychanged=False): """ (Deprecated; use .values() instead.) Return a list of name,value pairs for all Parameters of this object. When called on an instance with onlychanged set to True, will only return values that are not equal to the default value (onlychanged has no effect when called on a class). """ self_or_cls = self_.self_or_cls vals = [] for name, val in self_or_cls.param.objects('existing').items(): value = self_or_cls.param.get_value_generator(name) if not onlychanged or not all_equal(value, val.default): vals.append((name, value)) vals.sort(key=itemgetter(0)) return vals def values(self_, onlychanged=False): """ Return a dictionary of name,value pairs for the Parameters of this object. When called on an instance with onlychanged set to True, will only return values that are not equal to the default value (onlychanged has no effect when called on a class). """ # Defined in terms of get_param_values() to avoid ordering # issues in python2, but can be inverted if get_param_values # is removed when python2 support is dropped return dict(self_.get_param_values(onlychanged)) def force_new_dynamic_value(self_, name): # pylint: disable-msg=E0213 """ Force a new value to be generated for the dynamic attribute name, and return it. If name is not dynamic, its current value is returned (i.e. equivalent to getattr(name). """ cls_or_slf = self_.self_or_cls param_obj = cls_or_slf.param.objects('existing').get(name) if not param_obj: return getattr(cls_or_slf, name) cls, slf = None, None if isinstance(cls_or_slf,type): cls = cls_or_slf else: slf = cls_or_slf if not hasattr(param_obj,'_force'): return param_obj.__get__(slf, cls) else: return param_obj._force(slf, cls) def get_value_generator(self_,name): # pylint: disable-msg=E0213 """ Return the value or value-generating object of the named attribute. For most parameters, this is simply the parameter's value (i.e. the same as getattr()), but Dynamic parameters have their value-generating object returned. """ cls_or_slf = self_.self_or_cls param_obj = cls_or_slf.param.objects('existing').get(name) if not param_obj: value = getattr(cls_or_slf,name) # CompositeParameter detected by being a Parameter and having 'attribs' elif hasattr(param_obj,'attribs'): value = [cls_or_slf.param.get_value_generator(a) for a in param_obj.attribs] # not a Dynamic Parameter elif not hasattr(param_obj,'_value_is_dynamic'): value = getattr(cls_or_slf,name) # Dynamic Parameter... else: internal_name = "_%s_param_value"%name if hasattr(cls_or_slf,internal_name): # dealing with object and it's been set on this object value = getattr(cls_or_slf,internal_name) else: # dealing with class or isn't set on the object value = param_obj.default return value def inspect_value(self_,name): # pylint: disable-msg=E0213 """ Return the current value of the named attribute without modifying it. Same as getattr() except for Dynamic parameters, which have their last generated value returned. """ cls_or_slf = self_.self_or_cls param_obj = cls_or_slf.param.objects('existing').get(name) if not param_obj: value = getattr(cls_or_slf,name) elif hasattr(param_obj,'attribs'): value = [cls_or_slf.param.inspect_value(a) for a in param_obj.attribs] elif not hasattr(param_obj,'_inspect'): value = getattr(cls_or_slf,name) else: if isinstance(cls_or_slf,type): value = param_obj._inspect(None,cls_or_slf) else: value = param_obj._inspect(cls_or_slf,None) return value def method_dependencies(self_, name, intermediate=False): """ Given the name of a method, returns a PInfo object for each dependency of this method. See help(PInfo) for the contents of these objects. By default intermediate dependencies on sub-objects are not returned as these are primarily useful for internal use to determine when a sub-object dependency has to be updated. """ method = getattr(self_.self_or_cls, name) minfo = MInfo(cls=self_.cls, inst=self_.self, name=name, method=method) deps, dynamic = _params_depended_on( minfo, dynamic=False, intermediate=intermediate) if self_.self is None: return deps return _resolve_mcs_deps( self_.self, deps, dynamic, intermediate=intermediate) # PARAM2_DEPRECATION: Backwards compatibilitity for param<1.12 params_depended_on = method_dependencies def outputs(self_): """ Returns a mapping between any declared outputs and a tuple of the declared Parameter type, the output method, and the index into the output if multiple outputs are returned. """ outputs = {} for cls in classlist(self_.cls): for name in dir(cls): method = getattr(self_.self_or_cls, name) dinfo = getattr(method, '_dinfo', {}) if 'outputs' not in dinfo: continue for override, otype, idx in dinfo['outputs']: if override is not None: name = override outputs[name] = (otype, method, idx) return outputs def _spec_to_obj(self_, spec, dynamic=True, intermediate=True): """ Resolves a dependency specification into lists of explicit parameter dependencies and dynamic dependencies. Dynamic dependencies are specifications to be resolved when the sub-object whose parameters are being depended on is defined. During class creation dynamic=False which means sub-object dependencies are not resolved. At instance creation and whenever a sub-object is set on an object this method will be invoked to determine whether the dependency is available. For sub-object dependencies we also return dependencies for every part of the path, e.g. for a dependency specification like "a.b.c" we return dependencies for sub-object "a" and the sub-sub-object "b" in addition to the dependency on the actual parameter "c" on object "b". This is to ensure that if a sub-object is swapped out we are notified and can update the dynamic dependency to the new object. Even if a sub-object dependency can only partially resolved, e.g. if object "a" does not yet have a sub-object "b" we must watch for changes to "b" on sub-object "a" in case such a subobject is put in "b". """ if isinstance(spec, Parameter): inst = spec.owner if isinstance(spec.owner, Parameterized) else None cls = spec.owner if inst is None else type(inst) info = PInfo(inst=inst, cls=cls, name=spec.name, pobj=spec, what='value') return [] if intermediate == 'only' else [info], [] obj, attr, what = _parse_dependency_spec(spec) if obj is None: src = self_.self_or_cls elif not dynamic: return [], [DInfo(spec=spec)] else: src = _getattrr(self_.self_or_cls, obj[1::], None) if src is None: path = obj[1:].split('.') deps = [] # Attempt to partially resolve subobject path to ensure # that if a subobject is later updated making the full # subobject path available we have to be notified and # set up watchers if len(path) >= 1 and intermediate: sub_src = None subpath = path while sub_src is None and subpath: subpath = subpath[:-1] sub_src = _getattrr(self_.self_or_cls, '.'.join(subpath), None) if subpath: subdeps, _ = self_._spec_to_obj( '.'.join(path[:len(subpath)+1]), dynamic, intermediate) deps += subdeps return deps, [] if intermediate == 'only' else [DInfo(spec=spec)] cls, inst = (src, None) if isinstance(src, type) else (type(src), src) if attr == 'param': deps, dynamic_deps = self_._spec_to_obj(obj[1:], dynamic, intermediate) for p in src.param: param_deps, param_dynamic_deps = src.param._spec_to_obj(p, dynamic, intermediate) deps += param_deps dynamic_deps += param_dynamic_deps return deps, dynamic_deps elif attr in src.param: info = PInfo(inst=inst, cls=cls, name=attr, pobj=src.param[attr], what=what) elif hasattr(src, attr): info = MInfo(inst=inst, cls=cls, name=attr, method=getattr(src, attr)) elif getattr(src, "abstract", None): return [], [] if intermediate == 'only' else [DInfo(spec=spec)] else: raise AttributeError("Attribute %r could not be resolved on %s." % (attr, src)) if obj is None or not intermediate: return [info], [] deps, dynamic_deps = self_._spec_to_obj(obj[1:], dynamic, intermediate) if intermediate != 'only': deps.append(info) return deps, dynamic_deps def _register_watcher(self_, action, watcher, what='value'): parameter_names = watcher.parameter_names for parameter_name in parameter_names: if parameter_name not in self_.cls.param: raise ValueError("%s parameter was not found in list of " "parameters of class %s" % (parameter_name, self_.cls.__name__)) if self_.self is not None and what == "value": watchers = self_.self._param_watchers if parameter_name not in watchers: watchers[parameter_name] = {} if what not in watchers[parameter_name]: watchers[parameter_name][what] = [] getattr(watchers[parameter_name][what], action)(watcher) else: watchers = self_[parameter_name].watchers if what not in watchers: watchers[what] = [] getattr(watchers[what], action)(watcher) def watch(self_, fn, parameter_names, what='value', onlychanged=True, queued=False, precedence=0): """ Register the given callback function `fn` to be invoked for events on the indicated parameters. `what`: What to watch on each parameter; either the value (by default) or else the indicated slot (e.g. 'constant'). `onlychanged`: By default, only invokes the function when the watched item changes, but if `onlychanged=False` also invokes it when the `what` item is set to its current value again. `queued`: By default, additional watcher events generated inside the callback fn are dispatched immediately, effectively doing depth-first processing of Watcher events. However, in certain scenarios, it is helpful to wait to dispatch such downstream events until all events that triggered this watcher have been processed. In such cases setting `queued=True` on this Watcher will queue up new downstream events generated during `fn` until `fn` completes and all other watchers invoked by that same event have finished executing), effectively doing breadth-first processing of Watcher events. `precedence`: Declares a precedence level for the Watcher that determines the priority with which the callback is executed. Lower precedence levels are executed earlier. Negative precedences are reserved for internal Watchers, i.e. those set up by param.depends. When the `fn` is called, it will be provided the relevant Event objects as positional arguments, which allows it to determine which of the possible triggering events occurred. Returns a Watcher object. See help(Watcher) and help(Event) for the contents of those objects. """ if precedence < 0: raise ValueError("User-defined watch callbacks must declare " "a positive precedence. Negative precedences " "are reserved for internal Watchers.") return self_._watch(fn, parameter_names, what, onlychanged, queued, precedence) def _watch(self_, fn, parameter_names, what='value', onlychanged=True, queued=False, precedence=-1): parameter_names = tuple(parameter_names) if isinstance(parameter_names, list) else (parameter_names,) watcher = Watcher(inst=self_.self, cls=self_.cls, fn=fn, mode='args', onlychanged=onlychanged, parameter_names=parameter_names, what=what, queued=queued, precedence=precedence) self_._register_watcher('append', watcher, what) return watcher def unwatch(self_, watcher): """ Remove the given Watcher object (from `watch` or `watch_values`) from this object's list. """ try: self_._register_watcher('remove', watcher, what=watcher.what) except Exception: self_.warning('No such watcher {watcher} to remove.'.format(watcher=str(watcher))) def watch_values(self_, fn, parameter_names, what='value', onlychanged=True, queued=False, precedence=0): """ Easier-to-use version of `watch` specific to watching for changes in parameter values. Only allows `what` to be 'value', and invokes the callback `fn` using keyword arguments = rather than with a list of Event objects. """ if precedence < 0: raise ValueError("User-defined watch callbacks must declare " "a positive precedence. Negative precedences " "are reserved for internal Watchers.") assert what == 'value' if isinstance(parameter_names, list): parameter_names = tuple(parameter_names) else: parameter_names = (parameter_names,) watcher = Watcher(inst=self_.self, cls=self_.cls, fn=fn, mode='kwargs', onlychanged=onlychanged, parameter_names=parameter_names, what=what, queued=queued, precedence=precedence) self_._register_watcher('append', watcher, what) return watcher # Instance methods # PARAM2_DEPRECATION: Backwards compatibilitity for param<1.12 def defaults(self_): """ Return {parameter_name:parameter.default} for all non-constant Parameters. Note that a Parameter for which instantiate==True has its default instantiated. """ self = self_.self d = {} for param_name, param in self.param.objects('existing').items(): if param.constant: pass if param.instantiate: self.param._instantiate_param(param, dict_=d, key=param_name) d[param_name] = param.default return d # Designed to avoid any processing unless the print # level is high enough, though not all callers of message(), # verbose(), debug(), etc are taking advantage of this. def __db_print(self_,level,msg,*args,**kw): """ Calls the logger returned by the get_logger() function, prepending the result of calling dbprint_prefix() (if any). See python's logging module for details. """ self_or_cls = self_.self_or_cls if get_logger(name=self_or_cls.name).isEnabledFor(level): if dbprint_prefix and callable(dbprint_prefix): msg = dbprint_prefix() + ": " + msg # pylint: disable-msg=E1102 get_logger(name=self_or_cls.name).log(level, msg, *args, **kw) # PARAM2_DEPRECATION: Backwards compatibilitity for param<1.12 def print_param_values(self_): """Print the values of all this object's Parameters.""" self = self_.self for name, val in self.param.values().items(): print('%s.%s = %s' % (self.name,name,val)) def warning(self_, msg,*args,**kw): """ Print msg merged with args as a warning, unless module variable warnings_as_exceptions is True, then raise an Exception containing the arguments. See Python's logging module for details of message formatting. """ self_.log(WARNING, msg, *args, **kw) # PARAM2_DEPRECATION: Could be removed post param 2.0 def message(self_,msg,*args,**kw): """ Print msg merged with args as a message. See Python's logging module for details of message formatting. """ self_.__db_print(INFO,msg,*args,**kw) # PARAM2_DEPRECATION: Could be removed post param 2.0 def verbose(self_,msg,*args,**kw): """ Print msg merged with args as a verbose message. See Python's logging module for details of message formatting. """ self_.__db_print(VERBOSE,msg,*args,**kw) # PARAM2_DEPRECATION: Could be removed post param 2.0 def debug(self_,msg,*args,**kw): """ Print msg merged with args as a debugging statement. See Python's logging module for details of message formatting. """ self_.__db_print(DEBUG,msg,*args,**kw) def log(self_, level, msg, *args, **kw): """ Print msg merged with args as a message at the indicated logging level. Logging levels include those provided by the Python logging module plus VERBOSE, either obtained directly from the logging module like `logging.INFO`, or from parameterized like `param.parameterized.INFO`. Supported logging levels include (in order of severity) DEBUG, VERBOSE, INFO, WARNING, ERROR, CRITICAL See Python's logging module for details of message formatting. """ if level is WARNING: if warnings_as_exceptions: raise Exception("Warning: " + msg % args) else: global warning_count warning_count+=1 self_.__db_print(level, msg, *args, **kw) def pprint(self_, imports=None, prefix=" ", unknown_value='', qualify=False, separator=""): """See Parameterized.pprint""" self = self_.self return self._pprint(imports, prefix, unknown_value, qualify, separator) class ParameterizedMetaclass(type): """ The metaclass of Parameterized (and all its descendents). The metaclass overrides type.__setattr__ to allow us to set Parameter values on classes without overwriting the attribute descriptor. That is, for a Parameterized class of type X with a Parameter y, the user can type X.y=3, which sets the default value of Parameter y to be 3, rather than overwriting y with the constant value 3 (and thereby losing all other info about that Parameter, such as the doc string, bounds, etc.). The __init__ method is used when defining a Parameterized class, usually when the module where that class is located is imported for the first time. That is, the __init__ in this metaclass initializes the *class* object, while the __init__ method defined in each Parameterized class is called for each new instance of that class. Additionally, a class can declare itself abstract by having an attribute __abstract set to True. The 'abstract' attribute can be used to find out if a class is abstract or not. """ def __init__(mcs, name, bases, dict_): """ Initialize the class object (not an instance of the class, but the class itself). Initializes all the Parameters by looking up appropriate default values (see __param_inheritance()) and setting attrib_names (see _set_names()). """ type.__init__(mcs, name, bases, dict_) # Give Parameterized classes a useful 'name' attribute. mcs.name = name mcs._parameters_state = { "BATCH_WATCH": False, # If true, Event and watcher objects are queued. "TRIGGER": False, "events": [], # Queue of batched events "watchers": [] # Queue of batched watchers } mcs._param = Parameters(mcs) # All objects (with their names) of type Parameter that are # defined in this class parameters = [(n, o) for (n, o) in dict_.items() if isinstance(o, Parameter)] mcs._param._parameters = dict(parameters) for param_name,param in parameters: mcs._initialize_parameter(param_name, param) # retrieve depends info from methods and store more conveniently dependers = [(n, m, m._dinfo) for (n, m) in dict_.items() if hasattr(m, '_dinfo')] # Resolve dependencies of current class _watch = [] for name, method, dinfo in dependers: watch = dinfo.get('watch', False) on_init = dinfo.get('on_init', False) if not watch: continue minfo = MInfo(cls=mcs, inst=None, name=name, method=method) deps, dynamic_deps = _params_depended_on(minfo, dynamic=False) _watch.append((name, watch == 'queued', on_init, deps, dynamic_deps)) # Resolve dependencies in class hierarchy _inherited = [] for cls in classlist(mcs)[:-1][::-1]: if not hasattr(cls, '_param'): continue for dep in cls.param._depends['watch']: method = getattr(mcs, dep[0], None) dinfo = getattr(method, '_dinfo', {'watch': False}) if (not any(dep[0] == w[0] for w in _watch+_inherited) and dinfo.get('watch')): _inherited.append(dep) mcs.param._depends = {'watch': _inherited+_watch} if docstring_signature: mcs.__class_docstring_signature() def __class_docstring_signature(mcs, max_repr_len=15): """ Autogenerate a keyword signature in the class docstring for all available parameters. This is particularly useful in the IPython Notebook as IPython will parse this signature to allow tab-completion of keywords. max_repr_len: Maximum length (in characters) of value reprs. """ processed_kws, keyword_groups = set(), [] for cls in reversed(mcs.mro()): keyword_group = [] for (k,v) in sorted(cls.__dict__.items()): if isinstance(v, Parameter) and k not in processed_kws: param_type = v.__class__.__name__ keyword_group.append("%s=%s" % (k, param_type)) processed_kws.add(k) keyword_groups.append(keyword_group) keywords = [el for grp in reversed(keyword_groups) for el in grp] class_docstr = "\n"+mcs.__doc__ if mcs.__doc__ else '' signature = "params(%s)" % (", ".join(keywords)) description = param_pager(mcs) if (docstring_describe_params and param_pager) else '' mcs.__doc__ = signature + class_docstr + '\n' + description def _initialize_parameter(mcs,param_name,param): # A Parameter has no way to find out the name a # Parameterized class has for it param._set_names(param_name) mcs.__param_inheritance(param_name,param) # Should use the official Python 2.6+ abstract base classes; see # https://github.com/holoviz/param/issues/84 def __is_abstract(mcs): """ Return True if the class has an attribute __abstract set to True. Subclasses will return False unless they themselves have __abstract set to true. This mechanism allows a class to declare itself to be abstract (e.g. to avoid it being offered as an option in a GUI), without the "abstract" property being inherited by its subclasses (at least one of which is presumably not abstract). """ # Can't just do ".__abstract", because that is mangled to # _ParameterizedMetaclass__abstract before running, but # the actual class object will have an attribute # _ClassName__abstract. So, we have to mangle it ourselves at # runtime. Mangling follows description in # https://docs.python.org/2/tutorial/classes.html#private-variables-and-class-local-references try: return getattr(mcs,'_%s__abstract'%mcs.__name__.lstrip("_")) except AttributeError: return False abstract = property(__is_abstract) def _get_param(mcs): return mcs._param param = property(_get_param) def __setattr__(mcs, attribute_name, value): """ Implements 'self.attribute_name=value' in a way that also supports Parameters. If there is already a descriptor named attribute_name, and that descriptor is a Parameter, and the new value is *not* a Parameter, then call that Parameter's __set__ method with the specified value. In all other cases set the attribute normally (i.e. overwrite the descriptor). If the new value is a Parameter, once it has been set we make sure that the value is inherited from Parameterized superclasses as described in __param_inheritance(). """ # Find out if there's a Parameter called attribute_name as a # class attribute of this class - if not, parameter is None. parameter,owning_class = mcs.get_param_descriptor(attribute_name) if parameter and not isinstance(value,Parameter): if owning_class != mcs: parameter = copy.copy(parameter) parameter.owner = mcs type.__setattr__(mcs,attribute_name,parameter) mcs.__dict__[attribute_name].__set__(None,value) else: type.__setattr__(mcs,attribute_name,value) if isinstance(value,Parameter): mcs.__param_inheritance(attribute_name,value) elif isinstance(value,Parameters): pass else: # the purpose of the warning below is to catch # mistakes ("thinking you are setting a parameter, but # you're not"). There are legitimate times when # something needs be set on the class, and we don't # want to see a warning then. Such attributes should # presumably be prefixed by at least one underscore. # (For instance, python's own pickling mechanism # caches __slotnames__ on the class: # http://mail.python.org/pipermail/python-checkins/2003-February/033517.html.) # This warning bypasses the usual mechanisms, which # has have consequences for warning counts, warnings # as exceptions, etc. if not attribute_name.startswith('_'): get_logger().log(WARNING, "Setting non-Parameter class attribute %s.%s = %s ", mcs.__name__,attribute_name,repr(value)) def __param_inheritance(mcs,param_name,param): """ Look for Parameter values in superclasses of this Parameterized class. Ordinarily, when a Python object is instantiated, attributes not given values in the constructor will inherit the value given in the object's class, or in its superclasses. For Parameters owned by Parameterized classes, we have implemented an additional level of default lookup, should this ordinary lookup return only None. In such a case, i.e. when no non-None value was found for a Parameter by the usual inheritance mechanisms, we explicitly look for Parameters with the same name in superclasses of this Parameterized class, and use the first such value that we find. The goal is to be able to set the default value (or other slots) of a Parameter within a Parameterized class, just as we can set values for non-Parameter objects in Parameterized classes, and have the values inherited through the Parameterized hierarchy as usual. Note that instantiate is handled differently: if there is a parameter with the same name in one of the superclasses with instantiate set to True, this parameter will inherit instantiate=True. """ # get all relevant slots (i.e. slots defined in all # superclasses of this parameter) slots = {} for p_class in classlist(type(param))[1::]: slots.update(dict.fromkeys(p_class.__slots__)) # note for some eventual future: python 3.6+ descriptors grew # __set_name__, which could replace this and _set_names setattr(param,'owner',mcs) del slots['owner'] # backwards compatibility (see Composite parameter) if 'objtype' in slots: setattr(param,'objtype',mcs) del slots['objtype'] # instantiate is handled specially for superclass in classlist(mcs)[::-1]: super_param = superclass.__dict__.get(param_name) if isinstance(super_param, Parameter) and super_param.instantiate is True: param.instantiate=True del slots['instantiate'] for slot in slots.keys(): superclasses = iter(classlist(mcs)[::-1]) # Search up the hierarchy until param.slot (which has to # be obtained using getattr(param,slot)) is not None, or # we run out of classes to search. while getattr(param,slot) is None: try: param_super_class = next(superclasses) except StopIteration: break new_param = param_super_class.__dict__.get(param_name) if new_param is not None and hasattr(new_param,slot): # (slot might not be there because could be a more # general type of Parameter) new_value = getattr(new_param,slot) setattr(param,slot,new_value) def get_param_descriptor(mcs,param_name): """ Goes up the class hierarchy (starting from the current class) looking for a Parameter class attribute param_name. As soon as one is found as a class attribute, that Parameter is returned along with the class in which it is declared. """ classes = classlist(mcs) for c in classes[::-1]: attribute = c.__dict__.get(param_name) if isinstance(attribute,Parameter): return attribute,c return None,None # Whether script_repr should avoid reporting the values of parameters # that are just inheriting their values from the class defaults. # Because deepcopying creates a new object, cannot detect such # inheritance when instantiate = True, so such values will be printed # even if they are just being copied from the default. script_repr_suppress_defaults=True def script_repr(val, imports=None, prefix="\n ", settings=[], qualify=True, unknown_value=None, separator="\n", show_imports=True): """ Variant of pprint() designed for generating a (nearly) runnable script. The output of script_repr(parameterized_obj) is meant to be a string suitable for running using `python file.py`. Not every object is guaranteed to have a runnable script_repr representation, but it is meant to be a good starting point for generating a Python script that (after minor edits) can be evaluated to get a newly initialized object similar to the one provided. The new object will only have the same parameter state, not the same internal (attribute) state; the script_repr captures only the state of the Parameters of that object and not any other attributes it may have. If show_imports is True (default), includes import statements for each of the modules required for the objects being instantiated. This list may not be complete, as it typically includes only the imports needed for the Parameterized object itself, not for values that may have been supplied to Parameters. Apart from show_imports, accepts the same arguments as pprint(), so see pprint() for explanations of the arguments accepted. The default values of each of these arguments differ from pprint() in ways that are more suitable for saving as a separate script than for e.g. pretty-printing at the Python prompt. """ if imports is None: imports = [] rep = pprint(val, imports, prefix, settings, unknown_value, qualify, separator) imports = list(set(imports)) imports_str = ("\n".join(imports) + "\n\n") if show_imports else "" return imports_str + rep # PARAM2_DEPRECATION: Remove entirely unused settings argument def pprint(val,imports=None, prefix="\n ", settings=[], unknown_value='', qualify=False, separator=''): """ Pretty printed representation of a parameterized object that may be evaluated with eval. Similar to repr except introspection of the constructor (__init__) ensures a valid and succinct representation is generated. Only parameters are represented (whether specified as standard, positional, or keyword arguments). Parameters specified as positional arguments are always shown, followed by modified parameters specified as keyword arguments, sorted by precedence. unknown_value determines what to do where a representation cannot be generated for something required to recreate the object. Such things include non-parameter positional and keyword arguments, and certain values of parameters (e.g. some random state objects). Supplying an unknown_value of None causes unrepresentable things to be silently ignored. If unknown_value is a string, that string will appear in place of any unrepresentable things. If unknown_value is False, an Exception will be raised if an unrepresentable value is encountered. If supplied, imports should be a list, and it will be populated with the set of imports required for the object and all of its parameter values. If qualify is True, the class's path will be included (e.g. "a.b.C()"), otherwise only the class will appear ("C()"). Parameters will be separated by a comma only by default, but the separator parameter allows an additional separator to be supplied (e.g. a newline could be supplied to have each Parameter appear on a separate line). Instances of types that require special handling can use the script_repr_reg dictionary. Using the type as a key, add a function that returns a suitable representation of instances of that type, and adds the required import statement. The repr of a parameter can be suppressed by returning None from the appropriate hook in script_repr_reg. """ if imports is None: imports = [] if isinstance(val,type): rep = type_script_repr(val,imports,prefix,settings) elif type(val) in script_repr_reg: rep = script_repr_reg[type(val)](val,imports,prefix,settings) elif hasattr(val,'_pprint'): rep=val._pprint(imports=imports, prefix=prefix+" ", qualify=qualify, unknown_value=unknown_value, separator=separator) else: rep=repr(val) return rep # Registry for special handling for certain types in script_repr and pprint script_repr_reg = {} # currently only handles list and tuple def container_script_repr(container,imports,prefix,settings): result=[] for i in container: result.append(pprint(i,imports,prefix,settings)) ## (hack to get container brackets) if isinstance(container,list): d1,d2='[',']' elif isinstance(container,tuple): d1,d2='(',')' else: raise NotImplementedError rep=d1+','.join(result)+d2 # no imports to add for built-in types return rep def empty_script_repr(*args): # pyflakes:ignore (unused arguments): return None try: # Suppress scriptrepr for objects not yet having a useful string representation import numpy script_repr_reg[random.Random] = empty_script_repr script_repr_reg[numpy.random.RandomState] = empty_script_repr except ImportError: pass # Support added only if those libraries are available def function_script_repr(fn,imports,prefix,settings): name = fn.__name__ module = fn.__module__ imports.append('import %s'%module) return module+'.'+name def type_script_repr(type_,imports,prefix,settings): module = type_.__module__ if module!='__builtin__': imports.append('import %s'%module) return module+'.'+type_.__name__ script_repr_reg[list]=container_script_repr script_repr_reg[tuple]=container_script_repr script_repr_reg[FunctionType]=function_script_repr #: If not None, the value of this Parameter will be called (using '()') #: before every call to __db_print, and is expected to evaluate to a #: string that is suitable for prefixing messages and warnings (such #: as some indicator of the global state). dbprint_prefix=None # Copy of Python 3.2 reprlib's recursive_repr but allowing extra arguments if sys.version_info.major >= 3: from threading import get_ident def recursive_repr(fillvalue='...'): 'Decorator to make a repr function return fillvalue for a recursive call' def decorating_function(user_function): repr_running = set() def wrapper(self, *args, **kwargs): key = id(self), get_ident() if key in repr_running: return fillvalue repr_running.add(key) try: result = user_function(self, *args, **kwargs) finally: repr_running.discard(key) return result return wrapper return decorating_function else: def recursive_repr(fillvalue='...'): def decorating_function(user_function): return user_function return decorating_function @add_metaclass(ParameterizedMetaclass) class Parameterized(object): """ Base class for named objects that support Parameters and message formatting. Automatic object naming: Every Parameterized instance has a name parameter. If the user doesn't designate a name= argument when constructing the object, the object will be given a name consisting of its class name followed by a unique 5-digit number. Automatic parameter setting: The Parameterized __init__ method will automatically read the list of keyword parameters. If any keyword matches the name of a Parameter (see Parameter class) defined in the object's class or any of its superclasses, that parameter in the instance will get the value given as a keyword argument. For example: class Foo(Parameterized): xx = Parameter(default=1) foo = Foo(xx=20) in this case foo.xx gets the value 20. When initializing a Parameterized instance ('foo' in the example above), the values of parameters can be supplied as keyword arguments to the constructor (using parametername=parametervalue); these values will override the class default values for this one instance. If no 'name' parameter is supplied, self.name defaults to the object's class name with a unique number appended to it. Message formatting: Each Parameterized instance has several methods for optionally printing output. This functionality is based on the standard Python 'logging' module; using the methods provided here, wraps calls to the 'logging' module's root logger and prepends each message with information about the instance from which the call was made. For more information on how to set the global logging level and change the default message prefix, see documentation for the 'logging' module. """ name = String(default=None, constant=True, doc=""" String identifier for this object.""") def __init__(self, **params): global object_count # Flag that can be tested to see if e.g. constant Parameters # can still be set self.initialized = False self._parameters_state = { "BATCH_WATCH": False, # If true, Event and watcher objects are queued. "TRIGGER": False, "events": [], # Queue of batched events "watchers": [] # Queue of batched watchers } self._instance__params = {} self._param_watchers = {} self._dynamic_watchers = defaultdict(list) self.param._generate_name() self.param._setup_params(**params) object_count += 1 self.param._update_deps(init=True) self.initialized = True @property def param(self): return Parameters(self.__class__, self=self) # 'Special' methods def __getstate__(self): """ Save the object's state: return a dictionary that is a shallow copy of the object's __dict__ and that also includes the object's __slots__ (if it has any). """ # Unclear why this is a copy and not simply state.update(self.__dict__) state = self.__dict__.copy() for slot in get_occupied_slots(self): state[slot] = getattr(self,slot) # Note that Parameterized object pickling assumes that # attributes to be saved are only in __dict__ or __slots__ # (the standard Python places to store attributes, so that's a # reasonable assumption). (Additionally, class attributes that # are Parameters are also handled, even when they haven't been # instantiated - see PickleableClassAttributes.) return state def __setstate__(self, state): """ Restore objects from the state dictionary to this object. During this process the object is considered uninitialized. """ self.initialized=False # When making a copy the internal watchers have to be # recreated and point to the new instance if '_param_watchers' in state: param_watchers = state['_param_watchers'] for p, attrs in param_watchers.items(): for attr, watchers in attrs.items(): new_watchers = [] for watcher in watchers: watcher_args = list(watcher) if watcher.inst is not None: watcher_args[0] = self fn = watcher.fn if hasattr(fn, '_watcher_name'): watcher_args[2] = _m_caller(self, fn._watcher_name) elif get_method_owner(fn) is watcher.inst: watcher_args[2] = getattr(self, fn.__name__) new_watchers.append(Watcher(*watcher_args)) param_watchers[p][attr] = new_watchers if '_instance__params' not in state: state['_instance__params'] = {} if '_param_watchers' not in state: state['_param_watchers'] = {} state.pop('param', None) for name,value in state.items(): setattr(self,name,value) self.initialized=True @recursive_repr() def __repr__(self): """ Provide a nearly valid Python representation that could be used to recreate the item with its parameters, if executed in the appropriate environment. Returns 'classname(parameter1=x,parameter2=y,...)', listing all the parameters of this object. """ try: settings = ['%s=%s' % (name, repr(val)) # PARAM2_DEPRECATION: Update to self.param.values.items() # (once python2 support is dropped) for name, val in self.param.get_param_values()] except RuntimeError: # Handle recursion in parameter depth settings = [] return self.__class__.__name__ + "(" + ", ".join(settings) + ")" def __str__(self): """Return a short representation of the name and class of this object.""" return "<%s %s>" % (self.__class__.__name__,self.name) # PARAM2_DEPRECATION: Remove this compatibility alias for param 2.0 and later; use self.param.pprint instead def script_repr(self,imports=[],prefix=" "): """ Deprecated variant of __repr__ designed for generating a runnable script. """ return self.pprint(imports,prefix, unknown_value=None, qualify=True, separator="\n") @recursive_repr() def _pprint(self, imports=None, prefix=" ", unknown_value='', qualify=False, separator=""): """ (Experimental) Pretty printed representation that may be evaluated with eval. See pprint() function for more details. """ if imports is None: imports = [] # would have been simpler to use a set from the start imports[:] = list(set(imports)) # Generate import statement mod = self.__module__ bits = mod.split('.') imports.append("import %s"%mod) imports.append("import %s"%bits[0]) changed_params = self.param.values(onlychanged=script_repr_suppress_defaults) values = self.param.values() spec = getfullargspec(self.__init__) args = spec.args[1:] if spec.args[0] == 'self' else spec.args if spec.defaults is not None: posargs = spec.args[:-len(spec.defaults)] kwargs = dict(zip(spec.args[-len(spec.defaults):], spec.defaults)) else: posargs, kwargs = args, [] parameters = self.param.objects('existing') ordering = sorted( sorted(changed_params), # alphanumeric tie-breaker key=lambda k: (- float('inf') # No precedence is lowest possible precendence if parameters[k].precedence is None else parameters[k].precedence)) arglist, keywords, processed = [], [], [] for k in args + ordering: if k in processed: continue # Suppresses automatically generated names. if k == 'name' and (values[k] is not None and re.match('^'+self.__class__.__name__+'[0-9]+$', values[k])): continue value = pprint(values[k], imports, prefix=prefix,settings=[], unknown_value=unknown_value, qualify=qualify) if k in values else None if value is None: if unknown_value is False: raise Exception("%s: unknown value of %r" % (self.name,k)) elif unknown_value is None: # i.e. suppress repr continue else: value = unknown_value # Explicit kwarg (unchanged, known value) if (k in kwargs) and (k in values) and kwargs[k] == values[k]: continue if k in posargs: # value will be unknown_value unless k is a parameter arglist.append(value) elif (k in kwargs or (hasattr(spec, 'varkw') and (spec.varkw is not None)) or (hasattr(spec, 'keywords') and (spec.keywords is not None))): # Explicit modified keywords or parameters in # precendence order (if **kwargs present) keywords.append('%s=%s' % (k, value)) processed.append(k) qualifier = mod + '.' if qualify else '' arguments = arglist + keywords + (['**%s' % spec.varargs] if spec.varargs else []) return qualifier + '%s(%s)' % (self.__class__.__name__, (','+separator+prefix).join(arguments)) # PARAM2_DEPRECATION: Backwards compatibilitity for param<1.12 pprint = _pprint # Note that there's no state_push method on the class, so # dynamic parameters set on a class can't have state saved. This # is because, to do this, state_push() would need to be a # @bothmethod, but that complicates inheritance in cases where we # already have a state_push() method. # (isinstance(g,Parameterized) below is used to exclude classes.) def state_push(self): """ Save this instance's state. For Parameterized instances, this includes the state of dynamically generated values. Subclasses that maintain short-term state should additionally save and restore that state using state_push() and state_pop(). Generally, this method is used by operations that need to test something without permanently altering the objects' state. """ for pname, p in self.param.objects('existing').items(): g = self.param.get_value_generator(pname) if hasattr(g,'_Dynamic_last'): g._saved_Dynamic_last.append(g._Dynamic_last) g._saved_Dynamic_time.append(g._Dynamic_time) # CB: not storing the time_fn: assuming that doesn't # change. elif hasattr(g,'state_push') and isinstance(g,Parameterized): g.state_push() def state_pop(self): """ Restore the most recently saved state. See state_push() for more details. """ for pname, p in self.param.objects('existing').items(): g = self.param.get_value_generator(pname) if hasattr(g,'_Dynamic_last'): g._Dynamic_last = g._saved_Dynamic_last.pop() g._Dynamic_time = g._saved_Dynamic_time.pop() elif hasattr(g,'state_pop') and isinstance(g,Parameterized): g.state_pop() # API to be accessed via param namespace @classmethod @Parameters.deprecate def _add_parameter(cls, param_name,param_obj): return cls.param._add_parameter(param_name,param_obj) @bothmethod @Parameters.deprecate def params(cls,parameter_name=None): return cls.param.params(parameter_name=parameter_name) @classmethod @Parameters.deprecate def set_default(cls,param_name,value): return cls.param.set_default(param_name,value) @classmethod @Parameters.deprecate def print_param_defaults(cls): return cls.param.print_param_defaults() @bothmethod @Parameters.deprecate def set_param(self_or_cls,*args,**kwargs): return self_or_cls.param.set_param(*args,**kwargs) @bothmethod @Parameters.deprecate def set_dynamic_time_fn(self_or_cls,time_fn,sublistattr=None): return self_or_cls.param.set_dynamic_time_fn(time_fn,sublistattr=sublistattr) @bothmethod @Parameters.deprecate def get_param_values(self_or_cls,onlychanged=False): return self_or_cls.param.get_param_values(onlychanged=onlychanged) @bothmethod @Parameters.deprecate def force_new_dynamic_value(cls_or_slf,name): # pylint: disable-msg=E0213 return cls_or_slf.param.force_new_dynamic_value(name) @bothmethod @Parameters.deprecate def get_value_generator(cls_or_slf,name): # pylint: disable-msg=E0213 return cls_or_slf.param.get_value_generator(name) @bothmethod @Parameters.deprecate def inspect_value(cls_or_slf,name): # pylint: disable-msg=E0213 return cls_or_slf.param.inspect_value(name) @Parameters.deprecate def _set_name(self,name): return self.param._set_name(name) @Parameters.deprecate def __db_print(self,level,msg,*args,**kw): return self.param.__db_print(level,msg,*args,**kw) @Parameters.deprecate def warning(self,msg,*args,**kw): return self.param.warning(msg,*args,**kw) @Parameters.deprecate def message(self,msg,*args,**kw): return self.param.message(msg,*args,**kw) @Parameters.deprecate def verbose(self,msg,*args,**kw): return self.param.verbose(msg,*args,**kw) @Parameters.deprecate def debug(self,msg,*args,**kw): return self.param.debug(msg,*args,**kw) @Parameters.deprecate def print_param_values(self): return self.param.print_param_values() @Parameters.deprecate def defaults(self): return self.param.defaults() def print_all_param_defaults(): """Print the default values for all imported Parameters.""" print("_______________________________________________________________________________") print("") print(" Parameter Default Values") print("") classes = descendents(Parameterized) classes.sort(key=lambda x:x.__name__) for c in classes: c.print_param_defaults() print("_______________________________________________________________________________") # As of Python 2.6+, a fn's **args no longer has to be a # dictionary. This might allow us to use a decorator to simplify using # ParamOverrides (if that does indeed make them simpler to use). # http://docs.python.org/whatsnew/2.6.html class ParamOverrides(dict): """ A dictionary that returns the attribute of a specified object if that attribute is not present in itself. Used to override the parameters of an object. """ # NOTE: Attribute names of this object block parameters of the # same name, so all attributes of this object should have names # starting with an underscore (_). def __init__(self,overridden,dict_,allow_extra_keywords=False): """ If allow_extra_keywords is False, then all keys in the supplied dict_ must match parameter names on the overridden object (otherwise a warning will be printed). If allow_extra_keywords is True, then any items in the supplied dict_ that are not also parameters of the overridden object will be available via the extra_keywords() method. """ # This method should be fast because it's going to be # called a lot. This _might_ be faster (not tested): # def __init__(self,overridden,**kw): # ... # dict.__init__(self,**kw) self._overridden = overridden dict.__init__(self,dict_) if allow_extra_keywords: self._extra_keywords=self._extract_extra_keywords(dict_) else: self._check_params(dict_) def extra_keywords(self): """ Return a dictionary containing items from the originally supplied `dict_` whose names are not parameters of the overridden object. """ return self._extra_keywords def param_keywords(self): """ Return a dictionary containing items from the originally supplied `dict_` whose names are parameters of the overridden object (i.e. not extra keywords/parameters). """ return dict((key, self[key]) for key in self if key not in self.extra_keywords()) def __missing__(self,name): # Return 'name' from the overridden object return getattr(self._overridden,name) def __repr__(self): # As dict.__repr__, but indicate the overridden object return dict.__repr__(self)+" overriding params from %s"%repr(self._overridden) def __getattr__(self,name): # Provide 'dot' access to entries in the dictionary. # (This __getattr__ method is called only if 'name' isn't an # attribute of self.) return self.__getitem__(name) def __setattr__(self,name,val): # Attributes whose name starts with _ are set on self (as # normal), but all other attributes are inserted into the # dictionary. if not name.startswith('_'): self.__setitem__(name,val) else: dict.__setattr__(self,name,val) def get(self, key, default=None): try: return self[key] except KeyError: return default def __contains__(self, key): return key in self.__dict__ or key in self._overridden.param def _check_params(self,params): """ Print a warning if params contains something that is not a Parameter of the overridden object. """ overridden_object_params = list(self._overridden.param) for item in params: if item not in overridden_object_params: self.param.warning("'%s' will be ignored (not a Parameter).",item) def _extract_extra_keywords(self,params): """ Return any items in params that are not also parameters of the overridden object. """ extra_keywords = {} overridden_object_params = list(self._overridden.param) for name, val in params.items(): if name not in overridden_object_params: extra_keywords[name]=val # Could remove name from params (i.e. del params[name]) # so that it's only available via extra_keywords() return extra_keywords # Helper function required by ParameterizedFunction.__reduce__ def _new_parameterized(cls): return Parameterized.__new__(cls) class ParameterizedFunction(Parameterized): """ Acts like a Python function, but with arguments that are Parameters. Implemented as a subclass of Parameterized that, when instantiated, automatically invokes __call__ and returns the result, instead of returning an instance of the class. To obtain an instance of this class, call instance(). """ __abstract = True def __str__(self): return self.__class__.__name__+"()" @bothmethod def instance(self_or_cls,**params): """ Return an instance of this class, copying parameters from any existing instance provided. """ if isinstance (self_or_cls,ParameterizedMetaclass): cls = self_or_cls else: p = params params = self_or_cls.param.values() params.update(p) params.pop('name') cls = self_or_cls.__class__ inst=Parameterized.__new__(cls) Parameterized.__init__(inst,**params) if 'name' in params: inst.__name__ = params['name'] else: inst.__name__ = self_or_cls.name return inst def __new__(class_,*args,**params): # Create and __call__() an instance of this class. inst = class_.instance() inst.param._set_name(class_.__name__) return inst.__call__(*args,**params) def __call__(self,*args,**kw): raise NotImplementedError("Subclasses must implement __call__.") def __reduce__(self): # Control reconstruction (during unpickling and copying): # ensure that ParameterizedFunction.__new__ is skipped state = ParameterizedFunction.__getstate__(self) # Here it's necessary to use a function defined at the # module level rather than Parameterized.__new__ directly # because otherwise pickle will find .__new__'s module to be # __main__. Pretty obscure aspect of pickle.py... return (_new_parameterized,(self.__class__,),state) # PARAM2_DEPRECATION: Remove this compatibility alias for param 2.0 and later; use self.param.pprint instead def script_repr(self,imports=[],prefix=" "): """ Same as Parameterized.script_repr, except that X.classname(Y is replaced with X.classname.instance(Y """ return self.pprint(imports,prefix,unknown_value='',qualify=True, separator="\n") def _pprint(self, imports=None, prefix="\n ",unknown_value='', qualify=False, separator=""): """ Same as Parameterized._pprint, except that X.classname(Y is replaced with X.classname.instance(Y """ r = Parameterized._pprint(self,imports,prefix, unknown_value=unknown_value, qualify=qualify,separator=separator) classname=self.__class__.__name__ return r.replace(".%s("%classname,".%s.instance("%classname) class default_label_formatter(ParameterizedFunction): "Default formatter to turn parameter names into appropriate widget labels." capitalize = Parameter(default=True, doc=""" Whether or not the label should be capitalized.""") replace_underscores = Parameter(default=True, doc=""" Whether or not underscores should be replaced with spaces.""") overrides = Parameter(default={}, doc=""" Allows custom labels to be specified for specific parameter names using a dictionary where key is the parameter name and the value is the desired label.""") def __call__(self, pname): if pname in self.overrides: return self.overrides[pname] if self.replace_underscores: pname = pname.replace('_',' ') if self.capitalize: pname = pname[:1].upper() + pname[1:] return pname label_formatter = default_label_formatter # PARAM2_DEPRECATION: Should be able to remove this; was originally # adapted from OProperty from # infinitesque.net/articles/2005/enhancing%20Python's%20property.xhtml # but since python 2.6 the getter, setter, and deleter attributes of # a property should provide similar functionality already. class overridable_property(object): """ The same as Python's "property" attribute, but allows the accessor methods to be overridden in subclasses. """ # Delays looking up the accessors until they're needed, rather # than finding them when the class is first created. # Based on the emulation of PyProperty_Type() in Objects/descrobject.c def __init__(self, fget=None, fset=None, fdel=None, doc=None): self.fget = fget self.fset = fset self.fdel = fdel self.__doc__ = doc def __get__(self, obj, objtype=None): if obj is None: return self if self.fget is None: raise AttributeError("unreadable attribute") if self.fget.__name__ == '' or not self.fget.__name__: return self.fget(obj) else: return getattr(obj, self.fget.__name__)() def __set__(self, obj, value): if self.fset is None: raise AttributeError("can't set attribute") if self.fset.__name__ == '' or not self.fset.__name__: self.fset(obj, value) else: getattr(obj, self.fset.__name__)(value) def __delete__(self, obj): if self.fdel is None: raise AttributeError("can't delete attribute") if self.fdel.__name__ == '' or not self.fdel.__name__: self.fdel(obj) else: getattr(obj, self.fdel.__name__)() param-1.12.3/param/serializer.py000066400000000000000000000264221434441564000165340ustar00rootroot00000000000000""" Classes used to support string serialization of Parameters and Parameterized objects. """ import json import textwrap class UnserializableException(Exception): pass class UnsafeserializableException(Exception): pass def JSONNullable(json_type): "Express a JSON schema type as nullable to easily support Parameters that allow_None" return {'anyOf': [ json_type, {'type': 'null'}] } class Serialization(object): """ Base class used to implement different types of serialization. """ @classmethod def schema(cls, pobj, subset=None): raise NotImplementedError # noqa: unimplemented method @classmethod def serialize_parameters(cls, pobj, subset=None): """ Serialize the parameters on a Parameterized object into a single serialized object, e.g. a JSON string. """ raise NotImplementedError # noqa: unimplemented method @classmethod def deserialize_parameters(cls, pobj, serialized, subset=None): """ Deserialize a serialized object representing one or more Parameters into a dictionary of parameter values. """ raise NotImplementedError # noqa: unimplemented method @classmethod def serialize_parameter_value(cls, pobj, pname): """ Serialize a single parameter value. """ raise NotImplementedError # noqa: unimplemented method @classmethod def deserialize_parameter_value(cls, pobj, pname, value): """ Deserialize a single parameter value. """ raise NotImplementedError # noqa: unimplemented method class JSONSerialization(Serialization): """ Class responsible for specifying JSON serialization, deserialization and JSON schemas for Parameters and Parameterized classes and objects. """ unserializable_parameter_types = ['Callable'] json_schema_literal_types = { int:'integer', float:'number', str:'string', type(None): 'null' } @classmethod def loads(cls, serialized): return json.loads(serialized) @classmethod def dumps(cls, obj): return json.dumps(obj) @classmethod def schema(cls, pobj, safe=False, subset=None): schema = {} for name, p in pobj.param.objects('existing').items(): if subset is not None and name not in subset: continue schema[name] = p.schema(safe=safe) if p.doc: schema[name]['description'] = textwrap.dedent(p.doc).replace('\n', ' ').strip() if p.label: schema[name]['title'] = p.label return schema @classmethod def serialize_parameters(cls, pobj, subset=None): components = {} for name, p in pobj.param.objects('existing').items(): if subset is not None and name not in subset: continue value = pobj.param.get_value_generator(name) components[name] = p.serialize(value) return cls.dumps(components) @classmethod def deserialize_parameters(cls, pobj, serialization, subset=None): deserialized = cls.loads(serialization) components = {} for name, value in deserialized.items(): if subset is not None and name not in subset: continue deserialized = pobj.param[name].deserialize(value) components[name] = deserialized return components # Parameter level methods @classmethod def _get_method(cls, ptype, suffix): "Returns specialized method if available, otherwise None" method_name = ptype.lower()+'_' + suffix return getattr(cls, method_name, None) @classmethod def param_schema(cls, ptype, p, safe=False, subset=None): if ptype in cls.unserializable_parameter_types: raise UnserializableException dispatch_method = cls._get_method(ptype, 'schema') if dispatch_method: schema = dispatch_method(p, safe=safe) else: schema = {'type': ptype.lower()} return JSONNullable(schema) if p.allow_None else schema @classmethod def serialize_parameter_value(cls, pobj, pname): value = pobj.param.get_value_generator(pname) return cls.dumps(pobj.param[pname].serialize(value)) @classmethod def deserialize_parameter_value(cls, pobj, pname, value): value = cls.loads(value) return pobj.param[pname].deserialize(value) # Custom Schemas @classmethod def class__schema(cls, class_, safe=False): from .parameterized import Parameterized if isinstance(class_, tuple): return {'anyOf': [cls.class__schema(cls_) for cls_ in class_]} elif class_ in cls.json_schema_literal_types: return {'type': cls.json_schema_literal_types[class_]} elif issubclass(class_, Parameterized): return {'type': 'object', 'properties': class_.param.schema(safe)} else: return {'type': 'object'} @classmethod def array_schema(cls, p, safe=False): if safe is True: msg = ('Array is not guaranteed to be safe for ' 'serialization as the dtype is unknown') raise UnsafeserializableException(msg) return {'type': 'array'} @classmethod def classselector_schema(cls, p, safe=False): return cls.class__schema(p.class_, safe=safe) @classmethod def dict_schema(cls, p, safe=False): if safe is True: msg = ('Dict is not guaranteed to be safe for ' 'serialization as the key and value types are unknown') raise UnsafeserializableException(msg) return {'type': 'object'} @classmethod def date_schema(cls, p, safe=False): return {'type': 'string', 'format': 'date-time'} @classmethod def calendardate_schema(cls, p, safe=False): return {'type': 'string', 'format': 'date'} @classmethod def tuple_schema(cls, p, safe=False): schema = {'type': 'array'} if p.length is not None: schema['minItems'] = p.length schema['maxItems'] = p.length return schema @classmethod def number_schema(cls, p, safe=False): schema = {'type': p.__class__.__name__.lower() } return cls.declare_numeric_bounds(schema, p.bounds, p.inclusive_bounds) @classmethod def declare_numeric_bounds(cls, schema, bounds, inclusive_bounds): "Given an applicable numeric schema, augment with bounds information" if bounds is not None: (low, high) = bounds if low is not None: key = 'minimum' if inclusive_bounds[0] else 'exclusiveMinimum' schema[key] = low if high is not None: key = 'maximum' if inclusive_bounds[1] else 'exclusiveMaximum' schema[key] = high return schema @classmethod def integer_schema(cls, p, safe=False): return cls.number_schema(p) @classmethod def numerictuple_schema(cls, p, safe=False): schema = cls.tuple_schema(p, safe=safe) schema['additionalItems'] = {'type': 'number'} return schema @classmethod def xycoordinates_schema(cls, p, safe=False): return cls.numerictuple_schema(p, safe=safe) @classmethod def range_schema(cls, p, safe=False): schema = cls.tuple_schema(p, safe=safe) bounded_number = cls.declare_numeric_bounds( {'type': 'number'}, p.bounds, p.inclusive_bounds) schema['additionalItems'] = bounded_number return schema @classmethod def list_schema(cls, p, safe=False): schema = {'type': 'array'} if safe is True and p.item_type is None: msg = ('List without a class specified cannot be guaranteed ' 'to be safe for serialization') raise UnsafeserializableException(msg) if p.class_ is not None: schema['items'] = cls.class__schema(p.item_type, safe=safe) return schema @classmethod def objectselector_schema(cls, p, safe=False): try: allowed_types = [{'type': cls.json_schema_literal_types[type(obj)]} for obj in p.objects] schema = {'anyOf': allowed_types} schema['enum'] = p.objects return schema except: if safe is True: msg = ('ObjectSelector cannot be guaranteed to be safe for ' 'serialization due to unserializable type in objects') raise UnsafeserializableException(msg) return {} @classmethod def selector_schema(cls, p, safe=False): try: allowed_types = [{'type': cls.json_schema_literal_types[type(obj)]} for obj in p.objects.values()] schema = {'anyOf': allowed_types} schema['enum'] = p.objects return schema except: if safe is True: msg = ('Selector cannot be guaranteed to be safe for ' 'serialization due to unserializable type in objects') raise UnsafeserializableException(msg) return {} @classmethod def listselector_schema(cls, p, safe=False): if p.objects is None: if safe is True: msg = ('ListSelector cannot be guaranteed to be safe for ' 'serialization as allowed objects unspecified') return {'type': 'array'} for obj in p.objects: if type(obj) not in cls.json_schema_literal_types: msg = 'ListSelector cannot serialize type %s' % type(obj) raise UnserializableException(msg) return {'type': 'array', 'items': {'enum': p.objects}} @classmethod def dataframe_schema(cls, p, safe=False): schema = {'type': 'array'} if safe is True: msg = ('DataFrame is not guaranteed to be safe for ' 'serialization as the column dtypes are unknown') raise UnsafeserializableException(msg) if p.columns is None: schema['items'] = {'type': 'object'} return schema mincols, maxcols = None, None if isinstance(p.columns, int): mincols, maxcols = p.columns, p.columns elif isinstance(p.columns, tuple): mincols, maxcols = p.columns if isinstance(p.columns, int) or isinstance(p.columns, tuple): schema['items'] = {'type': 'object', 'minItems': mincols, 'maxItems': maxcols} if isinstance(p.columns, list) or isinstance(p.columns, set): literal_types = [{'type':el} for el in cls.json_schema_literal_types.values()] allowable_types = {'anyOf': literal_types} properties = {name: allowable_types for name in p.columns} schema['items'] = {'type': 'object', 'properties': properties} minrows, maxrows = None, None if isinstance(p.rows, int): minrows, maxrows = p.rows, p.rows elif isinstance(p.rows, tuple): minrows, maxrows = p.rows if minrows is not None: schema['minItems'] = minrows if maxrows is not None: schema['maxItems'] = maxrows return schema param-1.12.3/param/version.py000066400000000000000000000744221434441564000160530ustar00rootroot00000000000000""" Provide consistent and up-to-date ``__version__`` strings for Python packages. See https://github.com/holoviz/autover for more information. """ # The Version class is a copy of autover.version.Version v0.2.5, # except as noted below. # # The current version of autover supports a workflow based on tagging # a git repository, and reports PEP440 compliant version information. # Previously, the workflow required editing of version numbers in # source code, and the version was not necessarily PEP440 compliant. # Version.__new__ is added here to provide the previous Version class # (OldDeprecatedVersion) if Version is called in the old way. __author__ = 'Jean-Luc Stevens' import os, subprocess, json def run_cmd(args, cwd=None): proc = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=cwd) output, error = (str(s.decode()).strip() for s in proc.communicate()) # Detects errors as _either_ a non-zero return code _or_ messages # printed to stderr, because the return code is erroneously fixed at # zero in some cases (see https://github.com/holoviz/param/pull/389). if proc.returncode != 0 or len(error) > 0: raise Exception(proc.returncode, error) return output class Version(object): """ A simple approach to Python package versioning that supports PyPI releases and additional information when working with version control. When obtaining a package from PyPI, the version returned is a string-formatted rendering of the supplied release tuple. For instance, release (1,0) tagged as ``v1.0`` in the version control system will return ``1.0`` for ``str(__version__)``. Any number of items can be supplied in the release tuple, with either two or three numeric versioning levels typical. During development, a command like ``git describe`` will be used to compute the number of commits since the last version tag, the short commit hash, and whether the commit is dirty (has changes not yet committed). Version tags must start with a lowercase 'v' and have a period in them, e.g. v2.0, v0.9.8 or v0.1 and may include the PEP440 prerelease identifiers of 'a' (alpha) 'b' (beta) or 'rc' (release candidate) allowing tags such as v2.0.a3, v0.9.8.b3 or v0.1.rc5. Also note that when version control system (VCS) information is used, the number of commits since the last version tag is determined. This approach is often useful in practice to decide which version is newer for a single developer, but will not necessarily be reliable when comparing against a different fork or branch in a distributed VCS. For git, if you want version control information available even in an exported archive (e.g. a .zip file from GitHub), you can set the following line in the .gitattributes file of your project:: __init__.py export-subst Note that to support pip installation directly from GitHub via git archive, a .version file must be tracked by the repo to supply the release number (otherwise only the short SHA is available). The PEP440 format returned is [N!]N(.N)*[{a|b|rc}N][.postN+SHA] where everything before .postN is obtained from the tag, the N in .postN is the number of commits since the last tag and the SHA is obtained via git describe. This later portion is only shown if the commit count since the last tag is non zero. Instead of '.post', an alternate valid prefix such as '.rev', '_rev', '_r' or '.r' may be supplied.""" def __new__(cls,**kw): # If called in the old way, provide the previous class. Means # PEP440/tag based workflow warning below will never appear. if ('release' in kw and kw['release'] is not None) or \ ('dev' in kw and kw['dev'] is not None) or \ ('commit_count' in kw): return OldDeprecatedVersion(**kw) else: return super(Version, cls).__new__(cls) def __init__(self, release=None, fpath=None, commit=None, reponame=None, commit_count_prefix='.post', archive_commit=None, **kwargs): """ :release: Release tuple (corresponding to the current VCS tag) :commit Short SHA. Set to '$Format:%h$' for git archive support. :fpath: Set to ``__file__`` to access version control information :reponame: Used to verify VCS repository name. """ self.fpath = fpath self._expected_commit = commit if release is not None or 'commit_count' in kwargs: print('WARNING: param.Version now supports PEP440 and a new tag based workflow. See param/version.py for more details') self.expected_release = release self._commit = None if (commit is None or commit.startswith("$Format")) else commit self._commit_count = None self._release = None self._dirty = False self._prerelease = None self.archive_commit= archive_commit self.reponame = reponame self.commit_count_prefix = commit_count_prefix @property def prerelease(self): """ Either None or one of 'aN' (alpha), 'bN' (beta) or 'rcN' (release candidate) where N is an integer. """ return self.fetch()._prerelease @property def release(self): "Return the release tuple" return self.fetch()._release @property def commit(self): "A specification for this particular VCS version, e.g. a short git SHA" return self.fetch()._commit @property def commit_count(self): "Return the number of commits since the last release" return self.fetch()._commit_count @property def dirty(self): "True if there are uncommited changes, False otherwise" return self.fetch()._dirty def fetch(self): """ Returns a tuple of the major version together with the appropriate SHA and dirty bit (for development version only). """ if self._release is not None: return self self._release = self.expected_release if not self.fpath: self._commit = self._expected_commit return self # Only git right now but easily extended to SVN, Mercurial, etc. for cmd in ['git', 'git.cmd', 'git.exe']: try: self.git_fetch(cmd) break except EnvironmentError: pass return self def git_fetch(self, cmd='git', as_string=False): commit_argument = self._commit output = None try: if self.reponame is not None: # Verify this is the correct repository (since fpath could # be an unrelated git repository, and autover could just have # been copied/installed into it). remotes = run_cmd([cmd, 'remote', '-v'], cwd=os.path.dirname(self.fpath)) repo_matches = ['/' + self.reponame + '.git' , # A remote 'server:reponame.git' can also be referred # to (i.e. cloned) as `server:reponame`. '/' + self.reponame + ' '] if not any(m in remotes for m in repo_matches): try: output = self._output_from_file() if output is not None: self._update_from_vcs(output) except: pass if output is None: # glob pattern (not regexp) matching vX.Y.Z* tags output = run_cmd([cmd, 'describe', '--long', '--match', "v[0-9]*.[0-9]*.[0-9]*", '--dirty'], cwd=os.path.dirname(self.fpath)) if as_string: return output except Exception as e1: try: output = self._output_from_file() if output is not None: self._update_from_vcs(output) if self._known_stale(): self._commit_count = None if as_string: return output # If an explicit commit was supplied (e.g from git # archive), it should take precedence over the file. if commit_argument: self._commit = commit_argument return except IOError: if e1.args[1] == 'fatal: No names found, cannot describe anything.': raise Exception("Cannot find any git version tags of format v*.*") # If there is any other error, return (release value still useful) return self self._update_from_vcs(output) def _known_stale(self): """ The commit is known to be from a file (and therefore stale) if a SHA is supplied by git archive and doesn't match the parsed commit. """ if self._output_from_file() is None: commit = None else: commit = self.commit known_stale = (self.archive_commit is not None and not self.archive_commit.startswith('$Format') and self.archive_commit != commit) if known_stale: self._commit_count = None return known_stale def _output_from_file(self, entry='git_describe'): """ Read the version from a .version file that may exist alongside __init__.py. This file can be generated by piping the following output to file: git describe --long --match v*.* """ try: vfile = os.path.join(os.path.dirname(self.fpath), '.version') with open(vfile, 'r') as f: return json.loads(f.read()).get(entry, None) except: # File may be missing if using pip + git archive return None def _update_from_vcs(self, output): "Update state based on the VCS state e.g the output of git describe" split = output[1:].split('-') dot_split = split[0].split('.') for prefix in ['a','b','rc']: if prefix in dot_split[-1]: prefix_split = dot_split[-1].split(prefix) self._prerelease = prefix + prefix_split[-1] dot_split[-1] = prefix_split[0] self._release = tuple(int(el) for el in dot_split) self._commit_count = int(split[1]) self._commit = str(split[2][1:]) # Strip out 'g' prefix ('g'=>'git') self._dirty = (split[-1]=='dirty') return self def __str__(self): """ Version in x.y.z string format. Does not include the "v" prefix of the VCS version tags, for pip compatibility. If the commit count is non-zero or the repository is dirty, the string representation is equivalent to the output of:: git describe --long --match v*.* --dirty (with "v" prefix removed). """ known_stale = self._known_stale() if self.release is None and not known_stale: extracted_directory_tag = self._output_from_file(entry='extracted_directory_tag') return 'None' if extracted_directory_tag is None else extracted_directory_tag elif self.release is None and known_stale: extracted_directory_tag = self._output_from_file(entry='extracted_directory_tag') if extracted_directory_tag is not None: return extracted_directory_tag return '0.0.0+g{SHA}-gitarchive'.format(SHA=self.archive_commit) release = '.'.join(str(el) for el in self.release) prerelease = '' if self.prerelease is None else self.prerelease if self.commit_count == 0 and not self.dirty: return release + prerelease commit = self.commit dirty = '-dirty' if self.dirty else '' archive_commit = '' if known_stale: archive_commit = '-gitarchive' commit = self.archive_commit if archive_commit != '': postcount = self.commit_count_prefix + '0' elif self.commit_count not in [0, None]: postcount = self.commit_count_prefix + str(self.commit_count) else: postcount = '' components = [release, prerelease, postcount, '' if commit is None else '+g' + commit, dirty, archive_commit] return ''.join(components) def __repr__(self): return str(self) def abbrev(self): """ Abbreviated string representation of just the release number. """ return '.'.join(str(el) for el in self.release) def verify(self, string_version=None): """ Check that the version information is consistent with the VCS before doing a release. If supplied with a string version, this is also checked against the current version. Should be called from setup.py with the declared package version before releasing to PyPI. """ if string_version and string_version != str(self): raise Exception("Supplied string version does not match current version.") if self.dirty: raise Exception("Current working directory is dirty.") if self.expected_release is not None and self.release != self.expected_release: raise Exception("Declared release does not match current release tag.") if self.commit_count !=0: raise Exception("Please update the VCS version tag before release.") if (self._expected_commit is not None and not self._expected_commit.startswith( "$Format")): raise Exception("Declared release does not match the VCS version tag") @classmethod def get_setup_version(cls, setup_path, reponame, describe=False, dirty='report', pkgname=None, archive_commit=None): """ Helper for use in setup.py to get the version from the .version file (if available) or more up-to-date information from git describe (if available). Assumes the __init__.py will be found in the directory {reponame}/__init__.py relative to setup.py unless pkgname is explicitly specified in which case that name is used instead. If describe is True, the raw string obtained from git described is returned which is useful for updating the .version file. The dirty policy can be one of 'report', 'strip', 'raise'. If it is 'report' the version string may end in '-dirty' if the repository is in a dirty state. If the policy is 'strip', the '-dirty' suffix will be stripped out if present. If the policy is 'raise', an exception is raised if the repository is in a dirty state. This can be useful if you want to make sure packages are not built from a dirty repository state. """ pkgname = reponame if pkgname is None else pkgname policies = ['raise','report', 'strip'] if dirty not in policies: raise AssertionError("get_setup_version dirty policy must be in %r" % policies) fpath = os.path.join(setup_path, pkgname, "__init__.py") version = Version(fpath=fpath, reponame=reponame, archive_commit=archive_commit) if describe: vstring = version.git_fetch(as_string=True) else: vstring = str(version) if version.dirty and dirty == 'raise': raise AssertionError('Repository is in a dirty state.') elif version.dirty and dirty=='strip': return vstring.replace('-dirty', '') else: return vstring @classmethod def extract_directory_tag(cls, setup_path, reponame): setup_dir = os.path.split(setup_path)[-1] # Directory containing setup.py prefix = reponame + '-' # Prefix to match if setup_dir.startswith(prefix): tag = setup_dir[len(prefix):] # Assuming the tag is a version if it isn't empty, 'master' and has a dot in it if tag not in ['', 'master'] and ('.' in tag): return tag return None @classmethod def setup_version(cls, setup_path, reponame, archive_commit=None, pkgname=None, dirty='report'): info = {} git_describe = None pkgname = reponame if pkgname is None else pkgname try: # Will only work if in a git repo and git is available git_describe = Version.get_setup_version(setup_path, reponame, describe=True, dirty=dirty, pkgname=pkgname, archive_commit=archive_commit) if git_describe is not None: info['git_describe'] = git_describe except: pass if git_describe is None: extracted_directory_tag = Version.extract_directory_tag(setup_path, reponame) if extracted_directory_tag is not None: info['extracted_directory_tag'] = extracted_directory_tag try: with open(os.path.join(setup_path, pkgname, '.version'), 'w') as f: f.write(json.dumps({'extracted_directory_tag':extracted_directory_tag})) except: print('Error in setup_version: could not write .version file.') info['version_string'] = Version.get_setup_version(setup_path, reponame, describe=False, dirty=dirty, pkgname=pkgname, archive_commit=archive_commit) try: with open(os.path.join(setup_path, pkgname, '.version'), 'w') as f: f.write(json.dumps(info)) except: print('Error in setup_version: could not write .version file.') return info['version_string'] def get_setup_version(location, reponame, pkgname=None, archive_commit=None): """Helper for use in setup.py to get the current version from either git describe or the .version file (if available). Set pkgname to the package name if it is different from the repository name. To ensure git information is included in a git archive, add setup.py to .gitattributes (in addition to __init__): ``` __init__.py export-subst setup.py export-subst ``` Then supply "$Format:%h$" for archive_commit. """ import warnings pkgname = reponame if pkgname is None else pkgname if archive_commit is None: warnings.warn("No archive commit available; git archives will not contain version information") return Version.setup_version(os.path.dirname(os.path.abspath(location)),reponame,pkgname=pkgname,archive_commit=archive_commit) def get_setupcfg_version(): """As get_setup_version(), but configure via setup.cfg. If your project uses setup.cfg to configure setuptools, and hence has at least a "name" key in the [metadata] section, you can set the version as follows: ``` [metadata] name = mypackage version = attr: autover.version.get_setup_version2 ``` If the repository name is different from the package name, specify `reponame` as a [tool:autover] option: ``` [tool:autover] reponame = mypackage ``` To ensure git information is included in a git archive, add setup.cfg to .gitattributes (in addition to __init__): ``` __init__.py export-subst setup.cfg export-subst ``` Then add the following to setup.cfg: ``` [tool:autover.configparser_workaround.archive_commit=$Format:%h$] ``` The above being a section heading rather than just a key is because setuptools requires % to be escaped with %, or it can't parse setup.cfg...but then git export-subst would not work. """ try: import configparser except ImportError: import ConfigParser as configparser # python2 (also prevents dict-like access) import re cfg = "setup.cfg" autover_section = 'tool:autover' config = configparser.ConfigParser() config.read(cfg) pkgname = config.get('metadata','name') reponame = config.get(autover_section,'reponame',vars={'reponame':pkgname}) if autover_section in config.sections() else pkgname ### # hack archive_commit into section heading; see docstring archive_commit = None archive_commit_key = autover_section+'.configparser_workaround.archive_commit' for section in config.sections(): if section.startswith(archive_commit_key): archive_commit = re.match(r".*=\s*(\S*)\s*",section).group(1) ### return get_setup_version(cfg,reponame=reponame,pkgname=pkgname,archive_commit=archive_commit) # from param/version.py aa087db29976d9b7e0f59c29789dfd721c85afd0 class OldDeprecatedVersion(object): """ A simple approach to Python package versioning that supports PyPI releases and additional information when working with version control. When obtaining a package from PyPI, the version returned is a string-formatted rendering of the supplied release tuple. For instance, release (1,0) tagged as ``v1.0`` in the version control system will return ``1.0`` for ``str(__version__)``. Any number of items can be supplied in the release tuple, with either two or three numeric versioning levels typical. During development, a command like ``git describe`` will be used to compute the number of commits since the last version tag, the short commit hash, and whether the commit is dirty (has changes not yet committed). Version tags must start with a lowercase 'v' and have a period in them, e.g. v2.0, v0.9.8 or v0.1. Development versions are supported by setting the dev argument to an appropriate dev version number. The corresponding tag can be PEP440 compliant (using .devX) of the form v0.1.dev3, v1.9.0.dev2 etc but it doesn't have to be as the dot may be omitted i.e v0.1dev3, v1.9.0dev2 etc. Also note that when version control system (VCS) information is used, the comparison operators take into account the number of commits since the last version tag. This approach is often useful in practice to decide which version is newer for a single developer, but will not necessarily be reliable when comparing against a different fork or branch in a distributed VCS. For git, if you want version control information available even in an exported archive (e.g. a .zip file from GitHub), you can set the following line in the .gitattributes file of your project:: __init__.py export-subst """ def __init__(self, release=None, fpath=None, commit=None, reponame=None, dev=None, commit_count=0): """ :release: Release tuple (corresponding to the current VCS tag) :commit Short SHA. Set to '$Format:%h$' for git archive support. :fpath: Set to ``__file__`` to access version control information :reponame: Used to verify VCS repository name. :dev: Development version number. None if not a development version. :commit_count Commits since last release. Set for dev releases. """ self.fpath = fpath self._expected_commit = commit self.expected_release = release self._commit = None if commit in [None, "$Format:%h$"] else commit self._commit_count = commit_count self._release = None self._dirty = False self.reponame = reponame self.dev = dev @property def release(self): "Return the release tuple" return self.fetch()._release @property def commit(self): "A specification for this particular VCS version, e.g. a short git SHA" return self.fetch()._commit @property def commit_count(self): "Return the number of commits since the last release" return self.fetch()._commit_count @property def dirty(self): "True if there are uncommited changes, False otherwise" return self.fetch()._dirty def fetch(self): """ Returns a tuple of the major version together with the appropriate SHA and dirty bit (for development version only). """ if self._release is not None: return self self._release = self.expected_release if not self.fpath: self._commit = self._expected_commit return self # Only git right now but easily extended to SVN, Mercurial, etc. for cmd in ['git', 'git.cmd', 'git.exe']: try: self.git_fetch(cmd) break except EnvironmentError: pass return self def git_fetch(self, cmd='git'): try: if self.reponame is not None: # Verify this is the correct repository (since fpath could # be an unrelated git repository, and param could just have # been copied/installed into it). output = run_cmd([cmd, 'remote', '-v'], cwd=os.path.dirname(self.fpath)) repo_matches = ['/' + self.reponame + '.git' , # A remote 'server:reponame.git' can also be referred # to (i.e. cloned) as `server:reponame`. '/' + self.reponame + ' '] if not any(m in output for m in repo_matches): return self output = run_cmd([cmd, 'describe', '--long', '--match', 'v*.*', '--dirty'], cwd=os.path.dirname(self.fpath)) except Exception as e: if e.args[1] == 'fatal: No names found, cannot describe anything.': raise Exception("Cannot find any git version tags of format v*.*") # If there is any other error, return (release value still useful) return self self._update_from_vcs(output) def _update_from_vcs(self, output): "Update state based on the VCS state e.g the output of git describe" split = output[1:].split('-') if 'dev' in split[0]: dev_split = split[0].split('dev') self.dev = int(dev_split[1]) split[0] = dev_split[0] # Remove the pep440 dot if present if split[0].endswith('.'): split[0] = dev_split[0][:-1] self._release = tuple(int(el) for el in split[0].split('.')) self._commit_count = int(split[1]) self._commit = str(split[2][1:]) # Strip out 'g' prefix ('g'=>'git') self._dirty = (split[-1]=='dirty') return self def __str__(self): """ Version in x.y.z string format. Does not include the "v" prefix of the VCS version tags, for pip compatibility. If the commit count is non-zero or the repository is dirty, the string representation is equivalent to the output of:: git describe --long --match v*.* --dirty (with "v" prefix removed). """ if self.release is None: return 'None' release = '.'.join(str(el) for el in self.release) release = '%s.dev%d' % (release, self.dev) if self.dev is not None else release if (self._expected_commit is not None) and ("$Format" not in self._expected_commit): pass # Concrete commit supplied - print full version string elif (self.commit_count == 0 and not self.dirty): return release dirty_status = '-dirty' if self.dirty else '' return '%s-%s-g%s%s' % (release, self.commit_count if self.commit_count else 'x', self.commit, dirty_status) def __repr__(self): return str(self) def abbrev(self,dev_suffix=""): """ Abbreviated string representation, optionally declaring whether it is a development version. """ return '.'.join(str(el) for el in self.release) + \ (dev_suffix if self.commit_count > 0 or self.dirty else "") def __eq__(self, other): """ Two versions are considered equivalent if and only if they are from the same release, with the same commit count, and are not dirty. Any dirty version is considered different from any other version, since it could potentially have any arbitrary changes even for the same release and commit count. """ if self.dirty or other.dirty: return False return ((self.release, self.commit_count, self.dev) == (other.release, other.commit_count, other.dev)) def __gt__(self, other): if self.release == other.release: if self.dev == other.dev: return self.commit_count > other.commit_count elif None in [self.dev, other.dev]: return self.dev is None else: return self.dev > other.dev else: return (self.release, self.commit_count) > (other.release, other.commit_count) def __lt__(self, other): if self==other: return False else: return not (self > other) def verify(self, string_version=None): """ Check that the version information is consistent with the VCS before doing a release. If supplied with a string version, this is also checked against the current version. Should be called from setup.py with the declared package version before releasing to PyPI. """ if string_version and string_version != str(self): raise Exception("Supplied string version does not match current version.") if self.dirty: raise Exception("Current working directory is dirty.") if self.release != self.expected_release: raise Exception("Declared release does not match current release tag.") if self.commit_count !=0: raise Exception("Please update the VCS version tag before release.") if self._expected_commit not in [None, "$Format:%h$"]: raise Exception("Declared release does not match the VCS version tag") param-1.12.3/setup.cfg000066400000000000000000000010461434441564000145250ustar00rootroot00000000000000[wheel] universal = 1 [flake8] # TODO tests should not be excluded (one day...) include = setup.py param numbergen exclude = .git,__pycache__,.tox,.eggs,*.egg,doc,dist,build,_build,tests,.ipynb_checkpoints,run_test.py ignore = E114, E116, E126, E128, E129, E2, E3, E4, E5, E731, E701, E702, E703, E704, E722, E741, E742, E743, W503, W504, [tool:pytest] python_files = test*.py param-1.12.3/setup.py000066400000000000000000000061521434441564000144210ustar00rootroot00000000000000import os from setuptools import setup ########## autover ########## def get_setup_version(reponame): """Use autover to get up to date version.""" # importing self into setup.py is unorthodox, but param has no # required dependencies outside of python from param.version import Version return Version.setup_version(os.path.dirname(__file__),reponame,archive_commit="$Format:%h$") ########## dependencies ########## extras_require = { # pip doesn't support tests_require # (https://github.com/pypa/pip/issues/1197) 'tests': [ 'pytest', 'pytest-cov', 'flake8', ], 'doc': [ 'pygraphviz', 'nbsite ==0.8.0rc2', 'pydata-sphinx-theme <0.9.0', 'myst-parser', 'nbconvert', 'graphviz', 'myst_nb ==0.12.2', 'sphinx-copybutton', 'aiohttp', 'panel', 'pandas', # Temporar pin due to https://github.com/ipython/ipython/issues/13845 'ipython !=8.7.0', ] } extras_require['all'] = sorted(set(sum(extras_require.values(), []))) ########## metadata for setuptools ########## setup_args = dict( name='param', version=get_setup_version("param"), description='Make your Python code clearer and more reliable by declaring Parameters.', long_description=open('README.md').read() if os.path.isfile('README.md') else 'Consult README.md', long_description_content_type="text/markdown", author="HoloViz", author_email="developers@holoviz.org", maintainer="HoloViz", maintainer_email="developers@holoviz.org", platforms=['Windows', 'Mac OS X', 'Linux'], license='BSD', url='http://param.holoviz.org/', packages=["param","numbergen"], provides=["param","numbergen"], include_package_data = True, python_requires=">=2.7", install_requires=[], extras_require=extras_require, tests_require=extras_require['tests'], project_urls={ "Documentation": "https://param.holoviz.org/", "Releases": "https://github.com/holoviz/param/releases", "Bug Tracker": "https://github.com/holoviz/param/issues", "Source Code": "https://github.com/holoviz/param", "Panel Examples": "https://panel.holoviz.org/user_guide/Param.html", }, classifiers=[ "License :: OSI Approved :: BSD License", "Development Status :: 5 - Production/Stable", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Operating System :: OS Independent", "Intended Audience :: Science/Research", "Intended Audience :: Developers", "Natural Language :: English", "Topic :: Scientific/Engineering", "Topic :: Software Development :: Libraries"] ) if __name__=="__main__": setup(**setup_args) param-1.12.3/tests/000077500000000000000000000000001434441564000140455ustar00rootroot00000000000000param-1.12.3/tests/API0/000077500000000000000000000000001434441564000145365ustar00rootroot00000000000000param-1.12.3/tests/API0/__init__.py000066400000000000000000000000001434441564000166350ustar00rootroot00000000000000param-1.12.3/tests/API0/testcalendardateparam.py000066400000000000000000000036471434441564000214520ustar00rootroot00000000000000""" Unit test for CalendarDate parameters. """ import unittest import pytest import datetime as dt import param class TestDateTimeParameters(unittest.TestCase): def test_initialization_out_of_bounds(self): try: class Q(param.Parameterized): q = param.Date(dt.date(2017,2,27), bounds=(dt.date(2017,2,1), dt.date(2017,2,26))) except ValueError: pass else: raise AssertionError("No exception raised on out-of-bounds date") def test_set_out_of_bounds(self): class Q(param.Parameterized): q = param.Date(bounds=(dt.date(2017,2,1), dt.date(2017,2,26))) try: Q.q = dt.date(2017,2,27) except ValueError: pass else: raise AssertionError("No exception raised on out-of-bounds date") def test_set_exclusive_out_of_bounds(self): class Q(param.Parameterized): q = param.Date(bounds=(dt.date(2017,2,1), dt.date(2017,2,26)), inclusive_bounds=(True, False)) try: Q.q = dt.date(2017,2,26) except ValueError: pass else: raise AssertionError("No exception raised on out-of-bounds date") def test_get_soft_bounds(self): q = param.Date(dt.date(2017,2,25), bounds=(dt.date(2017,2,1), dt.date(2017,2,26)), softbounds=(dt.date(2017,2,1), dt.date(2017,2,25))) self.assertEqual(q.get_soft_bounds(), (dt.date(2017,2,1), dt.date(2017,2,25))) def test_datetime_not_accepted(self): with pytest.raises(ValueError): param.CalendarDate(dt.datetime(2021, 8, 16, 10)) param-1.12.3/tests/API0/testcalendardaterangeparam.py000066400000000000000000000040041434441564000224530ustar00rootroot00000000000000""" Unit tests for CalendarDateRange parameter. """ import unittest import datetime as dt import param # Assuming tests of range parameter cover most of what's needed to # test date range. class TestDateTimeRange(unittest.TestCase): bad_range = (dt.date(2017,2,27),dt.date(2017,2,26)) def test_wrong_type_default(self): try: class Q(param.Parameterized): a = param.CalendarDateRange(default=(1.0,2.0)) except ValueError: pass else: raise AssertionError("Bad date type was accepted.") def test_wrong_type_init(self): class Q(param.Parameterized): a = param.CalendarDateRange() try: Q(a=self.bad_range) except ValueError: pass else: raise AssertionError("Bad date type was accepted.") def test_wrong_type_set(self): class Q(param.Parameterized): a = param.CalendarDateRange() q = Q() try: q.a = self.bad_range except ValueError: pass else: raise AssertionError("Bad date type was accepted.") def test_start_before_end_default(self): try: class Q(param.Parameterized): a = param.CalendarDateRange(default=self.bad_range) except ValueError: pass else: raise AssertionError("Bad date range was accepted.") def test_start_before_end_init(self): class Q(param.Parameterized): a = param.CalendarDateRange() try: Q(a=self.bad_range) except ValueError: pass else: raise AssertionError("Bad date range was accepted.") def test_start_before_end_set(self): class Q(param.Parameterized): a = param.CalendarDateRange() q = Q() try: q.a = self.bad_range except ValueError: pass else: raise AssertionError("Bad date range was accepted.") param-1.12.3/tests/API0/testclassselector.py000066400000000000000000000043541434441564000206640ustar00rootroot00000000000000""" Unit test for ClassSelector parameters. """ import unittest from numbers import Number import param if not hasattr(unittest.TestCase, 'assertRaisesRegex'): unittest.TestCase.assertRaisesRegex = unittest.TestCase.assertRaisesRegexp class TestClassSelectorParameters(unittest.TestCase): def setUp(self): class P(param.Parameterized): e = param.ClassSelector(default=1,class_=int) f = param.ClassSelector(default=int,class_=Number, is_instance=False) g = param.ClassSelector(default=1,class_=(int,str)) h = param.ClassSelector(default=int,class_=(int,str), is_instance=False) self.P = P def test_single_class_instance_constructor(self): p = self.P(e=6) self.assertEqual(p.e, 6) def test_single_class_instance_error(self): exception = "ClassSelector parameter 'e' value must be an instance of int, not 'a'." with self.assertRaisesRegex(ValueError, exception): self.P(e='a') def test_single_class_type_constructor(self): p = self.P(f=float) self.assertEqual(p.f, float) def test_single_class_type_error(self): exception = "ClassSelector parameter 'f' must be a subclass of Number, not 'str'." with self.assertRaisesRegex(ValueError, exception): self.P(f=str) def test_multiple_class_instance_constructor1(self): p = self.P(g=1) self.assertEqual(p.g, 1) def test_multiple_class_instance_constructor2(self): p = self.P(g='A') self.assertEqual(p.g, 'A') def test_multiple_class_instance_error(self): exception = r"ClassSelector parameter 'g' value must be an instance of \(int, str\), not 3.0." with self.assertRaisesRegex(ValueError, exception): self.P(g=3.0) def test_multiple_class_type_constructor1(self): p = self.P(h=int) self.assertEqual(p.h, int) def test_multiple_class_type_constructor2(self): p = self.P(h=str) self.assertEqual(p.h, str) def test_multiple_class_type_error(self): exception = r"ClassSelector parameter 'h' must be a subclass of \(int, str\), not 'float'." with self.assertRaisesRegex(ValueError, exception): self.P(h=float) param-1.12.3/tests/API0/testcolorparameter.py000066400000000000000000000034251434441564000210330ustar00rootroot00000000000000""" Unit test for Color parameters. """ import unittest import param class TestColorParameters(unittest.TestCase): def test_initialization_invalid_string(self): try: class Q(param.Parameterized): q = param.Color('red', allow_named=False) except ValueError: pass else: raise AssertionError("No exception raised on invalid color") def test_set_invalid_string(self): class Q(param.Parameterized): q = param.Color(allow_named=False) try: Q.q = 'red' except ValueError: pass else: raise AssertionError("No exception raised on invalid color") def test_set_invalid_named_color(self): class Q(param.Parameterized): q = param.Color(allow_named=True) try: Q.q = 'razzmatazz' except ValueError: pass else: raise AssertionError("No exception raised on invalid color") def test_invalid_long_hex(self): class Q(param.Parameterized): q = param.Color() try: Q.q = '#gfffff' except ValueError: pass else: raise AssertionError("No exception raised on invalid color") def test_valid_long_hex(self): class Q(param.Parameterized): q = param.Color() Q.q = '#ffffff' self.assertEqual(Q.q, '#ffffff') def test_valid_short_hex(self): class Q(param.Parameterized): q = param.Color() Q.q = '#fff' self.assertEqual(Q.q, '#fff') def test_valid_named_color(self): class Q(param.Parameterized): q = param.Color(allow_named=True) Q.q = 'indianred' self.assertEqual(Q.q, 'indianred') param-1.12.3/tests/API0/testcompositeparams.py000066400000000000000000000053211434441564000212170ustar00rootroot00000000000000""" Unit test for composite parameters. Originally implemented as doctests in Topographica in the file testCompositeParameter.txt """ import unittest import param class TestCompositeParameters(unittest.TestCase): def setUp(self): # initialize a class with a compound parameter class A(param.Parameterized): x = param.Number(default=0) y = param.Number(default=0) xy = param.Composite(attribs=['x','y']) self.A = A self.a = self.A() class SomeSequence(object): "Can't use iter with Dynamic (doesn't pickle, doesn't copy)" def __init__(self,sequence): self.sequence=sequence self.index=0 def __call__(self): val=self.sequence[self.index] self.index+=1 return val self.SomeSequence = SomeSequence def test_initialization(self): "Make an instance and do default checks" self.assertEqual(self.a.x, 0) self.assertEqual(self.a.y, 0) self.assertEqual(self.a.xy, [0,0]) def test_set_component(self): self.a.x = 1 self.assertEqual(self.a.xy, [1,0]) def test_set_compound(self): self.a.xy = (2,3) self.assertEqual(self.a.x, 2) self.assertEqual(self.a.y, 3) def test_compound_class(self): " Get the compound on the class " self.assertEqual(self.A.xy, [0,0]) def test_set_compound_class_set(self): self.A.xy = (5,6) self.assertEqual(self.A.x, 5) self.assertEqual(self.A.y, 6) def test_set_compound_class_instance(self): self.A.xy = (5,6) # # Make a new instance b = self.A() self.assertEqual(b.x, 5) self.assertEqual(b.y, 6) def test_set_compound_class_instance_unchanged(self): self.a.xy = (2,3) self.A.xy = (5,6) self.assertEqual(self.a.x, 2) self.assertEqual(self.a.y, 3) def test_composite_dynamic(self): """ Check CompositeParameter is ok with Dynamic CB: this test is really of Parameterized. """ a2 = self.A(x=self.SomeSequence([1,2,3]), y=self.SomeSequence([4,5,6])) a2.x, a2.y # Call of x and y params # inspect should not advance numbers self.assertEqual(a2.inspect_value('xy'), [1, 4]) def test_composite_dynamic_generator(self): a2 = self.A(x=self.SomeSequence([1,2,3]), y=self.SomeSequence([4,5,6])) a2.x, a2.y # Call of x and y params ix,iy = a2.get_value_generator('xy') # get_value_generator() should give the objects self.assertEqual(ix(), 2) self.assertEqual(iy(), 5) param-1.12.3/tests/API0/testdateparam.py000066400000000000000000000034761434441564000177600ustar00rootroot00000000000000""" Unit test for Date parameters. """ import unittest import datetime as dt import param class TestDateParameters(unittest.TestCase): def test_initialization_out_of_bounds(self): try: class Q(param.Parameterized): q = param.Date(dt.datetime(2017,2,27), bounds=(dt.datetime(2017,2,1), dt.datetime(2017,2,26))) except ValueError: pass else: raise AssertionError("No exception raised on out-of-bounds date") def test_set_out_of_bounds(self): class Q(param.Parameterized): q = param.Date(bounds=(dt.datetime(2017,2,1), dt.datetime(2017,2,26))) try: Q.q = dt.datetime(2017,2,27) except ValueError: pass else: raise AssertionError("No exception raised on out-of-bounds date") def test_set_exclusive_out_of_bounds(self): class Q(param.Parameterized): q = param.Date(bounds=(dt.datetime(2017,2,1), dt.datetime(2017,2,26)), inclusive_bounds=(True, False)) try: Q.q = dt.datetime(2017,2,26) except ValueError: pass else: raise AssertionError("No exception raised on out-of-bounds date") def test_get_soft_bounds(self): q = param.Date(dt.datetime(2017,2,25), bounds=(dt.datetime(2017,2,1), dt.datetime(2017,2,26)), softbounds=(dt.datetime(2017,2,1), dt.datetime(2017,2,25))) self.assertEqual(q.get_soft_bounds(), (dt.datetime(2017,2,1), dt.datetime(2017,2,25))) param-1.12.3/tests/API0/testdaterangeparam.py000066400000000000000000000037221434441564000207670ustar00rootroot00000000000000""" Unit tests for DateRange parameter. """ import unittest import datetime as dt import param # Assuming tests of range parameter cover most of what's needed to # test date range. class TestDateRange(unittest.TestCase): bad_range = (dt.datetime(2017,2,27),dt.datetime(2017,2,26)) def test_wrong_type_default(self): try: class Q(param.Parameterized): a = param.DateRange(default=(1.0,2.0)) except ValueError: pass else: raise AssertionError("Bad date type was accepted.") def test_wrong_type_init(self): class Q(param.Parameterized): a = param.DateRange() try: Q(a=self.bad_range) except ValueError: pass else: raise AssertionError("Bad date type was accepted.") def test_wrong_type_set(self): class Q(param.Parameterized): a = param.DateRange() q = Q() try: q.a = self.bad_range except ValueError: pass else: raise AssertionError("Bad date type was accepted.") def test_start_before_end_default(self): try: class Q(param.Parameterized): a = param.DateRange(default=self.bad_range) except ValueError: pass else: raise AssertionError("Bad date range was accepted.") def test_start_before_end_init(self): class Q(param.Parameterized): a = param.DateRange() try: Q(a=self.bad_range) except ValueError: pass else: raise AssertionError("Bad date range was accepted.") def test_start_before_end_set(self): class Q(param.Parameterized): a = param.DateRange() q = Q() try: q.a = self.bad_range except ValueError: pass else: raise AssertionError("Bad date range was accepted.") param-1.12.3/tests/API0/testdefaults.py000066400000000000000000000023221434441564000176160ustar00rootroot00000000000000""" Do all subclasses of Parameter supply a valid default? """ import unittest import pytest from param.parameterized import add_metaclass from param import concrete_descendents, Parameter # import all parameter types from param import ClassSelector from param import * # noqa positional_args = { # ClassSelector: (object,) } skip = ['ClassSelector'] try: import numpy # noqa except ImportError: skip.append('Array') try: import pandas # noqa except ImportError: skip.append('DataFrame') skip.append('Series') class DefaultsMetaclassTest(type): def __new__(mcs, name, bases, dict_): def test_skip(*args,**kw): pytest.skip() def add_test(p): def test(self): # instantiate parameter with no default (but supply # any required args) p(*positional_args.get(p,tuple())) return test for p_name, p_type in concrete_descendents(Parameter).items(): dict_["test_default_of_%s"%p_name] = add_test(p_type) if p_name not in skip else test_skip return type.__new__(mcs, name, bases, dict_) @add_metaclass(DefaultsMetaclassTest) class TestDefaults(unittest.TestCase): pass param-1.12.3/tests/API0/testdynamicparams.py000066400000000000000000000213501434441564000206410ustar00rootroot00000000000000""" Unit test for dynamic parameters. Tests __get__, __set__ and that inspect_value() and get_value_generator() work. Originally implemented as doctests in Topographica in the file testDynamicParameter.txt """ import copy import unittest import param import numbergen class TestDynamicParameters(unittest.TestCase): def setUp(self): param.Dynamic.time_dependent = False class TestPO1(param.Parameterized): x = param.Dynamic(default=numbergen.UniformRandom(lbound=-1,ubound=1,seed=1),doc="nothing") y = param.Dynamic(default=1) class TestPO2(param.Parameterized): x = param.Dynamic(default=numbergen.UniformRandom(lbound=-1,ubound=1,seed=30)) y = param.Dynamic(default=1.0) self.TestPO2 = TestPO2 self.TestPO1 = TestPO1 self.t1 = self.TestPO1() self.t2 = self.TestPO1(x=numbergen.UniformRandom(lbound=-1,ubound=1,seed=10)) self.t3 = self.TestPO1(x=numbergen.UniformRandom(lbound=-1,ubound=1,seed=10)) self.t2.set_dynamic_time_fn(None) self.t3.set_dynamic_time_fn(None) self.t6 = self.TestPO2() self.t7 = self.TestPO2() class TestDynamicParameterBasics(TestDynamicParameters): def test_set_dynamic_time_fn_x(self): self.t1.set_dynamic_time_fn(None) self.assertEqual( self.t1.params()['x']._value_is_dynamic(self.t1), True) def test_set_dynamic_time_fn_y(self): self.assertEqual( self.t1.params()['y']._value_is_dynamic(self.t1), False) def test_inspect_x(self): "no value generated yet" self.assertEqual(self.t1.inspect_value('x'), None) def test_inspect_y(self): self.assertEqual(self.t1.inspect_value('y'), 1) def test_inspect_y_set(self): self.t1.y = 2 self.assertEqual(self.t1.inspect_value('y'), 2) def test_set_dynamic_numbergen(self): is_numbergen = isinstance(self.t2.get_value_generator('x'), numbergen.UniformRandom) self.assertEqual(is_numbergen, True) def test_matching_numbergen_streams(self): "check that t2 and t3 have identical streams" self.assertEqual(self.t2.x, self.t3.x) def test_numbergen_objects_distinct(self): "check t2 and t3 do not share UniformRandom objects" self.t2.x self.assertNotEqual(self.t2.inspect_value('x'), self.t3.inspect_value('x')) def test_numbergen_inspect(self): " inspect_value() should return last generated value " self.t2.x # Call 1 self.t2.x # Call 2 t2_last_value = self.t2.x # advance t2 beyond t3 self.assertEqual(self.t2.inspect_value('x'), t2_last_value) # ensure last_value is not shared self.assertNotEqual(self.t3.inspect_value('x'), t2_last_value) def test_dynamic_value_instantiated(self): t6_first_value = self.t6.x self.assertNotEqual(self.t7.inspect_value('x'), t6_first_value) def test_non_dynamic_value_not_instantiated(self): " non-dynamic value not instantiated" self.TestPO2.y = 4 self.assertEqual(self.t6.y, 4) self.assertEqual(self.t7.y, 4) def test_dynamic_value_setting(self): self.t6.y = numbergen.UniformRandom() t8 = self.TestPO2() self.TestPO2.y = 10 # t6 got a dynamic value, but shouldn't have changed Parameter's instantiate self.assertEqual(t8.y, 10) def test_setting_y_param_numbergen(self): self.TestPO2.y=numbergen.UniformRandom() # now the Parameter instantiate should be true t9 = self.TestPO2() self.assertEqual('_y_param_value' in t9.__dict__, True) def test_shared_numbergen(self): """ Instances of TestPO2 that don't have their own value for the parameter share one UniformRandom object """ self.TestPO2.y=numbergen.UniformRandom() # now the Parameter instantiate should be true self.assertEqual(self.t7.get_value_generator('y') is self.TestPO2().params()['y'].default, True) self.assertEqual(self.TestPO2().params()['y'].default.__class__.__name__, 'UniformRandom') def test_copy_match(self): "check a copy is the same" t9 = copy.deepcopy(self.t7) self.assertEqual(t9.get_value_generator('y') is self.TestPO2().params()['y'].default, True) class TestDynamicTimeDependent(TestDynamicParameters): def setUp(self): super(TestDynamicTimeDependent, self).setUp() param.Dynamic.time_dependent = True class TestPO3(param.Parameterized): x = param.Dynamic(default=numbergen.UniformRandom(name='xgen', time_dependent=True)) class TestPO4(self.TestPO1): "Nested parameterized objects" z = param.Parameter(default=self.TestPO1()) self.TestPO3 = TestPO3 self.TestPO4 = TestPO4 self.t10 = self.TestPO1() self.t11 = TestPO3() def test_dynamic_values_unchanged_dependent(self): param.Dynamic.time_dependent = True call_1 = self.t10.x call_2 = self.t10.x call_3 = self.t10.x self.assertEqual(call_1, call_2) self.assertEqual(call_2, call_3) def test_dynamic_values_changed_independent(self): param.Dynamic.time_dependent = False call_1 = self.t10.x call_2 = self.t10.x call_3 = self.t10.x self.assertNotEqual(call_1, call_2) self.assertNotEqual(call_2, call_3) def test_dynamic_values_change(self): param.Dynamic.time_dependent = True with param.Dynamic.time_fn as t: t(0) call_1 = self.t10.x t += 1 call_2 = self.t10.x t(0) call_3 = self.t10.x self.assertNotEqual(call_1, call_2) self.assertNotEqual(call_1, call_3) def test_dynamic_values_time_dependent(self): param.Dynamic.time_dependent = True with param.Dynamic.time_fn as t: t(0) call_1 = self.t11.x t += 1 call_2 = self.t11.x t(0) call_3 = self.t11.x self.assertNotEqual(call_1, call_2) self.assertEqual(call_1, call_3) def test_class_dynamic_values_change(self): call_1 = self.TestPO3.x call_2 = self.TestPO3.x self.assertEqual(call_1, call_2) with param.Dynamic.time_fn as t: t += 1 call_3 = self.TestPO3.x self.assertNotEqual(call_2, call_3) def test_dynamic_value_change_independent(self): t12 = self.TestPO1() t12.set_dynamic_time_fn(None) self.assertNotEqual(t12.x, t12.x) self.assertEqual(t12.y, t12.y) def test_dynamic_value_change_disabled(self): " time_fn set on the UniformRandom() when t13.y was set" t13 = self.TestPO1() t13.set_dynamic_time_fn(None) t13.y = numbergen.UniformRandom() self.assertNotEqual(t13.y, t13.y) def test_dynamic_value_change_enabled(self): " time_fn set on the UniformRandom() when t13.y was set" t14 = self.TestPO1() t14.y = numbergen.UniformRandom() self.assertEqual(t14.y, t14.y) def test_dynamic_time_fn_not_inherited(self): " time_fn not inherited" t15 = self.TestPO4() t15.set_dynamic_time_fn(None) with param.Dynamic.time_fn as t: call_1 = t15.z.x t += 1 call_2 = t15.z.x self.assertNotEqual(call_1, call_2) class TestDynamicSharedNumbergen(TestDynamicParameters): "Check shared generator" def setUp(self): super(TestDynamicSharedNumbergen, self).setUp() self.shared = numbergen.UniformRandom(lbound=-1,ubound=1,seed=20) def test_dynamic_shared_numbergen(self): param.Dynamic.time_dependent = True t11 = self.TestPO1(x=self.shared) t12 = self.TestPO1(x=self.shared) with param.Dynamic.time_fn as t: t += 1 call_1 = t11.x self.assertEqual(call_1, t12.x) t += 1 self.assertNotEqual(call_1, t12.x) # Commented out block in the original doctest version. # Maybe these are features originally planned but never implemented """ It is not yet possible to set time_fn for a Parameter instance >>> class TestPO5(param.Parameterized): ... x = param.Dynamic(default=numbergen.UniformRandom(),dynamic_time_fn=None) """ """ We currently don't support iterators/generators in Dynamic unless they're wrapped. >>> i = iter([1,2,3]) >>> t11.x = i >>> topo.sim.run(1) >>> t11.x 1 >>> def gen(): ... yield 2 ... yield 4 ... yield 6 >>> g = gen() >>> t11.x = g >>> t11.x 2 >>> topo.sim.run(1) >>> t11.x 4 """ param-1.12.3/tests/API0/testipythonmagic.py000066400000000000000000000076511434441564000205140ustar00rootroot00000000000000""" Unit test for the IPython magic """ import re import sys import unittest import param try: import IPython # noqa except ImportError: import os if os.getenv('PARAM_TEST_IPYTHON','0') == '1': raise ImportError("PARAM_TEST_IPYTHON=1 but ipython not available.") # TODO: is the below actually true? # SkipTest will be raised if IPython unavailable from param.ipython import ParamPager test1_repr = """\x1b[1;32mParameters of 'TestClass'\n=========================\n\x1b[0m\n\x1b[1;31mParameters changed from their default values are marked in red.\x1b[0m\n\x1b[1;36mSoft bound values are marked in cyan.\x1b[0m\nC/V= Constant/Variable, RO/RW = ReadOnly/ReadWrite, AN=Allow None\n\n\x1b[1;34mNameValue Type Bounds Mode \x1b[0m\n\nu 4 Number V RW \nw 4 Number C RO \nv 4 Number C RW \nx None String V RW AN \ny 4 Number (-1, None) V RW \nz 4 Number (-1, 100) V RW \n\n\x1b[1;32mParameter docstrings:\n=====================\x1b[0m\n\n\x1b[1;34mu: < No docstring available >\x1b[0m\n\x1b[1;31mw: < No docstring available >\x1b[0m\n\x1b[1;34mv: < No docstring available >\x1b[0m\n\x1b[1;31mx: < No docstring available >\x1b[0m\n\x1b[1;34my: < No docstring available >\x1b[0m\n\x1b[1;31mz: < No docstring available >\x1b[0m""" test2_repr = """\x1b[1;32mParameters of 'TestClass' instance\n==================================\n\x1b[0m\n\x1b[1;31mParameters changed from their default values are marked in red.\x1b[0m\n\x1b[1;36mSoft bound values are marked in cyan.\x1b[0m\nC/V= Constant/Variable, RO/RW = ReadOnly/ReadWrite, AN=Allow None\n\n\x1b[1;34mNameValue Type Bounds Mode \x1b[0m\n\nu 4 Number V RW \nw 4 Number C RO \nv 4 Number C RW \nx None String V RW AN \ny 4 Number (-1, None) V RW \nz 4 Number (-1, 100) V RW \n\n\x1b[1;32mParameter docstrings:\n=====================\x1b[0m\n\n\x1b[1;34mu: < No docstring available >\x1b[0m\n\x1b[1;31mw: < No docstring available >\x1b[0m\n\x1b[1;34mv: < No docstring available >\x1b[0m\n\x1b[1;31mx: < No docstring available >\x1b[0m\n\x1b[1;34my: < No docstring available >\x1b[0m\n\x1b[1;31mz: < No docstring available >\x1b[0m""" class TestParamPager(unittest.TestCase): def setUp(self): self.maxDiff = None class TestClass(param.Parameterized): u = param.Number(4, precedence=0) w = param.Number(4, readonly=True, precedence=1) v = param.Number(4, constant=True, precedence=2) x = param.String(None, allow_None=True, precedence=3) y = param.Number(4, bounds=(-1, None), precedence=4) z = param.Number(4, bounds=(-1, 100), softbounds=(-100, -200), precedence=5) self.TestClass = TestClass self.pager = ParamPager() def test_parameterized_class(self): page_string = self.pager(self.TestClass) # Remove params automatic numbered names page_string = re.sub(r'TestClass(\d+)', 'TestClass', page_string) ref_string = re.sub(r'TestClass(\d+)', 'TestClass', test1_repr) try: self.assertEqual(page_string, ref_string) except Exception as e: sys.stderr.write(page_string) # Coloured output sys.stderr.write("\nRAW STRING:\n\n%r\n\n" % page_string) raise e def test_parameterized_instance(self): page_string = self.pager(self.TestClass()) # Remove params automatic numbered names page_string = re.sub(r'TestClass(\d+)', 'TestClass', page_string) ref_string = re.sub(r'TestClass(\d+)', 'TestClass', test2_repr) try: self.assertEqual(page_string, ref_string) except Exception as e: sys.stderr.write(page_string) # Coloured output sys.stderr.write("\nRAW STRING:\n\n%r\n\n" % page_string) raise e param-1.12.3/tests/API0/testlistselector.py000066400000000000000000000112361434441564000205270ustar00rootroot00000000000000import unittest import param # TODO: I copied the tests from testobjectselector, although I # struggled to understand some of them. Both files should be reviewed # and cleaned up together. # TODO: tests copied from testobjectselector could use assertRaises # context manager (and could be updated in testobjectselector too). class TestListSelectorParameters(unittest.TestCase): def setUp(self): class P(param.Parameterized): e = param.ListSelector(default=[5],objects=[5,6,7]) f = param.ListSelector(default=10) h = param.ListSelector(default=None) g = param.ListSelector(default=None,objects=[7,8]) i = param.ListSelector(default=[7],objects=[9],check_on_set=False) self.P = P def test_default_None(self): class Q(param.Parameterized): r = param.ListSelector(default=None) def test_set_object_constructor(self): p = self.P(e=[6]) self.assertEqual(p.e, [6]) def test_set_object_outside_bounds(self): p = self.P(e=[6]) try: p.e = [9] except ValueError: pass else: raise AssertionError("Object set outside range.") def test_set_object_setattr(self): p = self.P(e=[6]) p.f = [9] self.assertEqual(p.f, [9]) p.g = [7] self.assertEqual(p.g, [7]) p.i = [12] self.assertEqual(p.i, [12]) def test_set_object_not_None(self): p = self.P(e=[6]) p.g = [7] try: p.g = None except TypeError: pass else: raise AssertionError("Object set outside range.") def test_set_one_object_not_None(self): p = self.P(e=[6]) p.g = [7] try: p.g = [None] except ValueError: pass else: raise AssertionError("Object set outside range.") def test_set_object_setattr_post_error(self): p = self.P(e=[6]) p.f = [9] self.assertEqual(p.f, [9]) p.g = [7] try: p.g = [None] except ValueError: pass else: raise AssertionError("Object set outside range.") self.assertEqual(p.g, [7]) p.i = [12] self.assertEqual(p.i, [12]) def test_initialization_out_of_bounds(self): try: class Q(param.Parameterized): q = param.ListSelector([5],objects=[4]) except ValueError: pass else: raise AssertionError("ListSelector created outside range.") def test_initialization_no_bounds(self): try: class Q(param.Parameterized): q = param.ListSelector([5],objects=10) except TypeError: pass else: raise AssertionError("ListSelector created without range.") ################################################################## ################################################################## ### new tests (not copied from testobjectselector) def test_bad_default(self): with self.assertRaises(TypeError): class Q(param.Parameterized): r = param.ListSelector(default=6,check_on_set=True) def test_implied_check_on_set(self): with self.assertRaises(TypeError): class Q(param.Parameterized): r = param.ListSelector(default=7,objects=[7,8]) def test_default_not_checked(self): class Q(param.Parameterized): r = param.ListSelector(default=[6]) ########################## # CEBALERT: not sure it makes sense for ListSelector to be set to # a non-iterable value (except None). I.e. I think this first test # should fail. def test_default_not_checked_to_be_iterable(self): class Q(param.Parameterized): r = param.ListSelector(default=6) def test_set_checked_to_be_iterable(self): class Q(param.Parameterized): r = param.ListSelector(default=6,check_on_set=False) with self.assertRaises(TypeError): Q.r = 6 ########################## def test_compute_default(self): class Q(param.Parameterized): r = param.ListSelector(default=None, compute_default_fn=lambda: [1,2,3]) self.assertEqual(Q.r, None) Q.params('r').compute_default() self.assertEqual(Q.r, [1,2,3]) self.assertEqual(Q.params('r').objects, [1,2,3]) def test_bad_compute_default(self): class Q(param.Parameterized): r = param.ListSelector(default=None,compute_default_fn=lambda:1) with self.assertRaises(TypeError): Q.params('r').compute_default() param-1.12.3/tests/API0/testnumbergen.py000066400000000000000000000015721434441564000177770ustar00rootroot00000000000000""" Test cases for the numbergen module. """ import unittest import numbergen _seed = 0 # keep tests deterministic _iterations = 1000 class TestUniformRandom(unittest.TestCase): def test_range(self): lbound = 2.0 ubound = 5.0 gen = numbergen.UniformRandom( seed=_seed, lbound=lbound, ubound=ubound) for _ in range(_iterations): value = gen() self.assertTrue(lbound <= value < ubound) class TestUniformRandomOffset(unittest.TestCase): def test_range(self): lbound = 2.0 ubound = 5.0 gen = numbergen.UniformRandomOffset( seed=_seed, mean=(ubound + lbound) / 2, range=ubound - lbound) for _ in range(_iterations): value = gen() self.assertTrue(lbound <= value < ubound) param-1.12.3/tests/API0/testnumpy.py000066400000000000000000000014271434441564000171640ustar00rootroot00000000000000""" If numpy's present, is numpy stuff ok? """ import os import unittest import param try: import numpy import numpy.testing except ImportError: if os.getenv('PARAM_TEST_NUMPY','0') == '1': raise ImportError("PARAM_TEST_NUMPY=1 but numpy not available.") else: raise unittest.SkipTest("numpy not available") def _is_array_and_equal(test,ref): if not type(test) == numpy.ndarray: raise AssertionError numpy.testing.assert_array_equal(test,ref) # TODO: incomplete class TestNumpy(unittest.TestCase): def test_array_param(self): class Z(param.Parameterized): z = param.Array(default=numpy.array([1])) _is_array_and_equal(Z.z,[1]) z = Z(z=numpy.array([1,2])) _is_array_and_equal(z.z,[1,2]) param-1.12.3/tests/API0/testobjectselector.py000066400000000000000000000052541434441564000210250ustar00rootroot00000000000000""" Unit test for object selector parameters. Originally implemented as doctests in Topographica in the file testEnumerationParameter.txt """ import unittest import param opts=dict(A=[1,2],B=[3,4],C=dict(a=1,b=2)) class TestObjectSelectorParameters(unittest.TestCase): def setUp(self): class P(param.Parameterized): e = param.ObjectSelector(default=5,objects=[5,6,7]) f = param.ObjectSelector(default=10) h = param.ObjectSelector(default=None) g = param.ObjectSelector(default=None,objects=[7,8]) i = param.ObjectSelector(default=7,objects=[9],check_on_set=False) d = param.ObjectSelector(default=opts['B'],objects=opts) self.P = P def test_set_object_constructor(self): p = self.P(e=6) self.assertEqual(p.e, 6) def test_get_range_mutable(self): r = self.P.param.params("d").get_range() self.assertEqual(r['A'],opts['A']) self.assertEqual(r['C'],opts['C']) self.d=opts['A'] self.d=opts['C'] self.d=opts['B'] def test_set_object_outside_bounds(self): p = self.P(e=6) try: p.e = 9 except ValueError: pass else: raise AssertionError("Object set outside range.") def test_set_object_setattr(self): p = self.P(e=6) p.f = 9 self.assertEqual(p.f, 9) p.g = 7 self.assertEqual(p.g, 7) p.i = 12 self.assertEqual(p.i, 12) def test_set_object_not_None(self): p = self.P(e=6) p.g = 7 try: p.g = None except ValueError: pass else: raise AssertionError("Object set outside range.") def test_set_object_setattr_post_error(self): p = self.P(e=6) p.f = 9 self.assertEqual(p.f, 9) p.g = 7 try: p.g = None except ValueError: pass else: raise AssertionError("Object set outside range.") self.assertEqual(p.g, 7) p.i = 12 self.assertEqual(p.i, 12) def test_initialization_out_of_bounds(self): try: class Q(param.Parameterized): q = param.ObjectSelector(5,objects=[4]) except ValueError: pass else: raise AssertionError("ObjectSelector created outside range.") def test_initialization_no_bounds(self): try: class Q(param.Parameterized): q = param.ObjectSelector(5,objects=10) except TypeError: pass else: raise AssertionError("ObjectSelector created without range.") param-1.12.3/tests/API0/testparameterizedobject.py000066400000000000000000000215471434441564000220440ustar00rootroot00000000000000""" Unit test for Parameterized. """ import unittest import param import numbergen # CEBALERT: not anything like a complete test of Parameterized! import random from param.parameterized import ParamOverrides, shared_parameters class _SomeRandomNumbers(object): def __call__(self): return random.random() class TestPO(param.Parameterized): __test__ = False inst = param.Parameter(default=[1,2,3],instantiate=True) notinst = param.Parameter(default=[1,2,3],instantiate=False) const = param.Parameter(default=1,constant=True) ro = param.Parameter(default="Hello",readonly=True) ro2 = param.Parameter(default=object(),readonly=True,instantiate=True) dyn = param.Dynamic(default=1) class AnotherTestPO(param.Parameterized): instPO = param.Parameter(default=TestPO(),instantiate=True) notinstPO = param.Parameter(default=TestPO(),instantiate=False) class TestAbstractPO(param.Parameterized): __test__ = False __abstract = True class _AnotherAbstractPO(param.Parameterized): __abstract = True class TestParamInstantiation(AnotherTestPO): __test__ = False instPO = param.Parameter(default=AnotherTestPO(),instantiate=False) class TestParameterized(unittest.TestCase): def test_constant_parameter(self): """Test that you can't set a constant parameter after construction.""" testpo = TestPO(const=17) self.assertEqual(testpo.const,17) self.assertRaises(TypeError,setattr,testpo,'const',10) # check you can set on class TestPO.const=9 testpo = TestPO() self.assertEqual(testpo.const,9) def test_readonly_parameter(self): """Test that you can't set a read-only parameter on construction or as an attribute.""" testpo = TestPO() self.assertEqual(testpo.ro,"Hello") with self.assertRaises(TypeError): t = TestPO(ro=20) t=TestPO() self.assertRaises(TypeError,setattr,t,'ro',10) # check you cannot set on class self.assertRaises(TypeError,setattr,TestPO,'ro',5) self.assertEqual(testpo.params()['ro'].constant,True) # check that instantiate was ignored for readonly self.assertEqual(testpo.params()['ro2'].instantiate,False) def test_basic_instantiation(self): """Check that instantiated parameters are copied into objects.""" testpo = TestPO() self.assertEqual(testpo.inst,TestPO.inst) self.assertEqual(testpo.notinst,TestPO.notinst) TestPO.inst[1]=7 TestPO.notinst[1]=7 self.assertEqual(testpo.notinst,[1,7,3]) self.assertEqual(testpo.inst,[1,2,3]) def test_more_instantiation(self): """Show that objects in instantiated Parameters can still share data.""" anothertestpo = AnotherTestPO() ### CB: AnotherTestPO.instPO is instantiated, but ### TestPO.notinst is not instantiated - so notinst is still ### shared, even by instantiated parameters of AnotherTestPO. ### Seems like this behavior of Parameterized could be ### confusing, so maybe mention it in documentation somewhere. TestPO.notinst[1]=7 # (if you thought your instPO was completely an independent object, you # might be expecting [1,2,3] here) self.assertEqual(anothertestpo.instPO.notinst,[1,7,3]) def test_instantiation_inheritance(self): """Check that instantiate=True is always inherited (SF.net #2483932).""" t = TestParamInstantiation() assert t.params('instPO').instantiate is True assert isinstance(t.instPO,AnotherTestPO) def test_abstract_class(self): """Check that a class declared abstract actually shows up as abstract.""" self.assertEqual(TestAbstractPO.abstract,True) self.assertEqual(_AnotherAbstractPO.abstract,True) self.assertEqual(TestPO.abstract,False) def test_params(self): """Basic tests of params() method.""" # CB: test not so good because it requires changes if params # of PO are changed assert 'name' in param.Parameterized.params() assert len(param.Parameterized.params()) in [1,2] ## check for bug where subclass Parameters were not showing up ## if params() already called on a super class. assert 'inst' in TestPO.params() assert 'notinst' in TestPO.params() ## check caching assert param.Parameterized.params() is param.Parameterized().params(), "Results of params() should be cached." # just for performance reasons def test_state_saving(self): t = TestPO(dyn=_SomeRandomNumbers()) g = t.get_value_generator('dyn') g._Dynamic_time_fn=None assert t.dyn!=t.dyn orig = t.dyn t.state_push() t.dyn assert t.inspect_value('dyn')!=orig t.state_pop() assert t.inspect_value('dyn')==orig from param import parameterized class some_fn(param.ParameterizedFunction): __test__ = False num_phase = param.Number(18) frequencies = param.List([99]) scale = param.Number(0.3) def __call__(self,**params_to_override): params = parameterized.ParamOverrides(self,params_to_override) num_phase = params['num_phase'] frequencies = params['frequencies'] scale = params['scale'] return scale,num_phase,frequencies instance = some_fn.instance() class TestParameterizedFunction(unittest.TestCase): def _basic_tests(self,fn): self.assertEqual(fn(),(0.3,18,[99])) self.assertEqual(fn(frequencies=[1,2,3]),(0.3,18,[1,2,3])) self.assertEqual(fn(),(0.3,18,[99])) fn.frequencies=[10,20,30] self.assertEqual(fn(frequencies=[1,2,3]),(0.3,18,[1,2,3])) self.assertEqual(fn(),(0.3,18,[10,20,30])) def test_parameterized_function(self): self._basic_tests(some_fn) def test_parameterized_function_instance(self): self._basic_tests(instance) def test_pickle_instance(self): import pickle s = pickle.dumps(instance) instance.scale=0.8 i = pickle.loads(s) self.assertEqual(i(),(0.3,18,[10,20,30])) class TestPO1(param.Parameterized): __test__ = False x = param.Number(default=numbergen.UniformRandom(lbound=-1,ubound=1,seed=1),bounds=(-1,1)) y = param.Number(default=1,bounds=(-1,1)) class TestNumberParameter(unittest.TestCase): def test_outside_bounds(self): t1 = TestPO1() # Test bounds (non-dynamic number) try: t1.y = 10 except ValueError: pass else: assert False, "Should raise ValueError." def test_outside_bounds_numbergen(self): t1 = TestPO1() # Test bounds (dynamic number) t1.x = numbergen.UniformRandom(lbound=2,ubound=3) # bounds not checked on set try: t1.x except ValueError: pass else: assert False, "Should raise ValueError." class TestStringParameter(unittest.TestCase): def setUp(self): super(TestStringParameter, self).setUp() class TestString(param.Parameterized): a = param.String() b = param.String(default='',allow_None=True) c = param.String(default=None) self._TestString = TestString def test_handling_of_None(self): t = self._TestString() with self.assertRaises(ValueError): t.a = None t.b = None assert t.c is None class TestParamOverrides(unittest.TestCase): def setUp(self): super(TestParamOverrides, self).setUp() self.po = param.Parameterized(name='A',print_level=0) def test_init_name(self): self.assertEqual(self.po.name, 'A') def test_simple_override(self): overrides = ParamOverrides(self.po,{'name':'B'}) self.assertEqual(overrides['name'], 'B') self.assertEqual(overrides['print_level'], 0) # CEBALERT: missing test for allow_extra_keywords (e.g. getting a # warning on attempting to override non-existent parameter when # allow_extra_keywords is False) def test_missing_key(self): overrides = ParamOverrides(self.po,{'name':'B'}) with self.assertRaises(AttributeError): overrides['doesnotexist'] class TestSharedParameters(unittest.TestCase): def setUp(self): with shared_parameters(): self.p1 = TestPO(name='A', print_level=0) self.p2 = TestPO(name='B', print_level=0) self.ap1 = AnotherTestPO(name='A', print_level=0) self.ap2 = AnotherTestPO(name='B', print_level=0) def test_shared_object(self): self.assertTrue(self.ap1.instPO is self.ap2.instPO) self.assertTrue(self.ap1.params('instPO').default is not self.ap2.instPO) def test_shared_list(self): self.assertTrue(self.p1.inst is self.p2.inst) self.assertTrue(self.p1.params('inst').default is not self.p2.inst) param-1.12.3/tests/API0/testparameterizedrepr.py000066400000000000000000000124521434441564000215410ustar00rootroot00000000000000""" Unit test for the repr and pprint of parameterized objects. """ import unittest import param class TestParameterizedRepr(unittest.TestCase): def setUp(self): # initialize a parameterized class class A(param.Parameterized): a = param.Number(4, precedence=-5) b = param.String('B', precedence=-4) c = param.Number(4, precedence=0) d = param.Integer(-22, precedence=1) x = param.Number(1, precedence=2) y = param.Number(2, precedence=-1) z = param.Number(3, precedence=-2) def __init__(self, a, b, c=4, d=-22, **kwargs): super(A, self).__init__(a=a, b=b, c=c, **kwargs) self.A = A class B(param.Parameterized): # Similar to A but no **kwargs a = param.Number(4, precedence=-5) b = param.String('B', precedence=-4) c = param.Number(4, precedence=0) d = param.Integer(-22, precedence=1) x = param.Number(1, precedence=2) def __init__(self, a, b, c=4, d=-22): super(B, self).__init__(a=a, b=b, c=c, name='ClassB') self.B = B class C(param.Parameterized): # Similar to A but with *varargs a = param.Number(4, precedence=-5) b = param.String('B', precedence=-4) c = param.Number(4, precedence=0) d = param.Integer(-22, precedence=1) x = param.Number(1, precedence=2) y = param.Number(2, precedence=-1) z = param.Number(3, precedence=-2) def __init__(self, a, b, c=4, d=-22, *varargs, **kwargs): super(C, self).__init__(a=a, b=b, c=c, **kwargs) self.C = C class D(param.Parameterized): # Similar to A but with missing parameters a = param.Number(4, precedence=-5) b = param.String('B', precedence=-4) def __init__(self, a, b, c=4, d=-22, **kwargs): super(D, self).__init__(a=a, b=b, **kwargs) self.D = D # More realistically, positional args are not params class E(param.Parameterized): a = param.Number(4, precedence=-5) def __init__(self, p, q=4, **params): # (plus non-param kw too) super(E, self).__init__(**params) self.E = E def testparameterizedrepr(self): obj = self.A(4,'B', name='test1') self.assertEqual(repr(obj), "A(a=4, b='B', c=4, d=-22, name='test1', x=1, y=2, z=3)") def testparameterizedscriptrepr1(self): obj = self.A(4,'B', name='test') self.assertEqual(obj.pprint(), "A(4, 'B', name='test')") def testparameterizedscriptrepr2(self): obj = self.A(4,'B', c=5, name='test') self.assertEqual(obj.pprint(), "A(4, 'B', c=5, name='test')") def testparameterizedscriptrepr3(self): obj = self.A(4,'B', c=5, x=True, name='test') self.assertEqual(obj.pprint(), "A(4, 'B', c=5, name='test')") def testparameterizedscriptrepr4(self): obj = self.A(4,'B', c=5, x=10, name='test') self.assertEqual(obj.pprint(), "A(4, 'B', c=5, name='test', x=10)") def testparameterizedscriptrepr5(self): obj = self.A(4,'B', x=10, y=11, z=12, name='test') self.assertEqual(obj.pprint(), "A(4, 'B', name='test', z=12, y=11, x=10)") def testparameterizedscriptrepr_nokwargs(self): obj = self.B(4,'B', c=99) obj.x = 10 # Modified but not passable through constructor self.assertEqual(obj.pprint(), "B(4, 'B', c=99)") def testparameterizedscriptrepr_varags(self): obj = self.C(4,'C', c=99) self.assertEqual(obj.pprint(), "C(4, 'C', c=99, **varargs)") def testparameterizedscriptrepr_varags_kwargs(self): obj = self.C(4,'C', c=99, x=10, y=11, z=12) self.assertEqual(obj.pprint(), "C(4, 'C', c=99, z=12, y=11, x=10, **varargs)") def testparameterizedscriptrepr_missing_values(self): obj = self.D(4,'D', c=99) self.assertEqual(obj.pprint(), "D(4, 'D', c=, d=)") def testparameterizedscriptrepr_nonparams(self): obj = self.E(10,q='hi', a=99) self.assertEqual(obj.pprint(), "E(, q=, a=99)") def test_exceptions(self): obj = self.E(10,q='hi',a=99) try: obj.pprint(unknown_value=False) except Exception: pass else: raise AssertionError def test_suppression(self): obj = self.E(10,q='hi',a=99) self.assertEqual(obj.pprint(unknown_value=None), "E(a=99)") def test_imports_deduplication(self): obj = self.E(10,q='hi', a=99) imports = ['import me','import me'] obj.pprint(imports=imports) self.assertEqual(imports.count('import me'),1) def test_qualify(self): obj = self.E(10,q='hi', a=99) r = "E(, q=, a=99)" self.assertEqual(obj.pprint(qualify=False), r) self.assertEqual(obj.pprint(qualify=True), "tests.API0.testparameterizedrepr."+r) param-1.12.3/tests/API0/testrangeparameter.py000066400000000000000000000030241434441564000210040ustar00rootroot00000000000000""" Unit test for Range parameters. """ import unittest import param class TestRangeParameters(unittest.TestCase): def test_initialization_out_of_bounds(self): try: class Q(param.Parameterized): q = param.Range((0, 2), bounds=(0, 1)) except ValueError: pass else: raise AssertionError("No exception raised on out-of-bounds date") def test_set_exclusive_out_of_bounds_upper(self): class Q(param.Parameterized): q = param.Range(bounds=(0, 10), inclusive_bounds=(True, False)) try: Q.q = (0, 10) except ValueError: pass else: raise AssertionError("No exception raised on out-of-bounds date") def test_set_exclusive_out_of_bounds_lower(self): class Q(param.Parameterized): q = param.Range(bounds=(0, 10), inclusive_bounds=(False, True)) try: Q.q = (0, 10) except ValueError: pass else: raise AssertionError("No exception raised on out-of-bounds date") def test_set_out_of_bounds(self): class Q(param.Parameterized): q = param.Range(bounds=(0, 10)) try: Q.q = (5, 11) except ValueError: pass else: raise AssertionError("No exception raised on out-of-bounds date") def test_get_soft_bounds(self): q = param.Range((1,3), bounds=(0, 10), softbounds=(1, 9)) self.assertEqual(q.get_soft_bounds(), (1, 9)) param-1.12.3/tests/API0/teststringparam.py000066400000000000000000000037461434441564000203510ustar00rootroot00000000000000""" Unit test for String parameters """ import sys import unittest import param ip_regex = r'^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$' if not hasattr(unittest.TestCase, 'assertRaisesRegex'): unittest.TestCase.assertRaisesRegex = unittest.TestCase.assertRaisesRegexp class TestStringParameters(unittest.TestCase): def test_regex_ok(self): class A(param.Parameterized): s = param.String('0.0.0.0', ip_regex) a = A() a.s = '123.123.0.1' def test_reject_none(self): class A(param.Parameterized): s = param.String('0.0.0.0', ip_regex) a = A() cls = 'class' if sys.version_info.major > 2 else 'type' exception = "String parameter 's' only takes a string value, not value of type <%s 'NoneType'>." % cls with self.assertRaisesRegex(ValueError, exception): a.s = None # because allow_None should be False def test_default_none(self): class A(param.Parameterized): s = param.String(None, ip_regex) a = A() a.s = '123.123.0.1' a.s = None # because allow_None should be True with default of None def test_regex_incorrect(self): class A(param.Parameterized): s = param.String('0.0.0.0', regex=ip_regex) a = A() exception = "String parameter 's' value '123.123.0.256' does not match regex '%s'." % ip_regex with self.assertRaises(ValueError) as e: a.s = '123.123.0.256' self.assertEqual(str(e.exception), exception.replace('\\', '\\\\')) def test_regex_incorrect_default(self): exception = "String parameter None value '' does not match regex '%s'." % ip_regex with self.assertRaises(ValueError) as e: class A(param.Parameterized): s = param.String(regex=ip_regex) # default value '' does not match regular expression self.assertEqual(str(e.exception), exception.replace('\\', '\\\\')) param-1.12.3/tests/API0/testtimedependent.py000066400000000000000000000230071434441564000206370ustar00rootroot00000000000000""" Unit tests for the param.Time class, time dependent parameters and time-dependent numbergenerators. """ import unittest import param import numbergen import copy import pytest import fractions try: import gmpy except ImportError: import os if os.getenv('PARAM_TEST_GMPY','0') == '1': raise ImportError("PARAM_TEST_GMPY=1 but gmpy not available.") else: gmpy = None class TestTimeClass(unittest.TestCase): def test_time_init(self): param.Time() def test_time_init_int(self): t = param.Time(time_type=int) self.assertEqual(t(), 0) def test_time_int_iter(self): t = param.Time(time_type=int) self.assertEqual(next(t), 0) self.assertEqual(next(t), 1) def test_time_init_timestep(self): t = param.Time(time_type=int, timestep=2) self.assertEqual(next(t), 0) self.assertEqual(next(t), 2) def test_time_int_until(self): t = param.Time(time_type=int, until=3) self.assertEqual(next(t), 0) self.assertEqual(next(t), 1) self.assertEqual(next(t), 2) self.assertEqual(next(t), 3) try: self.assertEqual(next(t), 4) raise AssertionError("StopIteration should have been raised") except StopIteration: pass def test_time_int_eq(self): t = param.Time(time_type=int) s = param.Time(time_type=int) t(3); s(3) self.assertEqual(t == s, True) def test_time_int_context(self): t = param.Time(time_type=int) t(3) with t: self.assertEqual(t(), 3) t(5) self.assertEqual(t(), 5) self.assertEqual(t(), 3) def test_time_int_context_iadd(self): with param.Time(time_type=int) as t: self.assertEqual(t(), 0) t += 5 self.assertEqual(t(), 5) self.assertEqual(t(), 0) def test_time_int_change_type(self): t = param.Time(time_type=int) self.assertEqual(t(), 0) t(1, fractions.Fraction) self.assertEqual(t(), 1) self.assertEqual(t.time_type, fractions.Fraction) @pytest.mark.skipif(gmpy is None, reason="gmpy is not installed") def test_time_init_gmpy(self): t = param.Time(time_type=gmpy.mpq) self.assertEqual(t(), gmpy.mpq(0)) t.advance(gmpy.mpq(0.25)) self.assertEqual(t(), gmpy.mpq(1,4)) @pytest.mark.skipif(gmpy is None, reason="gmpy is not installed") def test_time_init_gmpy_advanced(self): t = param.Time(time_type=gmpy.mpq, timestep=gmpy.mpq(0.25), until=1.5) self.assertEqual(t(), gmpy.mpq(0,1)) t(0.5) self.assertEqual(t(), gmpy.mpq(1,2)) with t: t.advance(0.25) self.assertEqual(t(), gmpy.mpq(3,4)) self.assertEqual(t(), gmpy.mpq(1,2)) tvals = [tval for tval in t] self.assertEqual(tvals, [gmpy.mpq(1,2), gmpy.mpq(3,4), gmpy.mpq(1,1), gmpy.mpq(5,4), gmpy.mpq(3,2)]) class TestTimeDependentDynamic(unittest.TestCase): def setUp(self): param.Dynamic.time_dependent=None self.time_fn= param.Time(time_type=int) class Incrementer(object): def __init__(self): self.i = -1 def __call__(self): self.i+=1 return self.i self.Incrementer = Incrementer class DynamicClass(param.Parameterized): a = param.Number(default = self.Incrementer()) self.DynamicClass = DynamicClass self._start_state = copy.copy([param.Dynamic.time_dependent, numbergen.TimeAware.time_dependent, param.Dynamic.time_fn, numbergen.TimeAware.time_fn, param.random_seed]) def tearDown(self): param.Dynamic.time_dependent = self._start_state[0] numbergen.TimeAware.time_dependent = self._start_state[1] param.Dynamic.time_fn = self._start_state[2] numbergen.TimeAware.time_fn = self._start_state[3] param.random_seed = self._start_state[4] def test_non_time_dependent(self): """ With param.Dynamic.time_dependent=None every call should increment. """ param.Dynamic.time_dependent=None param.Dynamic.time_fn = self.time_fn dynamic = self.DynamicClass() self.assertEqual(dynamic.a, 0) self.assertEqual(dynamic.a, 1) self.assertEqual(dynamic.a, 2) def test_time_fixed(self): """ With param.Dynamic.time_dependent=True the value should only increment when the time value changes. """ param.Dynamic.time_dependent=True param.Dynamic.time_fn = self.time_fn dynamic = self.DynamicClass() self.assertEqual(dynamic.a, 0) self.assertEqual(dynamic.a, 0) self.time_fn += 1 self.assertEqual(dynamic.a, 1) self.assertEqual(dynamic.a, 1) param.Dynamic.time_fn -= 5 self.assertEqual(dynamic.a, 2) self.assertEqual(dynamic.a, 2) def test_time_dependent(self): """ With param.Dynamic.time_dependent=True and param.Dynamic and numbergen.TimeDependent sharing a common time_fn, the value should be a function of time. """ param.Dynamic.time_dependent=True param.Dynamic.time_fn = self.time_fn numbergen.TimeDependent.time_fn = self.time_fn class DynamicClass(param.Parameterized): b = param.Number(default = numbergen.ScaledTime(factor=2)) dynamic = DynamicClass() self.time_fn(0) self.assertEqual(dynamic.b, 0.0) self.time_fn += 5 self.assertEqual(dynamic.b, 10.0) self.assertEqual(dynamic.b, 10.0) self.time_fn -= 2 self.assertEqual(dynamic.b, 6.0) self.assertEqual(dynamic.b, 6.0) self.time_fn -= 3 self.assertEqual(dynamic.b, 0.0) def test_time_dependent_random(self): """ When set to time_dependent=True, random number generators should also be a function of time. """ param.Dynamic.time_dependent=True numbergen.TimeAware.time_dependent=True param.Dynamic.time_fn = self.time_fn numbergen.TimeAware.time_fn = self.time_fn param.random_seed = 42 class DynamicClass(param.Parameterized): c = param.Number(default = numbergen.UniformRandom(name = 'test1')) d = param.Number(default = numbergen.UniformRandom(name = 'test2')) e = param.Number(default = numbergen.UniformRandom(name = 'test1')) dynamic = DynamicClass() test1_t1 = 0.23589388250988552 test2_t1 = 0.12576257837158122 test1_t2 = 0.14117586161849593 test2_t2 = 0.9134917395930359 self.time_fn(0) self.assertEqual(dynamic.c, test1_t1) self.assertEqual(dynamic.c, dynamic.e) self.assertNotEqual(dynamic.c, dynamic.d) self.assertEqual(dynamic.d, test2_t1) self.time_fn(1) self.assertEqual(dynamic.c, test1_t2) self.assertEqual(dynamic.c, test1_t2) self.assertEqual(dynamic.d, test2_t2) self.time_fn(0) self.assertEqual(dynamic.c, test1_t1) self.assertEqual(dynamic.d, test2_t1) def test_time_hashing_integers(self): """ Check that ints, fractions and strings hash to the same value for integer values. """ hashfn = numbergen.Hash("test", input_count=1) hash_1 = hashfn(1) hash_42 = hashfn(42) hash_200001 = hashfn(200001) self.assertEqual(hash_1, hashfn(fractions.Fraction(1))) self.assertEqual(hash_1, hashfn("1")) self.assertEqual(hash_42, hashfn(fractions.Fraction(42))) self.assertEqual(hash_42, hashfn("42")) self.assertEqual(hash_200001, hashfn(fractions.Fraction(200001))) self.assertEqual(hash_200001, hashfn("200001")) def test_time_hashing_rationals(self): """ Check that hashes fractions and strings match for some reasonable rational numbers. """ hashfn = numbergen.Hash("test", input_count=1) pi = "3.141592" half = fractions.Fraction(0.5) self.assertEqual(hashfn(0.5), hashfn(half)) self.assertEqual(hashfn(pi), hashfn(fractions.Fraction(pi))) @pytest.mark.skipif(gmpy is None, reason="gmpy is not installed") def test_time_hashing_integers_gmpy(self): """ Check that hashes for gmpy values at the integers also matches those of ints, fractions and strings. """ hashfn = numbergen.Hash("test", input_count=1) hash_1 = hashfn(1) hash_42 = hashfn(42) self.assertEqual(hash_1, hashfn(gmpy.mpq(1))) self.assertEqual(hash_1, hashfn(1)) self.assertEqual(hash_42, hashfn(gmpy.mpq(42))) self.assertEqual(hash_42, hashfn(42)) @pytest.mark.skipif(gmpy is None, reason="gmpy is not installed") def test_time_hashing_rationals_gmpy(self): """ Check that hashes of fractions and gmpy mpqs match for some reasonable rational numbers. """ pi = "3.141592" hashfn = numbergen.Hash("test", input_count=1) self.assertEqual(hashfn(0.5), hashfn(gmpy.mpq(0.5))) self.assertEqual(hashfn(pi), hashfn(gmpy.mpq(3.141592))) param-1.12.3/tests/API1/000077500000000000000000000000001434441564000145375ustar00rootroot00000000000000param-1.12.3/tests/API1/__init__.py000066400000000000000000000007611434441564000166540ustar00rootroot00000000000000import param import unittest class API1TestCase(unittest.TestCase): def setUp(self): param.parameterized.Parameters._disable_stubs = True def tearDown(self): param.parameterized.Parameters._disable_stubs = False # Python 2 compatibility if not hasattr(API1TestCase, 'assertRaisesRegex'): API1TestCase.assertRaisesRegex = API1TestCase.assertRaisesRegexp if not hasattr(API1TestCase, 'assertEquals'): API1TestCase.assertEquals = API1TestCase.assertEqual param-1.12.3/tests/API1/testcalendardateparam.py000066400000000000000000000040261434441564000214430ustar00rootroot00000000000000""" Unit test for CalendarDate parameters. """ import datetime as dt import pytest import param from . import API1TestCase class TestDateTimeParameters(API1TestCase): def test_initialization_out_of_bounds(self): try: class Q(param.Parameterized): q = param.CalendarDate(dt.date(2017,2,27), bounds=(dt.date(2017,2,1), dt.date(2017,2,26))) except ValueError: pass else: raise AssertionError("No exception raised on out-of-bounds date") def test_set_out_of_bounds(self): class Q(param.Parameterized): q = param.CalendarDate(bounds=(dt.date(2017,2,1), dt.date(2017,2,26))) try: Q.q = dt.date(2017,2,27) except ValueError: pass else: raise AssertionError("No exception raised on out-of-bounds date") def test_set_exclusive_out_of_bounds(self): class Q(param.Parameterized): q = param.CalendarDate(bounds=(dt.date(2017,2,1), dt.date(2017,2,26)), inclusive_bounds=(True, False)) try: Q.q = dt.date(2017,2,26) except ValueError: pass else: raise AssertionError("No exception raised on out-of-bounds date") def test_get_soft_bounds(self): q = param.CalendarDate(dt.date(2017,2,25), bounds=(dt.date(2017,2,1), dt.date(2017,2,26)), softbounds=(dt.date(2017,2,1), dt.date(2017,2,25))) self.assertEqual(q.get_soft_bounds(), (dt.date(2017,2,1), dt.date(2017,2,25))) def test_datetime_not_accepted(self): with pytest.raises(ValueError): param.CalendarDate(dt.datetime(2021, 8, 16, 10)) param-1.12.3/tests/API1/testcalendardaterangeparam.py000066400000000000000000000040111434441564000224520ustar00rootroot00000000000000""" Unit tests for CalendarDateRange parameter. """ import datetime as dt import param from . import API1TestCase # Assuming tests of range parameter cover most of what's needed to # test date range. class TestDateTimeRange(API1TestCase): bad_range = (dt.date(2017,2,27),dt.date(2017,2,26)) def test_wrong_type_default(self): try: class Q(param.Parameterized): a = param.CalendarDateRange(default=(1.0,2.0)) except ValueError: pass else: raise AssertionError("Bad date type was accepted.") def test_wrong_type_init(self): class Q(param.Parameterized): a = param.CalendarDateRange() try: Q(a=self.bad_range) except ValueError: pass else: raise AssertionError("Bad date type was accepted.") def test_wrong_type_set(self): class Q(param.Parameterized): a = param.CalendarDateRange() q = Q() try: q.a = self.bad_range except ValueError: pass else: raise AssertionError("Bad date type was accepted.") def test_start_before_end_default(self): try: class Q(param.Parameterized): a = param.CalendarDateRange(default=self.bad_range) except ValueError: pass else: raise AssertionError("Bad date range was accepted.") def test_start_before_end_init(self): class Q(param.Parameterized): a = param.CalendarDateRange() try: Q(a=self.bad_range) except ValueError: pass else: raise AssertionError("Bad date range was accepted.") def test_start_before_end_set(self): class Q(param.Parameterized): a = param.CalendarDateRange() q = Q() try: q.a = self.bad_range except ValueError: pass else: raise AssertionError("Bad date range was accepted.") param-1.12.3/tests/API1/testclassselector.py000066400000000000000000000060641434441564000206650ustar00rootroot00000000000000""" Unit test for ClassSelector parameters. """ from numbers import Number import param from . import API1TestCase class TestClassSelectorParameters(API1TestCase): def setUp(self): super(TestClassSelectorParameters, self).setUp() class P(param.Parameterized): e = param.ClassSelector(default=1,class_=int) f = param.ClassSelector(default=int,class_=Number, is_instance=False) g = param.ClassSelector(default=1,class_=(int,str)) h = param.ClassSelector(default=int,class_=(int,str), is_instance=False) self.P = P def test_single_class_instance_constructor(self): p = self.P(e=6) self.assertEqual(p.e, 6) def test_single_class_instance_error(self): exception = "ClassSelector parameter 'e' value must be an instance of int, not 'a'." with self.assertRaisesRegex(ValueError, exception): self.P(e='a') def test_single_class_type_constructor(self): p = self.P(f=float) self.assertEqual(p.f, float) def test_single_class_type_error(self): exception = "ClassSelector parameter 'f' must be a subclass of Number, not 'str'." with self.assertRaisesRegex(ValueError, exception): self.P(f=str) def test_multiple_class_instance_constructor1(self): p = self.P(g=1) self.assertEqual(p.g, 1) def test_multiple_class_instance_constructor2(self): p = self.P(g='A') self.assertEqual(p.g, 'A') def test_multiple_class_instance_error(self): exception = r"ClassSelector parameter 'g' value must be an instance of \(int, str\), not 3.0." with self.assertRaisesRegex(ValueError, exception): self.P(g=3.0) def test_multiple_class_type_constructor1(self): p = self.P(h=int) self.assertEqual(p.h, int) def test_multiple_class_type_constructor2(self): p = self.P(h=str) self.assertEqual(p.h, str) def test_class_selector_get_range(self): p = self.P() classes = p.param.g.get_range() self.assertIn('int', classes) self.assertIn('str', classes) def test_multiple_class_type_error(self): exception = r"ClassSelector parameter 'h' must be a subclass of \(int, str\), not 'float'." with self.assertRaisesRegex(ValueError, exception): self.P(h=float) class TestDictParameters(API1TestCase): def test_valid_dict_parameter(self): valid_dict = {1:2, 3:3} class Test(param.Parameterized): items = param.Dict(default=valid_dict) def test_valid_dict_parameter_positional(self): valid_dict = {1:2, 3:3} class Test(param.Parameterized): items = param.Dict(valid_dict) def test_dict_invalid_set(self): valid_dict = {1:2, 3:3} class Test(param.Parameterized): items = param.Dict(valid_dict) test = Test() exception = "Dict parameter 'items' value must be an instance of dict, not 3." with self.assertRaisesRegex(ValueError, exception): test.items = 3 param-1.12.3/tests/API1/testcolorparameter.py000066400000000000000000000034311434441564000210310ustar00rootroot00000000000000""" Unit test for Color parameters. """ import param from . import API1TestCase class TestColorParameters(API1TestCase): def test_initialization_invalid_string(self): try: class Q(param.Parameterized): q = param.Color('red', allow_named=False) except ValueError: pass else: raise AssertionError("No exception raised on invalid color") def test_set_invalid_string(self): class Q(param.Parameterized): q = param.Color(allow_named=False) try: Q.q = 'red' except ValueError: pass else: raise AssertionError("No exception raised on invalid color") def test_set_invalid_named_color(self): class Q(param.Parameterized): q = param.Color(allow_named=True) try: Q.q = 'razzmatazz' except ValueError: pass else: raise AssertionError("No exception raised on invalid color") def test_invalid_long_hex(self): class Q(param.Parameterized): q = param.Color() try: Q.q = '#gfffff' except ValueError: pass else: raise AssertionError("No exception raised on invalid color") def test_valid_long_hex(self): class Q(param.Parameterized): q = param.Color() Q.q = '#ffffff' self.assertEqual(Q.q, '#ffffff') def test_valid_short_hex(self): class Q(param.Parameterized): q = param.Color() Q.q = '#fff' self.assertEqual(Q.q, '#fff') def test_valid_named_color(self): class Q(param.Parameterized): q = param.Color(allow_named=True) Q.q = 'indianred' self.assertEqual(Q.q, 'indianred') param-1.12.3/tests/API1/testcomparator.py000066400000000000000000000021611434441564000201600ustar00rootroot00000000000000import datetime import decimal import sys import pytest from param.parameterized import Comparator try: import numpy as np except ImportError: np = None try: import pandas as pd except ImportError: pd = None _now = datetime.datetime.now() _today = datetime.date.today() if sys.version_info[0] >= 3: _supported = { 'str': 'test', 'float': 1.2, 'int': 1, 'decimal': decimal.Decimal(1) / decimal.Decimal(7), 'bytes': (1024).to_bytes(2, byteorder='big'), 'None': None, 'list': [1, 2], 'tuple': (1, 2), 'set': {1, 2}, 'dict': {'a': 1, 'b': 2}, 'date': _today, 'datetime': _now, } if np: _supported.update({ 'np.datetime64': np.datetime64(_now), }) if pd: _supported.update({'pd.Timestamp': pd.Timestamp(_now)}) else: _supported = {} @pytest.mark.skipif(sys.version_info[0] == 2, reason="requires python 3 or higher") @pytest.mark.parametrize('obj', _supported.values(), ids=_supported.keys()) def test_comparator_equal(obj): assert Comparator.is_equal(obj, obj) param-1.12.3/tests/API1/testcompositeparams.py000066400000000000000000000054301434441564000212210ustar00rootroot00000000000000""" Unit test for composite parameters. Originally implemented as doctests in Topographica in the file testCompositeParameter.txt """ import param from . import API1TestCase class TestCompositeParameters(API1TestCase): def setUp(self): super(TestCompositeParameters, self).setUp() # initialize a class with a compound parameter class A(param.Parameterized): x = param.Number(default=0) y = param.Number(default=0) xy = param.Composite(attribs=['x','y']) self.A = A self.a = self.A() class SomeSequence(object): "Can't use iter with Dynamic (doesn't pickle, doesn't copy)" def __init__(self,sequence): self.sequence=sequence self.index=0 def __call__(self): val=self.sequence[self.index] self.index+=1 return val self.SomeSequence = SomeSequence def test_initialization(self): "Make an instance and do default checks" self.assertEqual(self.a.x, 0) self.assertEqual(self.a.y, 0) self.assertEqual(self.a.xy, [0,0]) def test_set_component(self): self.a.x = 1 self.assertEqual(self.a.xy, [1,0]) def test_set_compound(self): self.a.xy = (2,3) self.assertEqual(self.a.x, 2) self.assertEqual(self.a.y, 3) def test_compound_class(self): " Get the compound on the class " self.assertEqual(self.A.xy, [0,0]) def test_set_compound_class_set(self): self.A.xy = (5,6) self.assertEqual(self.A.x, 5) self.assertEqual(self.A.y, 6) def test_set_compound_class_instance(self): self.A.xy = (5,6) # # Make a new instance b = self.A() self.assertEqual(b.x, 5) self.assertEqual(b.y, 6) def test_set_compound_class_instance_unchanged(self): self.a.xy = (2,3) self.A.xy = (5,6) self.assertEqual(self.a.x, 2) self.assertEqual(self.a.y, 3) def test_composite_dynamic(self): """ Check CompositeParameter is ok with Dynamic CB: this test is really of Parameterized. """ a2 = self.A(x=self.SomeSequence([1,2,3]), y=self.SomeSequence([4,5,6])) a2.x, a2.y # Call of x and y params # inspect should not advance numbers self.assertEqual(a2.param.inspect_value('xy'), [1, 4]) def test_composite_dynamic_generator(self): a2 = self.A(x=self.SomeSequence([1,2,3]), y=self.SomeSequence([4,5,6])) a2.x, a2.y # Call of x and y params ix,iy = a2.param.get_value_generator('xy') # get_value_generator() should give the objects self.assertEqual(ix(), 2) self.assertEqual(iy(), 5) param-1.12.3/tests/API1/testdateparam.py000066400000000000000000000035041434441564000177510ustar00rootroot00000000000000""" Unit test for Date parameters. """ import datetime as dt import param from . import API1TestCase class TestDateParameters(API1TestCase): def test_initialization_out_of_bounds(self): try: class Q(param.Parameterized): q = param.Date(dt.datetime(2017,2,27), bounds=(dt.datetime(2017,2,1), dt.datetime(2017,2,26))) except ValueError: pass else: raise AssertionError("No exception raised on out-of-bounds date") def test_set_out_of_bounds(self): class Q(param.Parameterized): q = param.Date(bounds=(dt.datetime(2017,2,1), dt.datetime(2017,2,26))) try: Q.q = dt.datetime(2017,2,27) except ValueError: pass else: raise AssertionError("No exception raised on out-of-bounds date") def test_set_exclusive_out_of_bounds(self): class Q(param.Parameterized): q = param.Date(bounds=(dt.datetime(2017,2,1), dt.datetime(2017,2,26)), inclusive_bounds=(True, False)) try: Q.q = dt.datetime(2017,2,26) except ValueError: pass else: raise AssertionError("No exception raised on out-of-bounds date") def test_get_soft_bounds(self): q = param.Date(dt.datetime(2017,2,25), bounds=(dt.datetime(2017,2,1), dt.datetime(2017,2,26)), softbounds=(dt.datetime(2017,2,1), dt.datetime(2017,2,25))) self.assertEqual(q.get_soft_bounds(), (dt.datetime(2017,2,1), dt.datetime(2017,2,25))) param-1.12.3/tests/API1/testdaterangeparam.py000066400000000000000000000104471434441564000207720ustar00rootroot00000000000000""" Unit tests for DateRange parameter. """ import datetime as dt import param import pytest from . import API1TestCase try: import numpy as np except: np = None # Assuming tests of range parameter cover most of what's needed to # test date range. class TestDateRange(API1TestCase): bad_range = (dt.datetime(2017,2,27),dt.datetime(2017,2,26)) def test_wrong_type_default(self): with pytest.raises( ValueError, match="DateRange parameter None only takes a tuple value, not str." ): class Q(param.Parameterized): a = param.DateRange(default='wrong') def test_wrong_inner_type_default(self): with pytest.raises( ValueError, match='DateRange parameter None only takes date/datetime values, not type float.' ): class Q(param.Parameterized): a = param.DateRange(default=(1.0,2.0)) def test_wrong_inner_type_init(self): class Q(param.Parameterized): a = param.DateRange() with pytest.raises( ValueError, match="DateRange parameter 'a' only takes date/datetime values, not type float." ): Q(a=(1.0, 2.0)) def test_wrong_inner_type_set(self): class Q(param.Parameterized): a = param.DateRange() q = Q() with pytest.raises( ValueError, match="DateRange parameter 'a' only takes date/datetime values, not type float." ): q.a = (1.0, 2.0) def test_wrong_type_init(self): class Q(param.Parameterized): a = param.DateRange() with pytest.raises( ValueError, match="DateRange parameter 'a' only takes a tuple value, not str." ): Q(a='wrong') def test_wrong_type_set(self): class Q(param.Parameterized): a = param.DateRange() q = Q() with pytest.raises( ValueError, match="DateRange parameter 'a' only takes a tuple value, not str." ): q.a = 'wrong' def test_start_before_end_default(self): with pytest.raises( ValueError, match="DateRange parameter None's end datetime 2017-02-26 00:00:00 is before start datetime 2017-02-27 00:00:00." ): class Q(param.Parameterized): a = param.DateRange(default=self.bad_range) def test_start_before_end_init(self): class Q(param.Parameterized): a = param.DateRange() with pytest.raises( ValueError, match="DateRange parameter 'a''s end datetime 2017-02-26 00:00:00 is before start datetime 2017-02-27 00:00:00." ): Q(a=self.bad_range) def test_start_before_end_set(self): class Q(param.Parameterized): a = param.DateRange() q = Q() with pytest.raises( ValueError, match="DateRange parameter 'a''s end datetime 2017-02-26 00:00:00 is before start datetime 2017-02-27 00:00:00." ): q.a = self.bad_range @pytest.mark.skipif(np is None, reason='NumPy is not available') def test_numpy_default(self): class Q(param.Parameterized): a = param.DateRange(default=(np.datetime64('2022-01-01T00:00'), np.datetime64('2022-10-01T00:00'))) @pytest.mark.skipif(np is None, reason='NumPy is not available') def test_numpy_set(self): class Q(param.Parameterized): a = param.DateRange() q = Q() q.a = (np.datetime64('2022-01-01T00:00'), np.datetime64('2022-10-01T00:00')) @pytest.mark.skipif(np is None, reason='NumPy is not available') def test_numpy_init(self): class Q(param.Parameterized): a = param.DateRange() Q(a=(np.datetime64('2022-01-01T00:00'), np.datetime64('2022-10-01T00:00'))) @pytest.mark.skipif(np is None, reason='NumPy is not available') def test_numpy_start_before_end_default(self): with pytest.raises( ValueError, match="DateRange parameter None's end datetime 2022-01-01T00:00 is before start datetime 2022-10-01T00:00." ): class Q(param.Parameterized): a = param.DateRange(default=(np.datetime64('2022-10-01T00:00'), np.datetime64('2022-01-01T00:00'))) param-1.12.3/tests/API1/testdefaults.py000066400000000000000000000023241434441564000176210ustar00rootroot00000000000000""" Do all subclasses of Parameter supply a valid default? """ import pytest from param.parameterized import add_metaclass from param import concrete_descendents, Parameter # import all parameter types from param import * # noqa from param import ClassSelector from . import API1TestCase positional_args = { # ClassSelector: (object,) } skip = ['ClassSelector'] try: import numpy # noqa except ImportError: skip.append('Array') try: import pandas # noqa except ImportError: skip.append('DataFrame') skip.append('Series') class DefaultsMetaclassTest(type): def __new__(mcs, name, bases, dict_): def test_skip(*args,**kw): pytest.skip() def add_test(p): def test(self): # instantiate parameter with no default (but supply # any required args) p(*positional_args.get(p,tuple())) return test for p_name, p_type in concrete_descendents(Parameter).items(): dict_["test_default_of_%s"%p_name] = add_test(p_type) if p_name not in skip else test_skip return type.__new__(mcs, name, bases, dict_) @add_metaclass(DefaultsMetaclassTest) class TestDefaults(API1TestCase): pass param-1.12.3/tests/API1/testdynamicparams.py000066400000000000000000000216451434441564000206510ustar00rootroot00000000000000""" Unit test for dynamic parameters. Tests __get__, __set__ and that inspect_value() and get_value_generator() work. Originally implemented as doctests in Topographica in the file testDynamicParameter.txt """ import copy import param import numbergen from . import API1TestCase class TestDynamicParameters(API1TestCase): def setUp(self): super(TestDynamicParameters, self).setUp() param.Dynamic.time_dependent = False class TestPO1(param.Parameterized): x = param.Dynamic(default=numbergen.UniformRandom(lbound=-1,ubound=1,seed=1),doc="nothing") y = param.Dynamic(default=1) class TestPO2(param.Parameterized): x = param.Dynamic(default=numbergen.UniformRandom(lbound=-1,ubound=1,seed=30)) y = param.Dynamic(default=1.0) self.TestPO2 = TestPO2 self.TestPO1 = TestPO1 self.t1 = self.TestPO1() self.t2 = self.TestPO1(x=numbergen.UniformRandom(lbound=-1,ubound=1,seed=10)) self.t3 = self.TestPO1(x=numbergen.UniformRandom(lbound=-1,ubound=1,seed=10)) self.t2.param.set_dynamic_time_fn(None) self.t3.param.set_dynamic_time_fn(None) self.t6 = self.TestPO2() self.t7 = self.TestPO2() class TestDynamicParameterBasics(TestDynamicParameters): def test_set_dynamic_time_fn_x(self): self.t1.param.set_dynamic_time_fn(None) self.assertEqual( self.t1.param.params()['x']._value_is_dynamic(self.t1), True) def test_set_dynamic_time_fn_y(self): self.assertEqual( self.t1.param.params()['y']._value_is_dynamic(self.t1), False) def test_inspect_x(self): "no value generated yet" self.assertEqual(self.t1.param.inspect_value('x'), None) def test_inspect_y(self): self.assertEqual(self.t1.param.inspect_value('y'), 1) def test_inspect_y_set(self): self.t1.y = 2 self.assertEqual(self.t1.param.inspect_value('y'), 2) def test_set_dynamic_numbergen(self): is_numbergen = isinstance(self.t2.param.get_value_generator('x'), numbergen.UniformRandom) self.assertEqual(is_numbergen, True) def test_matching_numbergen_streams(self): "check that t2 and t3 have identical streams" self.assertEqual(self.t2.x, self.t3.x) def test_numbergen_objects_distinct(self): "check t2 and t3 do not share UniformRandom objects" self.t2.x self.assertNotEqual(self.t2.param.inspect_value('x'), self.t3.param.inspect_value('x')) def test_numbergen_inspect(self): " inspect_value() should return last generated value " self.t2.x # Call 1 self.t2.x # Call 2 t2_last_value = self.t2.x # advance t2 beyond t3 self.assertEqual(self.t2.param.inspect_value('x'), t2_last_value) # ensure last_value is not shared self.assertNotEqual(self.t3.param.inspect_value('x'), t2_last_value) def test_dynamic_value_instantiated(self): t6_first_value = self.t6.x self.assertNotEqual(self.t7.param.inspect_value('x'), t6_first_value) def test_non_dynamic_value_not_instantiated(self): " non-dynamic value not instantiated" self.TestPO2.y = 4 self.assertEqual(self.t6.y, 4) self.assertEqual(self.t7.y, 4) def test_dynamic_value_setting(self): self.t6.y = numbergen.UniformRandom() t8 = self.TestPO2() self.TestPO2.y = 10 # t6 got a dynamic value, but shouldn't have changed Parameter's instantiate self.assertEqual(t8.y, 10) def test_setting_y_param_numbergen(self): self.TestPO2.y=numbergen.UniformRandom() # now the Parameter instantiate should be true t9 = self.TestPO2() self.assertEqual('_y_param_value' in t9.__dict__, True) def test_shared_numbergen(self): """ Instances of TestPO2 that don't have their own value for the parameter share one UniformRandom object """ self.TestPO2.y=numbergen.UniformRandom() # now the Parameter instantiate should be true self.assertEqual(self.t7.param.get_value_generator('y') is self.TestPO2().param.params()['y'].default, True) self.assertEqual(self.TestPO2().param.params()['y'].default.__class__.__name__, 'UniformRandom') def test_copy_match(self): "check a copy is the same" t9 = copy.deepcopy(self.t7) self.assertEqual(t9.param.get_value_generator('y') is self.TestPO2().param.params()['y'].default, True) class TestDynamicTimeDependent(TestDynamicParameters): def setUp(self): super(TestDynamicTimeDependent, self).setUp() param.Dynamic.time_dependent = True class TestPO3(param.Parameterized): x = param.Dynamic(default=numbergen.UniformRandom(name='xgen', time_dependent=True)) class TestPO4(self.TestPO1): "Nested parameterized objects" z = param.Parameter(default=self.TestPO1()) self.TestPO3 = TestPO3 self.TestPO4 = TestPO4 self.t10 = self.TestPO1() self.t11 = TestPO3() def test_dynamic_values_unchanged_dependent(self): param.Dynamic.time_dependent = True call_1 = self.t10.x call_2 = self.t10.x call_3 = self.t10.x self.assertEqual(call_1, call_2) self.assertEqual(call_2, call_3) def test_dynamic_values_changed_independent(self): param.Dynamic.time_dependent = False call_1 = self.t10.x call_2 = self.t10.x call_3 = self.t10.x self.assertNotEqual(call_1, call_2) self.assertNotEqual(call_2, call_3) def test_dynamic_values_change(self): param.Dynamic.time_dependent = True with param.Dynamic.time_fn as t: t(0) call_1 = self.t10.x t += 1 call_2 = self.t10.x t(0) call_3 = self.t10.x self.assertNotEqual(call_1, call_2) self.assertNotEqual(call_1, call_3) def test_dynamic_values_time_dependent(self): param.Dynamic.time_dependent = True with param.Dynamic.time_fn as t: t(0) call_1 = self.t11.x t += 1 call_2 = self.t11.x t(0) call_3 = self.t11.x self.assertNotEqual(call_1, call_2) self.assertEqual(call_1, call_3) def test_class_dynamic_values_change(self): call_1 = self.TestPO3.x call_2 = self.TestPO3.x self.assertEqual(call_1, call_2) with param.Dynamic.time_fn as t: t += 1 call_3 = self.TestPO3.x self.assertNotEqual(call_2, call_3) def test_dynamic_value_change_independent(self): t12 = self.TestPO1() t12.param.set_dynamic_time_fn(None) self.assertNotEqual(t12.x, t12.x) self.assertEqual(t12.y, t12.y) def test_dynamic_value_change_disabled(self): " time_fn set on the UniformRandom() when t13.y was set" t13 = self.TestPO1() t13.param.set_dynamic_time_fn(None) t13.y = numbergen.UniformRandom() self.assertNotEqual(t13.y, t13.y) def test_dynamic_value_change_enabled(self): " time_fn set on the UniformRandom() when t13.y was set" t14 = self.TestPO1() t14.y = numbergen.UniformRandom() self.assertEqual(t14.y, t14.y) def test_dynamic_time_fn_not_inherited(self): " time_fn not inherited" t15 = self.TestPO4() t15.param.set_dynamic_time_fn(None) with param.Dynamic.time_fn as t: call_1 = t15.z.x t += 1 call_2 = t15.z.x self.assertNotEqual(call_1, call_2) class TestDynamicSharedNumbergen(TestDynamicParameters): "Check shared generator" def setUp(self): super(TestDynamicSharedNumbergen, self).setUp() self.shared = numbergen.UniformRandom(lbound=-1,ubound=1,seed=20) def test_dynamic_shared_numbergen(self): param.Dynamic.time_dependent = True t11 = self.TestPO1(x=self.shared) t12 = self.TestPO1(x=self.shared) with param.Dynamic.time_fn as t: t += 1 call_1 = t11.x self.assertEqual(call_1, t12.x) t += 1 self.assertNotEqual(call_1, t12.x) # Commented out block in the original doctest version. # Maybe these are features originally planned but never implemented """ It is not yet possible to set time_fn for a Parameter instance >>> class TestPO5(param.Parameterized): ... x = param.Dynamic(default=numbergen.UniformRandom(),dynamic_time_fn=None) """ """ We currently don't support iterators/generators in Dynamic unless they're wrapped. >>> i = iter([1,2,3]) >>> t11.x = i >>> topo.sim.run(1) >>> t11.x 1 >>> def gen(): ... yield 2 ... yield 4 ... yield 6 >>> g = gen() >>> t11.x = g >>> t11.x 2 >>> topo.sim.run(1) >>> t11.x 4 """ param-1.12.3/tests/API1/testipythonmagic.py000066400000000000000000000077321434441564000205150ustar00rootroot00000000000000""" Unit test for the IPython magic """ import re import sys import param from . import API1TestCase try: import IPython # noqa except ImportError: import os if os.getenv('PARAM_TEST_IPYTHON','0') == '1': raise ImportError("PARAM_TEST_IPYTHON=1 but ipython not available.") # TODO: is the below actually true? # SkipTest will be raised if IPython unavailable from param.ipython import ParamPager test1_repr = """\x1b[1;32mParameters of 'TestClass'\n=========================\n\x1b[0m\n\x1b[1;31mParameters changed from their default values are marked in red.\x1b[0m\n\x1b[1;36mSoft bound values are marked in cyan.\x1b[0m\nC/V= Constant/Variable, RO/RW = ReadOnly/ReadWrite, AN=Allow None\n\n\x1b[1;34mNameValue Type Bounds Mode \x1b[0m\n\nu 4 Number V RW \nw 4 Number C RO \nv 4 Number C RW \nx None String V RW AN \ny 4 Number (-1, None) V RW \nz 4 Number (-1, 100) V RW \n\n\x1b[1;32mParameter docstrings:\n=====================\x1b[0m\n\n\x1b[1;34mu: < No docstring available >\x1b[0m\n\x1b[1;31mw: < No docstring available >\x1b[0m\n\x1b[1;34mv: < No docstring available >\x1b[0m\n\x1b[1;31mx: < No docstring available >\x1b[0m\n\x1b[1;34my: < No docstring available >\x1b[0m\n\x1b[1;31mz: < No docstring available >\x1b[0m""" test2_repr = """\x1b[1;32mParameters of 'TestClass' instance\n==================================\n\x1b[0m\n\x1b[1;31mParameters changed from their default values are marked in red.\x1b[0m\n\x1b[1;36mSoft bound values are marked in cyan.\x1b[0m\nC/V= Constant/Variable, RO/RW = ReadOnly/ReadWrite, AN=Allow None\n\n\x1b[1;34mNameValue Type Bounds Mode \x1b[0m\n\nu 4 Number V RW \nw 4 Number C RO \nv 4 Number C RW \nx None String V RW AN \ny 4 Number (-1, None) V RW \nz 4 Number (-1, 100) V RW \n\n\x1b[1;32mParameter docstrings:\n=====================\x1b[0m\n\n\x1b[1;34mu: < No docstring available >\x1b[0m\n\x1b[1;31mw: < No docstring available >\x1b[0m\n\x1b[1;34mv: < No docstring available >\x1b[0m\n\x1b[1;31mx: < No docstring available >\x1b[0m\n\x1b[1;34my: < No docstring available >\x1b[0m\n\x1b[1;31mz: < No docstring available >\x1b[0m""" class TestParamPager(API1TestCase): def setUp(self): super(TestParamPager, self).setUp() self.maxDiff = None class TestClass(param.Parameterized): u = param.Number(4, precedence=0) w = param.Number(4, readonly=True, precedence=1) v = param.Number(4, constant=True, precedence=2) x = param.String(None, allow_None=True, precedence=3) y = param.Number(4, bounds=(-1, None), precedence=4) z = param.Number(4, bounds=(-1, 100), softbounds=(-100, -200), precedence=5) self.TestClass = TestClass self.pager = ParamPager() def test_parameterized_class(self): page_string = self.pager(self.TestClass) # Remove params automatic numbered names page_string = re.sub(r'TestClass(\d+)', 'TestClass', page_string) ref_string = re.sub(r'TestClass(\d+)', 'TestClass', test1_repr) try: self.assertEqual(page_string, ref_string) except Exception as e: sys.stderr.write(page_string) # Coloured output sys.stderr.write("\nRAW STRING:\n\n%r\n\n" % page_string) raise e def test_parameterized_instance(self): page_string = self.pager(self.TestClass()) # Remove params automatic numbered names page_string = re.sub(r'TestClass(\d+)', 'TestClass', page_string) ref_string = re.sub(r'TestClass(\d+)', 'TestClass', test2_repr) try: self.assertEqual(page_string, ref_string) except Exception as e: sys.stderr.write(page_string) # Coloured output sys.stderr.write("\nRAW STRING:\n\n%r\n\n" % page_string) raise e param-1.12.3/tests/API1/testjsonserialization.py000066400000000000000000000322761434441564000215720ustar00rootroot00000000000000""" Testing JSON serialization of parameters and the corresponding schemas. """ import datetime import json import sys import param from unittest import SkipTest, skipIf from . import API1TestCase try: from jsonschema import validate, ValidationError except ImportError: import os if os.getenv('PARAM_TEST_JSONSCHEMA','0') == '1': raise ImportError("PARAM_TEST_JSONSCHEMA=1 but jsonschema not available.") validate = None now = datetime.datetime.now() after_now = now + datetime.timedelta(days=1) try: import numpy as np ndarray = np.array([[1,2,3],[4,5,6]]) npdt1 = np.datetime64(now) npdt2 = np.datetime64(after_now) except: np, ndarray, npdt1, npdt2 = None, None, None, None np_skip = skipIf(np is None, "NumPy is not available") on_py2 = sys.version_info[0] == 2 py2_skip = skipIf(on_py2, "Ignore Python 2") try: import pandas as pd df1 = pd.DataFrame({'A':[1,2,3], 'B':[1.1,2.2,3.3]}) df2 = pd.DataFrame({'A':[1.1,2.2,3.3], 'B':[1.1,2.2,3.3]}) pdts1 = pd.Timestamp(now) pdts2 = pd.Timestamp(after_now) except: pd, df1, df2, pdts1, pdts2 = None, None, None, None, None pd_skip = skipIf(pd is None, "pandas is not available") simple_list = [1] class TestSet(param.Parameterized): __test__ = False numpy_params = ['r','y'] pandas_params = ['s','t','u','z'] conditionally_unsafe = ['f', 'o'] a = param.Integer(default=5, doc='Example doc', bounds=(2,30), inclusive_bounds=(True, False)) b = param.Number(default=4.3, allow_None=True) c = param.String(default='foo') d = param.Boolean(default=False) e = param.List([1,2,3], class_=int) f = param.List([1,2,3]) g = param.Date(default=datetime.datetime.now()) g2 = None if (np is None or on_py2) else param.Date(default=npdt1) g3 = None if pd is None else param.Date(default=pdts1) h = param.Tuple(default=(1,2,3), length=3) i = param.NumericTuple(default=(1,2,3,4)) j = param.XYCoordinates(default=(32.1, 51.5)) k = param.Integer(default=1) l = param.Range(default=(1.1,2.3), bounds=(1,3)) m = param.String(default='baz', allow_None=True) n = param.ObjectSelector(default=3, objects=[3,'foo'], allow_None=False) o = param.ObjectSelector(default=simple_list, objects=[simple_list], allow_None=False) p = param.ListSelector(default=[1,4,5], objects=[1,2,3,4,5,6]) q = param.CalendarDate(default=datetime.date.today()) r = None if np is None else param.Array(default=ndarray) s = None if pd is None else param.DataFrame(default=df1, columns=2) t = None if pd is None else param.DataFrame(default=pd.DataFrame( {'A':[1,2,3], 'B':[1.1,2.2,3.3]}), columns=(1,4), rows=(2,5)) u = None if pd is None else param.DataFrame(default=df2, columns=['A', 'B']) v = param.Dict({'1':2}) w = param.Date(default=None, allow_None=True) x = param.CalendarDate(default=None, allow_None=True) y = None if np is None else param.Array(default=None) z = None if pd is None else param.DataFrame(default=None, allow_None=True) aa = param.Tuple(default=None, allow_None=True, length=1) ab = param.CalendarDateRange(default=( datetime.date(2020, 1, 1), datetime.date(2021, 1, 1) )) ac = param.DateRange(default=( datetime.date(2020, 1, 1), datetime.date(2021, 1, 1) )) ad = param.DateRange(default=( datetime.datetime(2020, 1, 1, 1, 1, 1, 1), datetime.datetime(2021, 1, 1, 1, 1, 1, 1) )) # datetime.datetime comparison with numpy.datetime64 fails on Python 2 ae = None if (np is None or on_py2) else param.DateRange(default=(npdt1, npdt2)) af = None if pd is None else param.DateRange(default=(pdts1, pdts2)) test = TestSet(a=29) class TestSerialization(API1TestCase): """ Base class for testing serialization of Parameter values """ mode = None __test__ = False def _test_serialize(self, obj, pname): serialized = obj.param.serialize_value(pname, mode=self.mode) deserialized = obj.param.deserialize_value(pname, serialized, mode=self.mode) self.assertEqual(deserialized, getattr(obj, pname)) def test_serialize_integer_class(self): self._test_serialize(TestSet, 'a') def test_serialize_integer_instance(self): self._test_serialize(test, 'a') def test_serialize_number_class(self): self._test_serialize(TestSet, 'b') def test_serialize_number_instance(self): self._test_serialize(test, 'b') def test_serialize_string_class(self): self._test_serialize(TestSet, 'c') def test_serialize_string_instance(self): self._test_serialize(test, 'c') def test_serialize_boolean_class(self): self._test_serialize(TestSet, 'd') def test_serialize_boolean_instance(self): self._test_serialize(test, 'd') def test_serialize_list_class(self): self._test_serialize(TestSet, 'e') def test_serialize_list_instance(self): self._test_serialize(test, 'e') def test_serialize_date_class(self): self._test_serialize(TestSet, 'g') def test_serialize_date_instance(self): self._test_serialize(test, 'g') @py2_skip @np_skip def test_serialize_date_numpy_class(self): self._test_serialize(TestSet, 'g2') @py2_skip @np_skip def test_serialize_date_numpy_instance(self): self._test_serialize(test, 'g2') @pd_skip def test_serialize_date_pandas_class(self): self._test_serialize(TestSet, 'g3') @pd_skip def test_serialize_date_pandas_instance(self): self._test_serialize(test, 'g3') def test_serialize_tuple_class(self): self._test_serialize(TestSet, 'h') def test_serialize_tuple_instance(self): self._test_serialize(test, 'h') def test_serialize_calendar_date_class(self): self._test_serialize(TestSet, 'q') def test_serialize_calendar_date_instance(self): self._test_serialize(test, 'q') @np_skip def test_serialize_array_class(self): serialized = TestSet.param.serialize_value('r', mode=self.mode) deserialized = TestSet.param.deserialize_value('r', serialized, mode=self.mode) self.assertTrue(np.array_equal(deserialized, getattr(TestSet, 'r'))) @np_skip def test_serialize_array_instance(self): serialized = test.param.serialize_value('r', mode=self.mode) deserialized = test.param.deserialize_value('r', serialized, mode=self.mode) self.assertTrue(np.array_equal(deserialized, getattr(test, 'r'))) @pd_skip def test_serialize_dataframe_class(self): serialized = TestSet.param.serialize_value('s', mode=self.mode) deserialized = TestSet.param.deserialize_value('s', serialized, mode=self.mode) self.assertTrue(getattr(TestSet, 's').equals(deserialized)) @pd_skip def test_serialize_dataframe_instance(self): serialized = test.param.serialize_value('s', mode=self.mode) deserialized = test.param.deserialize_value('s', serialized, mode=self.mode) self.assertTrue(getattr(test, 's').equals(deserialized)) def test_serialize_dict_class(self): self._test_serialize(TestSet, 'v') def test_serialize_dict_instance(self): self._test_serialize(test, 'v') def test_instance_serialization(self): parameters = [p for p in test.param if p not in test.numpy_params + test.pandas_params] serialized = test.param.serialize_parameters(subset=parameters, mode=self.mode) deserialized = TestSet.param.deserialize_parameters(serialized, mode=self.mode) for pname in parameters: self.assertEqual(deserialized[pname], getattr(test, pname)) @np_skip def test_numpy_instance_serialization(self): serialized = test.param.serialize_parameters(subset=test.numpy_params, mode=self.mode) deserialized = TestSet.param.deserialize_parameters(serialized, mode=self.mode) for pname in test.numpy_params: self.assertTrue(np.array_equal(deserialized[pname], getattr(test, pname))) @pd_skip def test_pandas_instance_serialization(self): serialized = test.param.serialize_parameters(subset=test.pandas_params, mode=self.mode) deserialized = TestSet.param.deserialize_parameters(serialized, mode=self.mode) for pname in test.pandas_params: if getattr(test, pname) is None: self.assertTrue(deserialized[pname] is None) else: self.assertTrue(getattr(test, pname).equals(deserialized[pname])) def test_serialize_calendar_date_range_class(self): self._test_serialize(TestSet, 'ab') def test_serialize_calendar_date_range_instance(self): self._test_serialize(test, 'ab') def test_serialize_date_range_class(self): self._test_serialize(TestSet, 'ac') def test_serialize_date_range_instance(self): self._test_serialize(test, 'ac') def test_serialize_datetime_range_class(self): self._test_serialize(TestSet, 'ad') def test_serialize_datetime_range_instance(self): self._test_serialize(test, 'ad') @py2_skip @np_skip def test_serialize_datetime_range_numpy_class(self): self._test_serialize(TestSet, 'ae') @py2_skip @np_skip def test_serialize_datetime_range_numpy_instance(self): self._test_serialize(test, 'ae') @pd_skip def test_serialize_datetime_range_pandas_class(self): self._test_serialize(TestSet, 'af') @pd_skip def test_serialize_datetime_range_pandas_instance(self): self._test_serialize(test, 'af') class TestJSONSerialization(TestSerialization): mode = 'json' __test__ = True class TestJSONSchema(API1TestCase): def test_serialize_integer_schema_class(self): if validate is None: raise SkipTest('jsonschema needed for schema validation testing') param_schema = TestSet.param.schema(safe=True, subset=['a'], mode='json') schema = {"type" : "object", "properties" : param_schema} serialized = json.loads(TestSet.param.serialize_parameters(subset=['a'])) self.assertEqual({'a': {'type': 'integer', 'minimum': 2, 'exclusiveMaximum': 30, 'description': 'Example doc', 'title': 'A'}}, param_schema) validate(instance=serialized, schema=schema) def test_serialize_integer_schema_class_invalid(self): if validate is None: raise SkipTest('jsonschema needed for schema validation testing') param_schema = TestSet.param.schema(safe=True, subset=['a'], mode='json') schema = {"type" : "object", "properties" : param_schema} self.assertEqual({'a': {'type': 'integer', 'minimum': 2, 'exclusiveMaximum': 30, 'description': 'Example doc', 'title': 'A'}}, param_schema) exception = "1 is not of type 'object'" with self.assertRaisesRegex(ValidationError, exception): validate(instance=1, schema=schema) def test_serialize_integer_schema_instance(self): if validate is None: raise SkipTest('jsonschema needed for schema validation testing') param_schema = test.param.schema(safe=True, subset=['a'], mode='json') schema = {"type" : "object", "properties" : param_schema} serialized = json.loads(test.param.serialize_parameters(subset=['a'])) self.assertEqual({'a': {'type': 'integer', 'minimum': 2, 'exclusiveMaximum': 30, 'description': 'Example doc', 'title': 'A'}}, param_schema) validate(instance=serialized, schema=schema) @np_skip def test_numpy_schemas_always_unsafe(self): for param_name in test.numpy_params: with self.assertRaisesRegex(param.serializer.UnsafeserializableException,''): test.param.schema(safe=True, subset=[param_name], mode='json') @pd_skip def test_pandas_schemas_always_unsafe(self): for param_name in test.pandas_params: with self.assertRaisesRegex(param.serializer.UnsafeserializableException,''): test.param.schema(safe=True, subset=[param_name], mode='json') def test_class_instance_schemas_match_and_validate_unsafe(self): if validate is None: raise SkipTest('jsonschema needed for schema validation testing') for param_name in list(test.param): class_schema = TestSet.param.schema(safe=False, subset=[param_name], mode='json') instance_schema = test.param.schema(safe=False, subset=[param_name], mode='json') self.assertEqual(class_schema, instance_schema) instance_serialization_val = test.param.serialize_parameters(subset=[param_name]) validate(instance=instance_serialization_val, schema=class_schema) class_serialization_val = TestSet.param.serialize_parameters(subset=[param_name]) validate(instance=class_serialization_val, schema=class_schema) def test_conditionally_unsafe(self): for param_name in test.conditionally_unsafe: with self.assertRaisesRegex(param.serializer.UnsafeserializableException,''): test.param.schema(safe=True, subset=[param_name], mode='json') param-1.12.3/tests/API1/testlist.py000066400000000000000000000030101434441564000167560ustar00rootroot00000000000000import param from . import API1TestCase # TODO: I copied the tests from testobjectselector, although I # struggled to understand some of them. Both files should be reviewed # and cleaned up together. # TODO: tests copied from testobjectselector could use assertRaises # context manager (and could be updated in testobjectselector too). class TestListParameters(API1TestCase): def setUp(self): super(TestListParameters, self).setUp() class P(param.Parameterized): e = param.List([5,6,7], item_type=int) l = param.List(["red","green","blue"], item_type=str, bounds=(0,10)) self.P = P def test_default_None(self): class Q(param.Parameterized): r = param.List(default=[]) # Also check None) def test_set_object_constructor(self): p = self.P(e=[6]) self.assertEqual(p.e, [6]) def test_set_object_outside_bounds(self): p = self.P() try: p.l=[6]*11 except ValueError: pass else: raise AssertionError("Object set outside range.") def test_set_object_wrong_type(self): p = self.P() try: p.e=['s'] except TypeError: pass else: raise AssertionError("Object allowed of wrong type.") def test_set_object_not_None(self): p = self.P(e=[6]) try: p.e = None except ValueError: pass else: raise AssertionError("Object set outside range.") param-1.12.3/tests/API1/testlistselector.py000066400000000000000000000113541434441564000205310ustar00rootroot00000000000000import param from . import API1TestCase # TODO: I copied the tests from testobjectselector, although I # struggled to understand some of them. Both files should be reviewed # and cleaned up together. # TODO: tests copied from testobjectselector could use assertRaises # context manager (and could be updated in testobjectselector too). class TestListSelectorParameters(API1TestCase): def setUp(self): super(TestListSelectorParameters, self).setUp() class P(param.Parameterized): e = param.ListSelector(default=[5],objects=[5,6,7]) f = param.ListSelector(default=10) h = param.ListSelector(default=None) g = param.ListSelector(default=None,objects=[7,8]) i = param.ListSelector(default=[7],objects=[9],check_on_set=False) self.P = P def test_default_None(self): class Q(param.Parameterized): r = param.ListSelector(default=None) def test_set_object_constructor(self): p = self.P(e=[6]) self.assertEqual(p.e, [6]) def test_set_object_outside_bounds(self): p = self.P(e=[6]) try: p.e = [9] except ValueError: pass else: raise AssertionError("Object set outside range.") def test_set_object_setattr(self): p = self.P(e=[6]) p.f = [9] self.assertEqual(p.f, [9]) p.g = [7] self.assertEqual(p.g, [7]) p.i = [12] self.assertEqual(p.i, [12]) def test_set_object_not_None(self): p = self.P(e=[6]) p.g = [7] try: p.g = None except TypeError: pass else: raise AssertionError("Object set outside range.") def test_set_one_object_not_None(self): p = self.P(e=[6]) p.g = [7] try: p.g = [None] except ValueError: pass else: raise AssertionError("Object set outside range.") def test_set_object_setattr_post_error(self): p = self.P(e=[6]) p.f = [9] self.assertEqual(p.f, [9]) p.g = [7] try: p.g = [None] except ValueError: pass else: raise AssertionError("Object set outside range.") self.assertEqual(p.g, [7]) p.i = [12] self.assertEqual(p.i, [12]) def test_initialization_out_of_bounds(self): try: class Q(param.Parameterized): q = param.ListSelector([5],objects=[4]) except ValueError: pass else: raise AssertionError("ListSelector created outside range.") def test_initialization_no_bounds(self): try: class Q(param.Parameterized): q = param.ListSelector([5],objects=10) except TypeError: pass else: raise AssertionError("ListSelector created without range.") ################################################################## ################################################################## ### new tests (not copied from testobjectselector) def test_bad_default(self): with self.assertRaises(TypeError): class Q(param.Parameterized): r = param.ListSelector(default=6,check_on_set=True) def test_implied_check_on_set(self): with self.assertRaises(TypeError): class Q(param.Parameterized): r = param.ListSelector(default=7,objects=[7,8]) def test_default_not_checked(self): class Q(param.Parameterized): r = param.ListSelector(default=[6]) ########################## # CEBALERT: not sure it makes sense for ListSelector to be set to # a non-iterable value (except None). I.e. I think this first test # should fail. def test_default_not_checked_to_be_iterable(self): class Q(param.Parameterized): r = param.ListSelector(default=6) def test_set_checked_to_be_iterable(self): class Q(param.Parameterized): r = param.ListSelector(default=6,check_on_set=False) with self.assertRaises(TypeError): Q.r = 6 ########################## def test_compute_default(self): class Q(param.Parameterized): r = param.ListSelector(default=None, compute_default_fn=lambda: [1,2,3]) self.assertEqual(Q.r, None) Q.param.params('r').compute_default() self.assertEqual(Q.r, [1,2,3]) self.assertEqual(Q.param.params('r').objects, [1,2,3]) def test_bad_compute_default(self): class Q(param.Parameterized): r = param.ListSelector(default=None,compute_default_fn=lambda:1) with self.assertRaises(TypeError): Q.param.params('r').compute_default() param-1.12.3/tests/API1/testnumbergen.py000066400000000000000000000015711434441564000177770ustar00rootroot00000000000000""" Test cases for the numbergen module. """ import numbergen from . import API1TestCase _seed = 0 # keep tests deterministic _iterations = 1000 class TestUniformRandom(API1TestCase): def test_range(self): lbound = 2.0 ubound = 5.0 gen = numbergen.UniformRandom( seed=_seed, lbound=lbound, ubound=ubound) for _ in range(_iterations): value = gen() self.assertTrue(lbound <= value < ubound) class TestUniformRandomOffset(API1TestCase): def test_range(self): lbound = 2.0 ubound = 5.0 gen = numbergen.UniformRandomOffset( seed=_seed, mean=(ubound + lbound) / 2, range=ubound - lbound) for _ in range(_iterations): value = gen() self.assertTrue(lbound <= value < ubound) param-1.12.3/tests/API1/testnumberparameter.py000066400000000000000000000035431434441564000212070ustar00rootroot00000000000000""" Unit test for Number parameters and their subclasses. """ import param import datetime as dt from . import API1TestCase class TestNumberParameters(API1TestCase): def test_initialization_without_step_class(self): class Q(param.Parameterized): q = param.Number(default=1) self.assertEqual(Q.param['q'].step, None) def test_initialization_with_step_class(self): class Q(param.Parameterized): q = param.Number(default=1, step=0.5) self.assertEqual(Q.param['q'].step, 0.5) def test_initialization_without_step_instance(self): class Q(param.Parameterized): q = param.Number(default=1) qobj = Q() self.assertEqual(qobj.param['q'].step, None) def test_initialization_with_step_instance(self): class Q(param.Parameterized): q = param.Number(default=1, step=0.5) qobj = Q() self.assertEqual(qobj.param['q'].step, 0.5) def test_step_invalid_type_number_parameter(self): exception = "Step can only be None or a numeric value" with self.assertRaisesRegex(ValueError, exception): param.Number(step='invalid value') def test_step_invalid_type_integer_parameter(self): exception = "Step can only be None or an integer value" with self.assertRaisesRegex(ValueError, exception): param.Integer(step=3.4) def test_step_invalid_type_datetime_parameter(self): exception = "Step can only be None, a datetime or datetime type" with self.assertRaisesRegex(ValueError, exception): param.Date(dt.datetime(2017,2,27), step=3.2) def test_step_invalid_type_date_parameter(self): exception = "Step can only be None or a date type" with self.assertRaisesRegex(ValueError, exception): param.CalendarDate(dt.date(2017,2,27), step=3.2) param-1.12.3/tests/API1/testnumpy.py000066400000000000000000000020351434441564000171610ustar00rootroot00000000000000""" If numpy's present, is numpy stuff ok? """ import unittest import os import param from . import API1TestCase try: import numpy import numpy.testing except ImportError: if os.getenv('PARAM_TEST_NUMPY','0') == '1': raise ImportError("PARAM_TEST_NUMPY=1 but numpy not available.") else: raise unittest.SkipTest("numpy not available") def _is_array_and_equal(test,ref): if not type(test) == numpy.ndarray: raise AssertionError numpy.testing.assert_array_equal(test,ref) # TODO: incomplete class TestNumpy(API1TestCase): def test_array_param(self): class Z(param.Parameterized): z = param.Array(default=numpy.array([1])) _is_array_and_equal(Z.z,[1]) z = Z(z=numpy.array([1,2])) _is_array_and_equal(z.z,[1,2]) def test_array_param_positional(self): class Z(param.Parameterized): z = param.Array(numpy.array([1])) _is_array_and_equal(Z.z,[1]) z = Z(z=numpy.array([1,2])) _is_array_and_equal(z.z,[1,2]) param-1.12.3/tests/API1/testobjectselector.py000066400000000000000000000073041434441564000210240ustar00rootroot00000000000000""" Unit test for object selector parameters. Originally implemented as doctests in Topographica in the file testEnumerationParameter.txt """ import param from . import API1TestCase from collections import OrderedDict opts=dict(A=[1,2],B=[3,4],C=dict(a=1,b=2)) class TestObjectSelectorParameters(API1TestCase): def setUp(self): super(TestObjectSelectorParameters, self).setUp() class P(param.Parameterized): e = param.Selector(default=5,objects=[5,6,7]) f = param.Selector(default=10) h = param.Selector(default=None) g = param.Selector(default=None,objects=[7,8]) i = param.Selector(default=7,objects=[9],check_on_set=False) s = param.Selector(default=3,objects=OrderedDict(one=1,two=2,three=3)) d = param.Selector(default=opts['B'],objects=opts) self.P = P def test_set_object_constructor(self): p = self.P(e=6) self.assertEqual(p.e, 6) def test_get_range_list(self): r = self.P.param.params("g").get_range() self.assertEqual(r['7'],7) self.assertEqual(r['8'],8) def test_get_range_dict(self): r = self.P.param.params("s").get_range() self.assertEqual(r['one'],1) self.assertEqual(r['two'],2) def test_get_range_mutable(self): r = self.P.param.params("d").get_range() self.assertEqual(r['A'],opts['A']) self.assertEqual(r['C'],opts['C']) self.d=opts['A'] self.d=opts['C'] self.d=opts['B'] def test_set_object_outside_bounds(self): p = self.P(e=6) try: p.e = 9 except ValueError: pass else: raise AssertionError("Object set outside range.") def test_set_object_setattr(self): p = self.P(e=6) p.f = 9 self.assertEqual(p.f, 9) p.g = 7 self.assertEqual(p.g, 7) p.i = 12 self.assertEqual(p.i, 12) def test_set_object_not_None(self): p = self.P(e=6) p.g = 7 try: p.g = None except ValueError: pass else: raise AssertionError("Object set outside range.") def test_set_object_setattr_post_error(self): p = self.P(e=6) p.f = 9 self.assertEqual(p.f, 9) p.g = 7 try: p.g = None except ValueError: pass else: raise AssertionError("Object set outside range.") self.assertEqual(p.g, 7) p.i = 12 self.assertEqual(p.i, 12) def test_initialization_out_of_bounds(self): try: class Q(param.Parameterized): q = param.Selector(default=5,objects=[4]) except ValueError: pass else: raise AssertionError("ObjectSelector created outside range.") def test_initialization_no_bounds(self): try: class Q(param.Parameterized): q = param.Selector(default=5,objects=10) except TypeError: pass else: raise AssertionError("ObjectSelector created without range.") def test_initialization_out_of_bounds_objsel(self): try: class Q(param.Parameterized): q = param.ObjectSelector(5,objects=[4]) except ValueError: pass else: raise AssertionError("ObjectSelector created outside range.") def test_initialization_no_bounds_objsel(self): try: class Q(param.Parameterized): q = param.ObjectSelector(5,objects=10) except TypeError: pass else: raise AssertionError("ObjectSelector created without range.") param-1.12.3/tests/API1/testpandas.py000066400000000000000000000206471434441564000172700ustar00rootroot00000000000000""" Test Parameters based on pandas """ import unittest import os import param from . import API1TestCase try: import pandas except ImportError: if os.getenv('PARAM_TEST_PANDAS','0') == '1': raise ImportError("PARAM_TEST_PANDAS=1 but pandas not available.") else: raise unittest.SkipTest("pandas not available") class TestDataFrame(API1TestCase): def test_dataframe_positional_argument(self): valid_df = pandas.DataFrame({'a':[1,2], 'b':[2,3], 'c':[4,5]}, columns=['b', 'a', 'c']) class Test(param.Parameterized): df = param.DataFrame(valid_df) def test_dataframe_allow_none(self): class Test(param.Parameterized): df = param.DataFrame(default=None, rows=3) test = Test() self.assertIs(test.df, None) def test_dataframe_allow_none_constructor(self): class Test(param.Parameterized): df = param.DataFrame(allow_None=True, rows=3) test = Test(df=None) self.assertIs(test.df, None) def test_dataframe_allow_none_set_value(self): class Test(param.Parameterized): df = param.DataFrame(allow_None=True, rows=3) test = Test() test.df = None self.assertIs(test.df, None) def test_empty_dataframe_param_invalid_set(self): empty = pandas.DataFrame() class Test(param.Parameterized): df = param.DataFrame(default=empty) test = Test() exception = "DataFrame parameter 'df' value must be an instance of DataFrame, not 3." with self.assertRaisesRegex(ValueError, exception): test.df = 3 def test_dataframe_unordered_column_set_valid(self): valid_df = pandas.DataFrame({'a':[1,2], 'b':[2,3], 'c':[4,5]}, columns=['b', 'a', 'c']) class Test(param.Parameterized): df = param.DataFrame(default=valid_df, columns={'a', 'b'}) def test_dataframe_unordered_column_set_invalid(self): valid_df = pandas.DataFrame({'a':[1,2], 'b':[2,3], 'd':[4,5]}, columns=['b', 'a', 'd']) invalid_df = pandas.DataFrame({'a':[1,2], 'b':[2,3], 'c':[4,5]}, columns=['b', 'a', 'c']) class Test(param.Parameterized): df = param.DataFrame(default=valid_df, columns={'a', 'd'}) test = Test() self.assertEqual(test.param.params('df').ordered, False) exception = r"Provided DataFrame columns \['b', 'a', 'c'\] does not contain required columns \['a', 'd'\]" with self.assertRaisesRegex(ValueError, exception): test.df = invalid_df def test_dataframe_ordered_column_list_valid(self): valid_df = pandas.DataFrame({'a':[1,2], 'b':[2,3], 'c':[4,5]}, columns=['b', 'a', 'c']) class Test(param.Parameterized): test = param.DataFrame(default=valid_df, columns=['b', 'a', 'c']) def test_dataframe_ordered_column_list_invalid(self): valid_df = pandas.DataFrame({'a':[1,2], 'b':[2,3], 'd':[4,5]}, columns=['b', 'a', 'd']) invalid_df = pandas.DataFrame({'a':[1,2], 'b':[2,3], 'c':[4,5]}, columns=['a', 'b', 'd']) class Test(param.Parameterized): df = param.DataFrame(default=valid_df, columns=['b', 'a', 'd']) test = Test() self.assertEqual(test.param.params('df').ordered, True) exception = r"Provided DataFrame columns \['a', 'b', 'd'\] must exactly match \['b', 'a', 'd'\]" with self.assertRaisesRegex(ValueError, exception): test.df = invalid_df def test_dataframe_unordered_column_number_valid_df(self): valid_df = pandas.DataFrame({'a':[1,2], 'b':[2,3], 'c':[4,5]}, columns=['b', 'a', 'c']) class Test(param.Parameterized): df = param.DataFrame(default=valid_df, columns=3) def test_dataframe_unordered_column_number_invalid(self): valid_df = pandas.DataFrame({'a':[1,2], 'b':[2,3], 'c':[4,5]}, columns=['b', 'a', 'c']) invalid_df = pandas.DataFrame({'a':[1,2], 'b':[2,3]}, columns=['b', 'a']) class Test(param.Parameterized): df = param.DataFrame(default=valid_df, columns=3) test = Test() self.assertEqual(test.param.params('df').ordered, None) exception = "Column length 2 does not match declared bounds of 3" with self.assertRaisesRegex(ValueError, exception): test.df = invalid_df def test_dataframe_unordered_column_tuple_valid(self): valid_df = pandas.DataFrame({'a':[1,2], 'b':[2,3], 'c':[4,5]}, columns=['b', 'a', 'c']) class Test(param.Parameterized): df = param.DataFrame(default=valid_df, columns=(None,3)) def test_dataframe_unordered_column_tuple_invalid(self): invalid_df = pandas.DataFrame({'a':[1,2], 'b':[2,3], 'c':[4,5]}, columns=['b', 'a', 'c']) exception = r"Columns length 3 does not match declared bounds of \(None, 2\)" with self.assertRaisesRegex(ValueError, exception): class Test(param.Parameterized): df = param.DataFrame(default=invalid_df, columns=(None,2)) def test_dataframe_row_number_valid_df(self): valid_df = pandas.DataFrame({'a':[1,2], 'b':[2,3], 'c':[4,5]}, columns=['b', 'a', 'c']) class Test(param.Parameterized): df = param.DataFrame(default=valid_df, rows=2) def test_dataframe_row_number_invalid(self): valid_df = pandas.DataFrame({'a':[1,2], 'b':[2,3]}, columns=['b', 'a']) invalid_df = pandas.DataFrame({'a':[1,2,4], 'b':[2,3,4]}, columns=['b', 'a']) class Test(param.Parameterized): df = param.DataFrame(default=valid_df, rows=2) test = Test() exception = "Row length 3 does not match declared bounds of 2" with self.assertRaisesRegex(ValueError, exception): test.df = invalid_df def test_dataframe_unordered_row_tuple_valid(self): valid_df = pandas.DataFrame({'a':[1,2], 'b':[2,3], 'c':[4,5]}, columns=['b', 'a', 'c']) class Test(param.Parameterized): df = param.DataFrame(default=valid_df, rows=(None,3)) def test_dataframe_unordered_row_tuple_invalid(self): invalid_df = pandas.DataFrame({'a':[1,2], 'b':[2,3], 'c':[4,5]}, columns=['b', 'a', 'c']) exception = r"Row length 2 does not match declared bounds of \(5, 7\)" with self.assertRaisesRegex(ValueError, exception): class Test(param.Parameterized): df = param.DataFrame(default=invalid_df, rows=(5,7)) class TestSeries(API1TestCase): def test_series_positional_argument(self): valid_series = pandas.Series([1,2]) class Test(param.Parameterized): series = param.Series(valid_series, rows=2) def test_series_row_number_valid(self): valid_series = pandas.Series([1,2]) class Test(param.Parameterized): series = param.Series(default=valid_series, rows=2) def test_series_row_number_invalid(self): valid_series = pandas.Series([1,2]) invalid_series = pandas.Series([1,2,3]) class Test(param.Parameterized): series = param.Series(default=valid_series, rows=2) test = Test() exception = "Row length 3 does not match declared bounds of 2" with self.assertRaisesRegex(ValueError, exception): test.series = invalid_series def test_series_unordered_row_tuple_valid(self): valid_series = pandas.Series([1,2,3]) class Test(param.Parameterized): series = param.Series(default=valid_series, rows=(None,3)) def test_series_unordered_row_tuple_invalid(self): invalid_series = pandas.Series([1,2]) exception = r"Row length 2 does not match declared bounds of \(5, 7\)" with self.assertRaisesRegex(ValueError, exception): class Test(param.Parameterized): series = param.Series(default=invalid_series, rows=(5,7)) def test_series_allow_none(self): class Test(param.Parameterized): series = param.Series(default=None, rows=3) test = Test() self.assertIs(test.series, None) def test_series_allow_none_constructor(self): class Test(param.Parameterized): series = param.Series(allow_None=True, rows=3) test = Test(series=None) self.assertIs(test.series, None) def test_series_allow_none_set_value(self): class Test(param.Parameterized): series = param.Series(allow_None=True, rows=3) test = Test() test.series = None self.assertIs(test.series, None) param-1.12.3/tests/API1/testparamdepends.py000066400000000000000000000604761434441564000204710ustar00rootroot00000000000000""" Unit test for param.depends. """ import sys import param import pytest from param.parameterized import _parse_dependency_spec from . import API1TestCase try: import asyncio except ImportError: asyncio = None def async_executor(func): # Could be entirely replaced by asyncio.run(func()) in Python >=3.7 try: loop = asyncio.get_event_loop() except RuntimeError: loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) loop.run_until_complete(func()) class TestDependencyParser(API1TestCase): def test_parameter_value(self): obj, attr, what = _parse_dependency_spec('parameter') assert obj is None assert attr == 'parameter' assert what == 'value' def test_parameter_attribute(self): obj, attr, what = _parse_dependency_spec('parameter:constant') assert obj is None assert attr == 'parameter' assert what == 'constant' def test_subobject_parameter(self): obj, attr, what = _parse_dependency_spec('subobject.parameter') assert obj == '.subobject' assert attr == 'parameter' assert what == 'value' def test_subobject_parameter_attribute(self): obj, attr, what = _parse_dependency_spec('subobject.parameter:constant') assert obj == '.subobject' assert attr == 'parameter' assert what == 'constant' def test_sub_subobject_parameter(self): obj, attr, what = _parse_dependency_spec('subobject.subsubobject.parameter') assert obj == '.subobject.subsubobject' assert attr == 'parameter' assert what == 'value' def test_sub_subobject_parameter_attribute(self): obj, attr, what = _parse_dependency_spec('subobject.subsubobject.parameter:constant') assert obj == '.subobject.subsubobject' assert attr == 'parameter' assert what == 'constant' class TestParamDependsSubclassing(API1TestCase): def test_param_depends_override_depends_subset(self): class A(param.Parameterized): a = param.Parameter() b = param.Parameter() @param.depends('a', 'b', watch=True) def test(self): pass a = A() assert len(a.param.params_depended_on('test')) == 2 class B(A): @param.depends('b') def test(self): pass b = B() assert len(b.param.params_depended_on('test')) == 1 assert len(B.param._depends['watch']) == 0 def test_param_depends_override_depends_subset_watched(self): class A(param.Parameterized): a = param.Parameter() b = param.Parameter() @param.depends('a', 'b', watch=True) def test(self): pass a = A() assert len(a.param.params_depended_on('test')) == 2 class B(A): @param.depends('b', watch=True) def test(self): pass b = B() assert len(b.param.params_depended_on('test')) == 1 assert len(B.param._depends['watch']) == 1 m, _, _, deps, _ = B.param._depends['watch'][0] assert m == 'test' assert len(deps) == 1 assert deps[0].name == 'b' def test_param_depends_override_all_depends(self): class A(param.Parameterized): a = param.Parameter() b = param.Parameter() @param.depends('a', 'b', watch=True) def test(self): pass a = A() assert len(a.param.params_depended_on('test')) == 2 class B(A): def test(self): pass b = B() assert len(b.param.params_depended_on('test')) == 3 assert len(B.param._depends['watch']) == 0 def test_param_depends_subclass_ordering(self): values = [] class A(param.Parameterized): a = param.String() @param.depends('a', watch=True) def test(self): values.append(self.a) class B(A): @param.depends('a', watch=True) def more_test(self): values.append(self.a.upper()) b = B() b.a = 'a' assert values == ['a', 'A'] class TestParamDepends(API1TestCase): def setUp(self): class P(param.Parameterized): a = param.Parameter() b = param.Parameter() single_count = param.Integer() attr_count = param.Integer() single_nested_count = param.Integer() double_nested_count = param.Integer() nested_attr_count = param.Integer() nested_count = param.Integer() @param.depends('a', watch=True) def single_parameter(self): self.single_count += 1 @param.depends('a:constant', watch=True) def constant(self): self.attr_count += 1 @param.depends('b.a', watch=True) def single_nested(self): self.single_nested_count += 1 @param.depends('b.b.a', watch=True) def double_nested(self): self.double_nested_count += 1 @param.depends('b.a:constant', watch=True) def nested_attribute(self): self.nested_attr_count += 1 @param.depends('b.param', watch=True) def nested(self): self.nested_count += 1 class P2(param.Parameterized): @param.depends(P.param.a) def external_param(self, a): pass self.P = P self.P2 = P2 def test_param_depends_on_init(self): class A(param.Parameterized): a = param.Parameter() value = param.Integer() @param.depends('a', watch=True, on_init=True) def callback(self): self.value += 1 a = A() assert a.value == 1 a.a = True assert a.value == 2 def test_param_nested_depends_value_unchanged(self): class A(param.Parameterized): c = param.Parameter() d = param.Parameter() class B(param.Parameterized): a = param.Parameter() test_count = param.Integer() @param.depends('a.c', 'a.d', watch=True) def test(self): self.test_count += 1 b = B(a=A(c=1)) b.a = A(c=1) assert b.test_count == 0 def test_param_nested_at_class_definition(self): class A(param.Parameterized): c = param.Parameter() d = param.Parameter() class B(param.Parameterized): a = param.Parameter(A()) test_count = param.Integer() @param.depends('a.c', 'a.d', watch=True) def test(self): self.test_count += 1 b = B() b.a.c = 1 assert b.test_count == 1 b.a.param.update(c=2, d=1) assert b.test_count == 2 b.a = A() assert b.test_count == 3 B.a.c = 5 assert b.test_count == 3 def test_param_nested_depends_expands(self): class A(param.Parameterized): c = param.Parameter() d = param.Parameter() class B(param.Parameterized): a = param.Parameter() test_count = param.Integer() @param.depends('a.param', watch=True) def test(self): self.test_count += 1 b = B(a=A(c=1, name='A')) b.a = A(c=1, name='A') assert b.test_count == 0 def test_param_depends_class_default_dynamic(self): class A(param.Parameterized): c = param.Parameter() class B(param.Parameterized): a = param.Parameter(A()) nested_count = param.Integer() @param.depends('a.c', watch=True) def nested(self): self.nested_count += 1 b = B() b.a.c = 1 assert b.nested_count == 1 b.a = A() assert b.nested_count == 2 def test_param_instance_depends_dynamic_single_nested(self): inst = self.P() pinfos = inst.param.params_depended_on('single_nested', intermediate=True) self.assertEqual(len(pinfos), 0) inst.b = self.P() pinfos = inst.param.params_depended_on('single_nested', intermediate=True) self.assertEqual(len(pinfos), 2) pinfos = {(pi.inst, pi.name): pi for pi in pinfos} pinfo = pinfos[(inst, 'b')] self.assertIs(pinfo.cls, self.P) self.assertIs(pinfo.inst, inst) self.assertEqual(pinfo.name, 'b') self.assertEqual(pinfo.what, 'value') pinfo2 = pinfos[(inst.b, 'a')] self.assertIs(pinfo2.cls, self.P) self.assertIs(pinfo2.inst, inst.b) self.assertEqual(pinfo2.name, 'a') self.assertEqual(pinfo2.what, 'value') assert inst.single_nested_count == 1 inst.b.a = 1 assert inst.single_nested_count == 2 def test_param_instance_depends_dynamic_single_nested_initialized_no_intermediates(self): init_b = self.P() inst = self.P(b=init_b) pinfos = inst.param.params_depended_on('single_nested', intermediate=False) self.assertEqual(len(pinfos), 1) assert pinfos[0].inst is init_b assert pinfos[0].name == 'a' new_b = self.P() inst.b = new_b pinfos = inst.param.params_depended_on('single_nested', intermediate=False) self.assertEqual(len(pinfos), 1) assert pinfos[0].inst is new_b assert pinfos[0].name == 'a' def test_param_instance_depends_dynamic_single_nested_initialized_only_intermediates(self): init_b = self.P() inst = self.P(b=init_b) pinfos = inst.param.params_depended_on('single_nested', intermediate='only') self.assertEqual(len(pinfos), 1) assert pinfos[0].inst is inst assert pinfos[0].name == 'b' def test_param_instance_depends_dynamic_single_nested_initialized(self): init_b = self.P() inst = self.P(b=init_b) pinfos = inst.param.params_depended_on('single_nested', intermediate=True) self.assertEqual(len(pinfos), 2) inst.b = self.P() pinfos = inst.param.params_depended_on('single_nested', intermediate=True) self.assertEqual(len(pinfos), 2) pinfos = {(pi.inst, pi.name): pi for pi in pinfos} pinfo = pinfos[(inst, 'b')] self.assertIs(pinfo.cls, self.P) self.assertIs(pinfo.inst, inst) self.assertEqual(pinfo.name, 'b') self.assertEqual(pinfo.what, 'value') pinfo2 = pinfos[(inst.b, 'a')] self.assertIs(pinfo2.cls, self.P) self.assertIs(pinfo2.inst, inst.b) self.assertEqual(pinfo2.name, 'a') self.assertEqual(pinfo2.what, 'value') assert inst.single_nested_count == 0 inst.b.a = 1 assert inst.single_nested_count == 1 # Ensure watcher on initial value does not trigger event init_b.a = 2 assert inst.single_nested_count == 1 def test_param_instance_depends_dynamic_double_nested(self): inst = self.P() pinfos = inst.param.params_depended_on('double_nested', intermediate=True) self.assertEqual(len(pinfos), 0) inst.b = self.P(b=self.P()) pinfos = inst.param.params_depended_on('double_nested', intermediate=True) self.assertEqual(len(pinfos), 3) pinfos = {(pi.inst, pi.name): pi for pi in pinfos} pinfo = pinfos[(inst, 'b')] self.assertIs(pinfo.cls, self.P) self.assertIs(pinfo.inst, inst) self.assertEqual(pinfo.name, 'b') self.assertEqual(pinfo.what, 'value') pinfo2 = pinfos[(inst.b, 'b')] self.assertIs(pinfo2.cls, self.P) self.assertIs(pinfo2.inst, inst.b) self.assertEqual(pinfo2.name, 'b') self.assertEqual(pinfo2.what, 'value') pinfo3 = pinfos[(inst.b.b, 'a')] self.assertIs(pinfo3.cls, self.P) self.assertIs(pinfo3.inst, inst.b.b) self.assertEqual(pinfo3.name, 'a') self.assertEqual(pinfo3.what, 'value') assert inst.double_nested_count == 1 inst.b.b.a = 1 assert inst.double_nested_count == 2 old_subobj = inst.b.b inst.b.b = self.P(a=3) assert inst.double_nested_count == 3 old_subobj.a = 4 assert inst.double_nested_count == 3 inst.b.b = self.P(a=3) assert inst.double_nested_count == 3 inst.b.b.a = 4 assert inst.double_nested_count == 4 inst.b.b = self.P(a=3) assert inst.double_nested_count == 5 def test_param_instance_depends_dynamic_double_nested_partially_initialized(self): inst = self.P(b=self.P()) pinfos = inst.param.params_depended_on('double_nested', intermediate=True) self.assertEqual(len(pinfos), 2) pinfos = {(pi.inst, pi.name): pi for pi in pinfos} pinfo = pinfos[(inst, 'b')] self.assertIs(pinfo.cls, self.P) self.assertIs(pinfo.inst, inst) self.assertEqual(pinfo.name, 'b') self.assertEqual(pinfo.what, 'value') pinfo = pinfos[(inst.b, 'b')] self.assertIs(pinfo.cls, self.P) self.assertIs(pinfo.inst, inst.b) self.assertEqual(pinfo.name, 'b') self.assertEqual(pinfo.what, 'value') inst.b.b = self.P() assert inst.double_nested_count == 1 inst.b.b.a = 1 assert inst.double_nested_count == 2 def test_param_instance_depends_dynamic_nested_attribute(self): inst = self.P() pinfos = inst.param.params_depended_on('nested_attribute', intermediate=True) self.assertEqual(len(pinfos), 0) inst.b = self.P() pinfos = inst.param.params_depended_on('nested_attribute', intermediate=True) self.assertEqual(len(pinfos), 2) pinfos = {(pi.inst, pi.name): pi for pi in pinfos} pinfo = pinfos[(inst, 'b')] self.assertIs(pinfo.cls, self.P) self.assertIs(pinfo.inst, inst) self.assertEqual(pinfo.name, 'b') self.assertEqual(pinfo.what, 'value') pinfo2 = pinfos[(inst.b, 'a')] self.assertIs(pinfo2.cls, self.P) self.assertIs(pinfo2.inst, inst.b) self.assertEqual(pinfo2.name, 'a') self.assertEqual(pinfo2.what, 'constant') assert inst.nested_attr_count == 1 inst.b.param.a.constant = True assert inst.nested_attr_count == 2 new_b = self.P() new_b.param.a.constant = True inst.b = new_b assert inst.nested_attr_count == 2 def test_param_instance_depends_dynamic_nested_attribute_initialized(self): inst = self.P(b=self.P()) pinfos = inst.param.params_depended_on('nested_attribute', intermediate=True) self.assertEqual(len(pinfos), 2) inst.b = self.P() pinfos = inst.param.params_depended_on('nested_attribute', intermediate=True) self.assertEqual(len(pinfos), 2) pinfos = {(pi.inst, pi.name): pi for pi in pinfos} pinfo = pinfos[(inst, 'b')] self.assertIs(pinfo.cls, self.P) self.assertIs(pinfo.inst, inst) self.assertEqual(pinfo.name, 'b') self.assertEqual(pinfo.what, 'value') pinfo2 = pinfos[(inst.b, 'a')] self.assertIs(pinfo2.cls, self.P) self.assertIs(pinfo2.inst, inst.b) self.assertEqual(pinfo2.name, 'a') self.assertEqual(pinfo2.what, 'constant') assert inst.nested_attr_count == 0 inst.b.param.a.constant = True assert inst.nested_attr_count == 1 def test_param_instance_depends_dynamic_nested(self): inst = self.P() pinfos = inst.param.params_depended_on('nested') self.assertEqual(len(pinfos), 0) inst.b = self.P() pinfos = inst.param.params_depended_on('nested') self.assertEqual(len(pinfos), 10) pinfos = {(pi.inst, pi.name): pi for pi in pinfos} pinfo = pinfos[(inst, 'b')] self.assertIs(pinfo.cls, self.P) self.assertIs(pinfo.inst, inst) self.assertEqual(pinfo.name, 'b') self.assertEqual(pinfo.what, 'value') for p in ['a', 'b', 'name', 'nested_count', 'single_count', 'attr_count']: pinfo2 = pinfos[(inst.b, p)] self.assertIs(pinfo2.cls, self.P) self.assertIs(pinfo2.inst, inst.b) self.assertEqual(pinfo2.name, p) self.assertEqual(pinfo2.what, 'value') assert inst.nested_count == 1 inst.b.a = 1 assert inst.nested_count == 3 def test_param_instance_depends_dynamic_nested_initialized(self): init_b = self.P() inst = self.P(b=init_b) pinfos = inst.param.params_depended_on('nested') self.assertEqual(len(pinfos), 10) inst.b = self.P() pinfos = inst.param.params_depended_on('nested') self.assertEqual(len(pinfos), 10) pinfos = {(pi.inst, pi.name): pi for pi in pinfos} pinfo = pinfos[(inst, 'b')] self.assertIs(pinfo.cls, self.P) self.assertIs(pinfo.inst, inst) self.assertEqual(pinfo.name, 'b') self.assertEqual(pinfo.what, 'value') for p in ['a', 'b', 'name', 'nested_count', 'single_count', 'attr_count']: pinfo2 = pinfos[(inst.b, p)] self.assertIs(pinfo2.cls, self.P) self.assertIs(pinfo2.inst, inst.b) self.assertEqual(pinfo2.name, p) self.assertEqual(pinfo2.what, 'value') assert inst.single_nested_count == 0 inst.b.a = 1 assert inst.single_nested_count == 1 # Ensure watcher on initial value does not trigger event init_b.a = 2 assert inst.single_nested_count == 1 def test_param_instance_depends_dynamic_nested_changed_value(self): init_b = self.P(a=1) inst = self.P(b=init_b) pinfos = inst.param.params_depended_on('nested') self.assertEqual(len(pinfos), 10) inst.b = self.P(a=2) pinfos = inst.param.params_depended_on('nested') self.assertEqual(len(pinfos), 10) pinfos = {(pi.inst, pi.name): pi for pi in pinfos} pinfo = pinfos[(inst, 'b')] self.assertIs(pinfo.cls, self.P) self.assertIs(pinfo.inst, inst) self.assertEqual(pinfo.name, 'b') self.assertEqual(pinfo.what, 'value') for p in ['a', 'b', 'name', 'nested_count', 'single_count', 'attr_count']: pinfo2 = pinfos[(inst.b, p)] self.assertIs(pinfo2.cls, self.P) self.assertIs(pinfo2.inst, inst.b) self.assertEqual(pinfo2.name, p) self.assertEqual(pinfo2.what, 'value') assert inst.single_nested_count == 1 inst.b.a = 1 assert inst.single_nested_count == 2 # Ensure watcher on initial value does not trigger event init_b.a = 2 assert inst.single_nested_count == 2 def test_param_instance_depends(self): p = self.P() pinfos = p.param.params_depended_on('single_parameter') self.assertEqual(len(pinfos), 1) pinfo = pinfos[0] self.assertIs(pinfo.cls, self.P) self.assertIs(pinfo.inst, p) self.assertEqual(pinfo.name, 'a') self.assertEqual(pinfo.what, 'value') p.a = 1 assert p.single_count == 1 def test_param_class_depends(self): pinfos = self.P.param.params_depended_on('single_parameter') self.assertEqual(len(pinfos), 1) pinfo = pinfos[0] self.assertIs(pinfo.cls, self.P) self.assertIs(pinfo.inst, None) self.assertEqual(pinfo.name, 'a') self.assertEqual(pinfo.what, 'value') def test_param_class_depends_constant(self): pinfos = self.P.param.params_depended_on('constant') self.assertEqual(len(pinfos), 1) pinfo = pinfos[0] self.assertIs(pinfo.cls, self.P) self.assertIs(pinfo.inst, None) self.assertEqual(pinfo.name, 'a') self.assertEqual(pinfo.what, 'constant') def test_param_inst_depends_nested(self): inst = self.P(b=self.P()) pinfos = inst.param.params_depended_on('nested') self.assertEqual(len(pinfos), 10) pinfos = {(pi.inst, pi.name): pi for pi in pinfos} pinfo = pinfos[(inst, 'b')] self.assertIs(pinfo.cls, self.P) self.assertIs(pinfo.inst, inst) self.assertEqual(pinfo.name, 'b') self.assertEqual(pinfo.what, 'value') for p in ['name', 'a', 'b']: info = pinfos[(inst.b, p)] self.assertEqual(info.name, p) self.assertIs(info.inst, inst.b) def test_param_external_param_instance(self): inst = self.P2() pinfos = inst.param.params_depended_on('external_param') pinfo = pinfos[0] self.assertIs(pinfo.cls, self.P) self.assertIs(pinfo.inst, None) self.assertEqual(pinfo.name, 'a') self.assertEqual(pinfo.what, 'value') # @pytest.mark.skipif(sys.version_info.major == 2, reason='asyncio only on Python 3') # def test_async(self): # try: # param.parameterized.async_executor = async_executor # class P(param.Parameterized): # a = param.Parameter() # single_count = param.Integer() # @param.depends('a', watch=True) # async def single_parameter(self): # self.single_count += 1 # inst = P() # inst.a = 'test' # assert inst.single_count == 1 # finally: # param.parameterized.async_executor = None class TestParamDependsFunction(API1TestCase): def setUp(self): class P(param.Parameterized): a = param.Parameter() b = param.Parameter() self.P = P def test_param_depends_function_instance_params(self): p = self.P() @param.depends(p.param.a, c=p.param.b) def function(value, c): pass dependencies = { 'dependencies': (p.param.a,), 'kw': {'c': p.param.b}, 'watch': False, 'on_init': False } self.assertEqual(function._dinfo, dependencies) def test_param_depends_function_class_params(self): p = self.P @param.depends(p.param.a, c=p.param.b) def function(value, c): pass dependencies = { 'dependencies': (p.param.a,), 'kw': {'c': p.param.b}, 'watch': False, 'on_init': False } self.assertEqual(function._dinfo, dependencies) def test_param_depends_function_instance_params_watch(self): p = self.P(a=1, b=2) d = [] @param.depends(p.param.a, c=p.param.b, watch=True) def function(value, c): d.append(value+c) p.a = 2 self.assertEqual(d, [4]) p.b = 3 self.assertEqual(d, [4, 5]) def test_param_depends_function_class_params_watch(self): p = self.P p.a = 1 p.b = 2 d = [] @param.depends(p.param.a, c=p.param.b, watch=True) def function(value, c): d.append(value+c) p.a = 2 self.assertEqual(d, [4]) p.b = 3 self.assertEqual(d, [4, 5]) # @pytest.mark.skipif(sys.version_info.major == 2, reason='asyncio only on Python 3') # def test_async(self): # try: # param.parameterized.async_executor = async_executor # p = self.P(a=1) # d = [] # @param.depends(p.param.a, watch=True) # async def function(value): # d.append(value) # p.a = 2 # assert d == [2] # finally: # param.parameterized.async_executor = None def test_misspelled_parameter_in_depends(): class Example(param.Parameterized): xlim = param.Range((0, 10), bounds=(0, 100)) @param.depends("tlim") # <- Misspelled xlim def test(self): return True example = Example() with pytest.raises(AttributeError, match="Attribute 'tlim' could not be resolved on"): # Simulating: pn.panel(example.test) example.param.method_dependencies(example.test.__name__) def test_misspelled_parameter_in_depends_watch(): with pytest.raises(AttributeError, match="Attribute 'tlim' could not be resolved on"): class Example(param.Parameterized): xlim = param.Range((0, 10), bounds=(0, 100)) @param.depends("tlim", watch=True) # <- Misspelled xlim def test(self): return True param-1.12.3/tests/API1/testparamdepends_py3.py000066400000000000000000000057751434441564000212650ustar00rootroot00000000000000""" Unit test for param.depends. """ import sys import param import pytest from . import API1TestCase try: import asyncio except ImportError: asyncio = None def async_executor(func): # Could be entirely replaced by asyncio.run(func()) in Python >=3.7 try: loop = asyncio.get_event_loop() except RuntimeError: loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) loop.run_until_complete(func()) class TestParamDepends(API1TestCase): def setUp(self): class P(param.Parameterized): a = param.Parameter() b = param.Parameter() single_count = param.Integer() attr_count = param.Integer() single_nested_count = param.Integer() double_nested_count = param.Integer() nested_attr_count = param.Integer() nested_count = param.Integer() @param.depends('a', watch=True) def single_parameter(self): self.single_count += 1 @param.depends('a:constant', watch=True) def constant(self): self.attr_count += 1 @param.depends('b.a', watch=True) def single_nested(self): self.single_nested_count += 1 @param.depends('b.b.a', watch=True) def double_nested(self): self.double_nested_count += 1 @param.depends('b.a:constant', watch=True) def nested_attribute(self): self.nested_attr_count += 1 @param.depends('b.param', watch=True) def nested(self): self.nested_count += 1 class P2(param.Parameterized): @param.depends(P.param.a) def external_param(self, a): pass self.P = P self.P2 = P2 def test_async(self): try: param.parameterized.async_executor = async_executor class P(param.Parameterized): a = param.Parameter() single_count = param.Integer() @param.depends('a', watch=True) async def single_parameter(self): self.single_count += 1 inst = P() inst.a = 'test' assert inst.single_count == 1 finally: param.parameterized.async_executor = None class TestParamDependsFunction(API1TestCase): def setUp(self): class P(param.Parameterized): a = param.Parameter() b = param.Parameter() self.P = P @pytest.mark.skipif(sys.version_info.major == 2, reason='asyncio only on Python 3') def test_async(self): try: param.parameterized.async_executor = async_executor p = self.P(a=1) d = [] @param.depends(p.param.a, watch=True) async def function(value): d.append(value) p.a = 2 assert d == [2] finally: param.parameterized.async_executor = None param-1.12.3/tests/API1/testparameterizedobject.py000066400000000000000000000367361434441564000220530ustar00rootroot00000000000000""" Unit test for Parameterized. """ import param import numbergen from . import API1TestCase from .utils import MockLoggingHandler # CEBALERT: not anything like a complete test of Parameterized! import pytest import random from param.parameterized import ParamOverrides, shared_parameters from param.parameterized import default_label_formatter, no_instance_params class _SomeRandomNumbers(object): def __call__(self): return random.random() class TestPO(param.Parameterized): __test__ = False inst = param.Parameter(default=[1,2,3],instantiate=True) notinst = param.Parameter(default=[1,2,3],instantiate=False, per_instance=False) const = param.Parameter(default=1,constant=True) ro = param.Parameter(default="Hello",readonly=True) ro2 = param.Parameter(default=object(),readonly=True,instantiate=True) ro_label = param.Parameter(default=object(), label='Ro Label') ro_format = param.Parameter(default=object()) dyn = param.Dynamic(default=1) class TestPOValidation(param.Parameterized): __test__ = False value = param.Number(default=2, bounds=(0, 4)) @no_instance_params class TestPONoInstance(TestPO): __test__ = False pass class AnotherTestPO(param.Parameterized): instPO = param.Parameter(default=TestPO(),instantiate=True) notinstPO = param.Parameter(default=TestPO(),instantiate=False) class TestAbstractPO(param.Parameterized): __test__ = False __abstract = True class _AnotherAbstractPO(param.Parameterized): __abstract = True class TestParamInstantiation(AnotherTestPO): __test__ = False instPO = param.Parameter(default=AnotherTestPO(),instantiate=False) class TestParameterized(API1TestCase): @classmethod def setUpClass(cls): super(TestParameterized, cls).setUpClass() log = param.parameterized.get_logger() cls.log_handler = MockLoggingHandler(level='DEBUG') log.addHandler(cls.log_handler) def test_parameter_name_fixed(self): testpo = TestPO() with pytest.raises(AttributeError): testpo.param.const.name = 'notconst' def test_constant_parameter(self): """Test that you can't set a constant parameter after construction.""" testpo = TestPO(const=17) self.assertEqual(testpo.const,17) self.assertRaises(TypeError,setattr,testpo,'const',10) # check you can set on class TestPO.const=9 testpo = TestPO() self.assertEqual(testpo.const,9) def test_readonly_parameter(self): """Test that you can't set a read-only parameter on construction or as an attribute.""" testpo = TestPO() self.assertEqual(testpo.ro,"Hello") with self.assertRaises(TypeError): t = TestPO(ro=20) t=TestPO() self.assertRaises(TypeError,setattr,t,'ro',10) # check you cannot set on class self.assertRaises(TypeError,setattr,TestPO,'ro',5) self.assertEqual(testpo.param.params()['ro'].constant,True) # check that instantiate was ignored for readonly self.assertEqual(testpo.param.params()['ro2'].instantiate,False) def test_basic_instantiation(self): """Check that instantiated parameters are copied into objects.""" testpo = TestPO() self.assertEqual(testpo.inst,TestPO.inst) self.assertEqual(testpo.notinst,TestPO.notinst) TestPO.inst[1]=7 TestPO.notinst[1]=7 self.assertEqual(testpo.notinst,[1,7,3]) self.assertEqual(testpo.inst,[1,2,3]) def test_more_instantiation(self): """Show that objects in instantiated Parameters can still share data.""" anothertestpo = AnotherTestPO() ### CB: AnotherTestPO.instPO is instantiated, but ### TestPO.notinst is not instantiated - so notinst is still ### shared, even by instantiated parameters of AnotherTestPO. ### Seems like this behavior of Parameterized could be ### confusing, so maybe mention it in documentation somewhere. TestPO.notinst[1]=7 # (if you thought your instPO was completely an independent object, you # might be expecting [1,2,3] here) self.assertEqual(anothertestpo.instPO.notinst,[1,7,3]) def test_instantiation_inheritance(self): """Check that instantiate=True is always inherited (SF.net #2483932).""" t = TestParamInstantiation() assert t.param.params('instPO').instantiate is True assert isinstance(t.instPO,AnotherTestPO) def test_abstract_class(self): """Check that a class declared abstract actually shows up as abstract.""" self.assertEqual(TestAbstractPO.abstract,True) self.assertEqual(_AnotherAbstractPO.abstract,True) self.assertEqual(TestPO.abstract,False) def test_override_class_param_validation(self): test = TestPOValidation() test.param.value.bounds = (0, 3) with self.assertRaises(ValueError): test.value = 4 TestPOValidation.value = 4 def test_remove_class_param_validation(self): test = TestPOValidation() test.param.value.bounds = None test.value = 20 with self.assertRaises(ValueError): TestPOValidation.value = 10 def test_params(self): """Basic tests of params() method.""" # CB: test not so good because it requires changes if params # of PO are changed assert 'name' in param.Parameterized.param.params() assert len(param.Parameterized.param.params()) in [1,2] ## check for bug where subclass Parameters were not showing up ## if params() already called on a super class. assert 'inst' in TestPO.param.params() assert 'notinst' in TestPO.param.params() ## check caching assert param.Parameterized.param.params() is param.Parameterized().param.params(), "Results of params() should be cached." # just for performance reasons def test_param_iterator(self): self.assertEqual(set(TestPO.param), {'name', 'inst', 'notinst', 'const', 'dyn', 'ro', 'ro2', 'ro_label', 'ro_format'}) def test_param_contains(self): for p in ['name', 'inst', 'notinst', 'const', 'dyn', 'ro', 'ro2']: self.assertIn(p, TestPO.param) def test_class_param_objects(self): objects = TestPO.param.objects() self.assertEqual(set(objects), {'name', 'inst', 'notinst', 'const', 'dyn', 'ro', 'ro2', 'ro_label', 'ro_format'}) # Check caching assert TestPO.param.objects() is objects def test_instance_param_objects(self): inst = TestPO() objects = inst.param.objects() for p, obj in objects.items(): if p == 'notinst': assert obj is TestPO.param[p] else: assert obj is not TestPO.param[p] def test_instance_param_objects_set_to_false(self): inst = TestPO() objects = inst.param.objects(instance=False) for p, obj in objects.items(): assert obj is TestPO.param[p] def test_instance_param_objects_set_to_current(self): inst = TestPO() inst_param = inst.param.inst objects = inst.param.objects(instance='existing') for p, obj in objects.items(): if p == 'inst': assert obj is inst_param else: assert obj is TestPO.param[p] def test_instance_param_objects_warn_on_params(self): inst = TestPO() inst.param['inst'] inst.param.params() if param.parameterized.Parameters._disable_stubs is None: self.log_handler.assertContains( 'WARNING', 'The Parameterized instance has instance parameters') def test_instance_param_getitem(self): test = TestPO() assert test.param['inst'] is not TestPO.param['inst'] def test_instance_param_getitem_not_per_instance(self): test = TestPO() assert test.param['notinst'] is TestPO.param['notinst'] def test_instance_param_getitem_no_instance_params(self): test = TestPONoInstance() assert test.param['inst'] is TestPO.param['inst'] def test_instance_param_getattr(self): test = TestPO() assert test.param.inst is not TestPO.param.inst # Assert no deep copy assert test.param.inst.default is TestPO.param.inst.default def test_pprint_instance_params(self): # Ensure pprint does not make instance parameter copies test = TestPO() test.pprint() for p, obj in TestPO.param.objects('current').items(): assert obj is TestPO.param[p] def test_update_instance_params(self): # Ensure update does not make instance parameter copies test = TestPO() test.param.update(inst=3) for p, obj in TestPO.param.objects('current').items(): assert obj is TestPO.param[p] def test_values_instance_params(self): # Ensure values does not make instance parameter copies test = TestPO() test.param.values() for p, obj in TestPO.param.objects('current').items(): assert obj is TestPO.param[p] def test_defaults_instance_params(self): # Ensure defaults does not make instance parameter copies test = TestPO() test.param.defaults() for p, obj in TestPO.param.objects('current').items(): assert obj is TestPO.param[p] def test_state_saving(self): t = TestPO(dyn=_SomeRandomNumbers()) g = t.param.get_value_generator('dyn') g._Dynamic_time_fn=None assert t.dyn!=t.dyn orig = t.dyn t.state_push() t.dyn assert t.param.inspect_value('dyn')!=orig t.state_pop() assert t.param.inspect_value('dyn')==orig def test_label(self): t = TestPO() assert t.param.params('ro_label').label == 'Ro Label' def test_label_set(self): t = TestPO() assert t.param.params('ro_label').label == 'Ro Label' t.param.params('ro_label').label = 'Ro relabeled' assert t.param.params('ro_label').label == 'Ro relabeled' def test_label_default_format(self): t = TestPO() assert t.param.params('ro_format').label == 'Ro format' def test_label_custom_format(self): param.parameterized.label_formatter = default_label_formatter.instance(capitalize=False) t = TestPO() assert t.param.params('ro_format').label == 'ro format' param.parameterized.label_formatter = default_label_formatter def test_label_constant_format(self): param.parameterized.label_formatter = lambda x: 'Foo' t = TestPO() assert t.param.params('ro_format').label == 'Foo' param.parameterized.label_formatter = default_label_formatter from param import parameterized class some_fn(param.ParameterizedFunction): __test__ = False num_phase = param.Number(18) frequencies = param.List([99]) scale = param.Number(0.3) def __call__(self,**params_to_override): params = parameterized.ParamOverrides(self,params_to_override) num_phase = params['num_phase'] frequencies = params['frequencies'] scale = params['scale'] return scale,num_phase,frequencies instance = some_fn.instance() class TestParameterizedFunction(API1TestCase): def _basic_tests(self,fn): self.assertEqual(fn(),(0.3,18,[99])) self.assertEqual(fn(frequencies=[1,2,3]),(0.3,18,[1,2,3])) self.assertEqual(fn(),(0.3,18,[99])) fn.frequencies=[10,20,30] self.assertEqual(fn(frequencies=[1,2,3]),(0.3,18,[1,2,3])) self.assertEqual(fn(),(0.3,18,[10,20,30])) def test_parameterized_function(self): self._basic_tests(some_fn) def test_parameterized_function_instance(self): self._basic_tests(instance) def test_pickle_instance(self): import pickle s = pickle.dumps(instance) instance.scale=0.8 i = pickle.loads(s) self.assertEqual(i(),(0.3,18,[10,20,30])) class TestPO1(param.Parameterized): __test__ = False x = param.Number(default=numbergen.UniformRandom(lbound=-1,ubound=1,seed=1),bounds=(-1,1)) y = param.Number(default=1,bounds=(-1,1)) class TestNumberParameter(API1TestCase): def test_outside_bounds(self): t1 = TestPO1() # Test bounds (non-dynamic number) try: t1.y = 10 except ValueError: pass else: assert False, "Should raise ValueError." def test_outside_bounds_numbergen(self): t1 = TestPO1() # Test bounds (dynamic number) t1.x = numbergen.UniformRandom(lbound=2,ubound=3) # bounds not checked on set try: t1.x except ValueError: pass else: assert False, "Should raise ValueError." class TestStringParameter(API1TestCase): def setUp(self): super(TestStringParameter, self).setUp() class TestString(param.Parameterized): a = param.String() b = param.String(default='',allow_None=True) c = param.String(default=None) self._TestString = TestString def test_handling_of_None(self): t = self._TestString() with self.assertRaises(ValueError): t.a = None t.b = None assert t.c is None class TestParameterizedUtilities(API1TestCase): def setUp(self): super(TestParameterizedUtilities, self).setUp() def test_default_label_formatter(self): assert default_label_formatter('a_b_C') == 'A b C' def test_default_label_formatter_not_capitalized(self): assert default_label_formatter.instance(capitalize=False)('a_b_C') == 'a b C' def test_default_label_formatter_not_replace_underscores(self): assert default_label_formatter.instance(replace_underscores=False)('a_b_C') == 'A_b_C' def test_default_label_formatter_overrides(self): assert default_label_formatter.instance(overrides={'a': 'b'})('a') == 'b' class TestParamOverrides(API1TestCase): def setUp(self): super(TestParamOverrides, self).setUp() self.po = param.Parameterized(name='A',print_level=0) def test_init_name(self): self.assertEqual(self.po.name, 'A') def test_simple_override(self): overrides = ParamOverrides(self.po,{'name':'B'}) self.assertEqual(overrides['name'], 'B') self.assertEqual(overrides['print_level'], 0) # CEBALERT: missing test for allow_extra_keywords (e.g. getting a # warning on attempting to override non-existent parameter when # allow_extra_keywords is False) def test_missing_key(self): overrides = ParamOverrides(self.po,{'name':'B'}) with self.assertRaises(AttributeError): overrides['doesnotexist'] class TestSharedParameters(API1TestCase): def setUp(self): super(TestSharedParameters, self).setUp() with shared_parameters(): self.p1 = TestPO(name='A', print_level=0) self.p2 = TestPO(name='B', print_level=0) self.ap1 = AnotherTestPO(name='A', print_level=0) self.ap2 = AnotherTestPO(name='B', print_level=0) def test_shared_object(self): self.assertTrue(self.ap1.instPO is self.ap2.instPO) self.assertTrue(self.ap1.param.params('instPO').default is not self.ap2.instPO) def test_shared_list(self): self.assertTrue(self.p1.inst is self.p2.inst) self.assertTrue(self.p1.param.params('inst').default is not self.p2.inst) param-1.12.3/tests/API1/testparameterizedrepr.py000066400000000000000000000125421434441564000215420ustar00rootroot00000000000000""" Unit test for the repr and pprint of parameterized objects. """ import param from . import API1TestCase class TestParameterizedRepr(API1TestCase): def setUp(self): super(TestParameterizedRepr, self).setUp() # initialize a parameterized class class A(param.Parameterized): a = param.Number(4, precedence=-5) b = param.String('B', precedence=-4) c = param.Number(4, precedence=0) d = param.Integer(-22, precedence=1) x = param.Number(1, precedence=2) y = param.Number(2, precedence=-1) z = param.Number(3, precedence=-2) def __init__(self, a, b, c=4, d=-22, **kwargs): super(A, self).__init__(a=a, b=b, c=c, **kwargs) self.A = A class B(param.Parameterized): # Similar to A but no **kwargs a = param.Number(4, precedence=-5) b = param.String('B', precedence=-4) c = param.Number(4, precedence=0) d = param.Integer(-22, precedence=1) x = param.Number(1, precedence=2) def __init__(self, a, b, c=4, d=-22): super(B, self).__init__(a=a, b=b, c=c, name='ClassB') self.B = B class C(param.Parameterized): # Similar to A but with *varargs a = param.Number(4, precedence=-5) b = param.String('B', precedence=-4) c = param.Number(4, precedence=0) d = param.Integer(-22, precedence=1) x = param.Number(1, precedence=2) y = param.Number(2, precedence=-1) z = param.Number(3, precedence=-2) def __init__(self, a, b, c=4, d=-22, *varargs, **kwargs): super(C, self).__init__(a=a, b=b, c=c, **kwargs) self.C = C class D(param.Parameterized): # Similar to A but with missing parameters a = param.Number(4, precedence=-5) b = param.String('B', precedence=-4) def __init__(self, a, b, c=4, d=-22, **kwargs): super(D, self).__init__(a=a, b=b, **kwargs) self.D = D # More realistically, positional args are not params class E(param.Parameterized): a = param.Number(4, precedence=-5) def __init__(self, p, q=4, **params): # (plus non-param kw too) super(E, self).__init__(**params) self.E = E def testparameterizedrepr(self): obj = self.A(4,'B', name='test1') self.assertEqual(repr(obj), "A(a=4, b='B', c=4, d=-22, name='test1', x=1, y=2, z=3)") def testparameterizedscriptrepr1(self): obj = self.A(4,'B', name='test') self.assertEqual(obj.pprint(), "A(4, 'B', name='test')") def testparameterizedscriptrepr2(self): obj = self.A(4,'B', c=5, name='test') self.assertEqual(obj.pprint(), "A(4, 'B', c=5, name='test')") def testparameterizedscriptrepr3(self): obj = self.A(4,'B', c=5, x=True, name='test') self.assertEqual(obj.pprint(), "A(4, 'B', c=5, name='test')") def testparameterizedscriptrepr4(self): obj = self.A(4,'B', c=5, x=10, name='test') self.assertEqual(obj.pprint(), "A(4, 'B', c=5, name='test', x=10)") def testparameterizedscriptrepr5(self): obj = self.A(4,'B', x=10, y=11, z=12, name='test') self.assertEqual(obj.pprint(), "A(4, 'B', name='test', z=12, y=11, x=10)") def testparameterizedscriptrepr_nokwargs(self): obj = self.B(4,'B', c=99) obj.x = 10 # Modified but not passable through constructor self.assertEqual(obj.pprint(), "B(4, 'B', c=99)") def testparameterizedscriptrepr_varags(self): obj = self.C(4,'C', c=99) self.assertEqual(obj.pprint(), "C(4, 'C', c=99, **varargs)") def testparameterizedscriptrepr_varags_kwargs(self): obj = self.C(4,'C', c=99, x=10, y=11, z=12) self.assertEqual(obj.pprint(), "C(4, 'C', c=99, z=12, y=11, x=10, **varargs)") def testparameterizedscriptrepr_missing_values(self): obj = self.D(4,'D', c=99) self.assertEqual(obj.pprint(), "D(4, 'D', c=, d=)") def testparameterizedscriptrepr_nonparams(self): obj = self.E(10,q='hi', a=99) self.assertEqual(obj.pprint(), "E(, q=, a=99)") def test_exceptions(self): obj = self.E(10,q='hi',a=99) try: obj.pprint(unknown_value=False) except Exception: pass else: raise AssertionError def test_suppression(self): obj = self.E(10,q='hi',a=99) self.assertEqual(obj.pprint(unknown_value=None), "E(a=99)") def test_imports_deduplication(self): obj = self.E(10,q='hi', a=99) imports = ['import me','import me'] obj.pprint(imports=imports) self.assertEqual(imports.count('import me'),1) def test_qualify(self): obj = self.E(10,q='hi', a=99) r = "E(, q=, a=99)" self.assertEqual(obj.pprint(qualify=False), r) self.assertEqual(obj.pprint(qualify=True), "tests.API1.testparameterizedrepr."+r) param-1.12.3/tests/API1/testparamoutput.py000066400000000000000000000157231434441564000204020ustar00rootroot00000000000000""" Unit test for param.output. """ import sys from unittest import SkipTest import param from . import API1TestCase class TestParamDepends(API1TestCase): def test_simple_output(self): class P(param.Parameterized): @param.output() def single_output(self): return 1 p = P() outputs = p.param.outputs() self.assertEqual(list(outputs), ['single_output']) otype, method, idx = outputs['single_output'] self.assertIs(type(otype), param.Parameter) self.assertEqual(method, p.single_output) self.assertEqual(idx, None) def test_subclass_output(self): class A(param.Parameterized): @param.output() def single_output(self): return 1 class B(param.Parameterized): @param.output() def another_output(self): return 2 class C(A, B): pass p = C() outputs = p.param.outputs() self.assertEqual(sorted(outputs), ['another_output', 'single_output']) otype, method, idx = outputs['single_output'] self.assertIs(type(otype), param.Parameter) self.assertEqual(method, p.single_output) self.assertEqual(idx, None) otype, method, idx = outputs['another_output'] self.assertIs(type(otype), param.Parameter) self.assertEqual(method, p.another_output) self.assertEqual(idx, None) def test_named_kwarg_output(self): class P(param.Parameterized): @param.output(value=param.Integer) def single_output(self): return 1 p = P() outputs = p.param.outputs() self.assertEqual(list(outputs), ['value']) otype, method, idx = outputs['value'] self.assertIs(type(otype), param.Integer) self.assertEqual(method, p.single_output) self.assertEqual(idx, None) def test_named_and_typed_arg_output(self): class P(param.Parameterized): @param.output(('value', param.Integer)) def single_output(self): return 1 p = P() outputs = p.param.outputs() self.assertEqual(list(outputs), ['value']) otype, method, idx = outputs['value'] self.assertIs(type(otype), param.Integer) self.assertEqual(method, p.single_output) self.assertEqual(idx, None) def test_named_arg_output(self): class P(param.Parameterized): @param.output('value') def single_output(self): return 1 p = P() outputs = p.param.outputs() self.assertEqual(list(outputs), ['value']) otype, method, idx = outputs['value'] self.assertIs(type(otype), param.Parameter) self.assertEqual(method, p.single_output) self.assertEqual(idx, None) def test_typed_arg_output(self): class P(param.Parameterized): @param.output(int) def single_output(self): return 1 p = P() outputs = p.param.outputs() self.assertEqual(list(outputs), ['single_output']) otype, method, idx = outputs['single_output'] self.assertIs(type(otype), param.ClassSelector) self.assertIs(otype.class_, int) self.assertEqual(method, p.single_output) self.assertEqual(idx, None) def test_multiple_named_kwarg_output(self): py_major = sys.version_info.major py_minor = sys.version_info.minor if (py_major < 3 or (py_major == 3 and py_minor < 6)): raise SkipTest('Multiple keyword output declarations only ' 'supported in Python >= 3.6, skipping test.') class P(param.Parameterized): @param.output(value=param.Integer, value2=param.String) def multi_output(self): return (1, 'string') p = P() outputs = p.param.outputs() self.assertEqual(set(outputs), {'value', 'value2'}) otype, method, idx = outputs['value'] self.assertIs(type(otype), param.Integer) self.assertEqual(method, p.multi_output) self.assertEqual(idx, 0) otype, method, idx = outputs['value2'] self.assertIs(type(otype), param.String) self.assertEqual(method, p.multi_output) self.assertEqual(idx, 1) def test_multi_named_and_typed_arg_output(self): class P(param.Parameterized): @param.output(('value', param.Integer), ('value2', param.String)) def multi_output(self): return (1, 'string') p = P() outputs = p.param.outputs() self.assertEqual(set(outputs), {'value', 'value2'}) otype, method, idx = outputs['value'] self.assertIs(type(otype), param.Integer) self.assertEqual(method, p.multi_output) self.assertEqual(idx, 0) otype, method, idx = outputs['value2'] self.assertIs(type(otype), param.String) self.assertEqual(method, p.multi_output) self.assertEqual(idx, 1) def test_multi_named_arg_output(self): class P(param.Parameterized): @param.output('value', 'value2') def multi_output(self): return (1, 2) p = P() outputs = p.param.outputs() self.assertEqual(set(outputs), {'value', 'value2'}) otype, method, idx = outputs['value'] self.assertIs(type(otype), param.Parameter) self.assertEqual(method, p.multi_output) self.assertEqual(idx, 0) otype, method, idx = outputs['value2'] self.assertIs(type(otype), param.Parameter) self.assertEqual(method, p.multi_output) self.assertEqual(idx, 1) def test_multi_typed_arg_output(self): with self.assertRaises(ValueError): class P(param.Parameterized): @param.output(int, str) def single_output(self): return 1 def test_multi_method_named_and_typed_arg_output(self): class P(param.Parameterized): @param.output(('value', param.Integer), ('value2', str)) def multi_output(self): return (1, 'string') @param.output(('value3', param.Number)) def single_output(self): return 3.0 p = P() outputs = p.param.outputs() self.assertEqual(set(outputs), {'value', 'value2', 'value3'}) otype, method, idx = outputs['value'] self.assertIs(type(otype), param.Integer) self.assertEqual(method, p.multi_output) self.assertEqual(idx, 0) otype, method, idx = outputs['value2'] self.assertIs(type(otype), param.ClassSelector) self.assertIs(otype.class_, str) self.assertEqual(method, p.multi_output) self.assertEqual(idx, 1) otype, method, idx = outputs['value3'] self.assertIs(type(otype), param.Number) self.assertEqual(method, p.single_output) self.assertEqual(idx, None) param-1.12.3/tests/API1/testparamunion.py000066400000000000000000000035001434441564000201600ustar00rootroot00000000000000""" UnitTest for param_union helper """ import logging import param from . import API1TestCase class MyHandler(logging.StreamHandler): def __init__(self): super(MyHandler, self).__init__() self.records = [] def emit(self, record): self.records.append(record) class TestParamUnion(API1TestCase): def setUp(self): self.logger = param.get_logger() self.handler = MyHandler() self.logger.addHandler(self.handler) def tearDown(self): self.logger.removeHandler(self.handler) def test_param_union_values(self): class A(param.Parameterized): a = param.Number(1) class B(param.Parameterized): b = param.Number(2) class C(A, B): pass a = A() a.a = 10 b = B() b.b = 5 c_1 = C(**param.param_union(a)) self.assertTrue(c_1.a == 10 and c_1.b == 2) c_2 = C(**param.param_union(b)) self.assertTrue(c_2.a == 1 and c_2.b == 5) c_3 = C(**param.param_union(a, b)) self.assertTrue(c_3.a == 10 and c_3.b == 5) c_4 = C(**param.param_union()) self.assertTrue(c_4.a == 1 and c_4.b == 2) def test_param_union_warnings(self): class A(param.Parameterized): a = param.Number(1) a = A() A(**param.param_union(a)) self.assertFalse(self.handler.records) A(**param.param_union()) self.assertFalse(self.handler.records) A(**param.param_union(a, a)) self.assertTrue(self.handler.records) self.handler.records.pop() A(**param.param_union(a, a, warn=False)) self.assertFalse(self.handler.records) def test_param_union_raises_on_unexpected_kwarg(self): with self.assertRaises(TypeError): param.param_union(dumbdumbface=True) param-1.12.3/tests/API1/testrangeparameter.py000066400000000000000000000030311434441564000210030ustar00rootroot00000000000000""" Unit test for Range parameters. """ import param from . import API1TestCase class TestRangeParameters(API1TestCase): def test_initialization_out_of_bounds(self): try: class Q(param.Parameterized): q = param.Range((0, 2), bounds=(0, 1)) except ValueError: pass else: raise AssertionError("No exception raised on out-of-bounds date") def test_set_exclusive_out_of_bounds_upper(self): class Q(param.Parameterized): q = param.Range(bounds=(0, 10), inclusive_bounds=(True, False)) try: Q.q = (0, 10) except ValueError: pass else: raise AssertionError("No exception raised on out-of-bounds date") def test_set_exclusive_out_of_bounds_lower(self): class Q(param.Parameterized): q = param.Range(bounds=(0, 10), inclusive_bounds=(False, True)) try: Q.q = (0, 10) except ValueError: pass else: raise AssertionError("No exception raised on out-of-bounds date") def test_set_out_of_bounds(self): class Q(param.Parameterized): q = param.Range(bounds=(0, 10)) try: Q.q = (5, 11) except ValueError: pass else: raise AssertionError("No exception raised on out-of-bounds date") def test_get_soft_bounds(self): q = param.Range((1,3), bounds=(0, 10), softbounds=(1, 9)) self.assertEqual(q.get_soft_bounds(), (1, 9)) param-1.12.3/tests/API1/testselector.py000066400000000000000000000060061434441564000176330ustar00rootroot00000000000000""" Unit test for object selector parameters. Originally implemented as doctests in Topographica in the file testEnumerationParameter.txt """ import param from . import API1TestCase from collections import OrderedDict opts=dict(A=[1,2],B=[3,4],C=dict(a=1,b=2)) class TestSelectorParameters(API1TestCase): def setUp(self): super(TestSelectorParameters, self).setUp() class P(param.Parameterized): e = param.Selector([5,6,7]) f = param.Selector(default=10) h = param.Selector(default=None) g = param.Selector([7,8]) i = param.Selector([9],default=7, check_on_set=False) s = param.Selector(OrderedDict(one=1,two=2,three=3), default=3) d = param.Selector(opts, default=opts['B']) self.P = P def test_set_object_constructor(self): p = self.P(e=6) self.assertEqual(p.e, 6) def test_get_range_list(self): r = self.P.param.params("g").get_range() self.assertEqual(r['7'],7) self.assertEqual(r['8'],8) def test_get_range_dict(self): r = self.P.param.params("s").get_range() self.assertEqual(r['one'],1) self.assertEqual(r['two'],2) def test_get_range_mutable(self): r = self.P.param.params("d").get_range() self.assertEqual(r['A'],opts['A']) self.assertEqual(r['C'],opts['C']) self.d=opts['A'] self.d=opts['C'] self.d=opts['B'] def test_set_object_outside_bounds(self): p = self.P(e=6) try: p.e = 9 except ValueError: pass else: raise AssertionError("Object set outside range.") def test_set_object_setattr(self): p = self.P(e=6) p.f = 9 self.assertEqual(p.f, 9) p.g = 7 self.assertEqual(p.g, 7) p.i = 12 self.assertEqual(p.i, 12) def test_set_object_not_None(self): p = self.P(e=6) p.g = 7 try: p.g = None except ValueError: pass else: raise AssertionError("Object set outside range.") def test_set_object_setattr_post_error(self): p = self.P(e=6) p.f = 9 self.assertEqual(p.f, 9) p.g = 7 try: p.g = None except ValueError: pass else: raise AssertionError("Object set outside range.") self.assertEqual(p.g, 7) p.i = 12 self.assertEqual(p.i, 12) def test_initialization_out_of_bounds(self): try: class Q(param.Parameterized): q = param.Selector([4], 5) except ValueError: pass else: raise AssertionError("Selector created outside range.") def test_initialization_no_bounds(self): try: class Q(param.Parameterized): q = param.Selector(10, default=5) except TypeError: pass else: raise AssertionError("Selector created without range.") param-1.12.3/tests/API1/teststringparam.py000066400000000000000000000035451434441564000203470ustar00rootroot00000000000000""" Unit test for String parameters """ import sys from . import API1TestCase import param ip_regex = r'^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$' class TestStringParameters(API1TestCase): def test_regex_ok(self): class A(param.Parameterized): s = param.String('0.0.0.0', ip_regex) a = A() a.s = '123.123.0.1' def test_reject_none(self): class A(param.Parameterized): s = param.String('0.0.0.0', ip_regex) a = A() cls = 'class' if sys.version_info.major > 2 else 'type' exception = "String parameter 's' only takes a string value, not value of type <%s 'NoneType'>." % cls with self.assertRaisesRegex(ValueError, exception): a.s = None # because allow_None should be False def test_default_none(self): class A(param.Parameterized): s = param.String(None, ip_regex) a = A() a.s = '123.123.0.1' a.s = None # because allow_None should be True with default of None def test_regex_incorrect(self): class A(param.Parameterized): s = param.String('0.0.0.0', regex=ip_regex) a = A() exception = "String parameter 's' value '123.123.0.256' does not match regex '%s'." % ip_regex with self.assertRaises(ValueError) as e: a.s = '123.123.0.256' self.assertEqual(str(e.exception), exception.replace('\\', '\\\\')) def test_regex_incorrect_default(self): exception = "String parameter None value '' does not match regex '%s'." % ip_regex with self.assertRaises(ValueError) as e: class A(param.Parameterized): s = param.String(regex=ip_regex) # default value '' does not match regular expression self.assertEqual(str(e.exception), exception.replace('\\', '\\\\')) param-1.12.3/tests/API1/testtimedependent.py000066400000000000000000000243671434441564000206520ustar00rootroot00000000000000""" Unit tests for the param.Time class, time dependent parameters and time-dependent numbergenerators. """ import param import numbergen import copy from . import API1TestCase import pytest import fractions try: import gmpy except ImportError: import os if os.getenv('PARAM_TEST_GMPY','0') == '1': raise ImportError("PARAM_TEST_GMPY=1 but gmpy not available.") else: gmpy = None class TestTimeClass(API1TestCase): def test_time_init(self): param.Time() def test_time_init_int(self): t = param.Time(time_type=int) self.assertEqual(t(), 0) def test_time_int_iter(self): t = param.Time(time_type=int) self.assertEqual(next(t), 0) self.assertEqual(next(t), 1) def test_time_init_timestep(self): t = param.Time(time_type=int, timestep=2) self.assertEqual(next(t), 0) self.assertEqual(next(t), 2) def test_time_int_until(self): t = param.Time(time_type=int, until=3) self.assertEqual(next(t), 0) self.assertEqual(next(t), 1) self.assertEqual(next(t), 2) self.assertEqual(next(t), 3) try: self.assertEqual(next(t), 4) raise AssertionError("StopIteration should have been raised") except StopIteration: pass def test_time_int_eq(self): t = param.Time(time_type=int) s = param.Time(time_type=int) t(3); s(3) self.assertEqual(t == s, True) def test_time_int_context(self): t = param.Time(time_type=int) t(3) with t: self.assertEqual(t(), 3) t(5) self.assertEqual(t(), 5) self.assertEqual(t(), 3) def test_time_int_context_iadd(self): with param.Time(time_type=int) as t: self.assertEqual(t(), 0) t += 5 self.assertEqual(t(), 5) self.assertEqual(t(), 0) def test_time_int_change_type(self): t = param.Time(time_type=int) self.assertEqual(t(), 0) t(1, fractions.Fraction) self.assertEqual(t(), 1) self.assertEqual(t.time_type, fractions.Fraction) def test_time_integration(self): # This used to be a doctest of param.Time; moved # here not to have any doctest to run. time = param.Time(until=20, timestep=1) self.assertEqual(time(), 0) self.assertEqual(time(5), 5) time += 5 self.assertEqual(time(), 10) with time as t: self.assertEqual(t(), 10) self.assertEqual( [val for val in t], [10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20] ) self.assertEqual(t(), 20) 'Time after iteration: %s' % t() t += 2 self.assertEqual(t(), 22) self.assertEqual(time(), 10) @pytest.mark.skipif(gmpy is None, reason="gmpy is not installed") def test_time_init_gmpy(self): t = param.Time(time_type=gmpy.mpq) self.assertEqual(t(), gmpy.mpq(0)) t.advance(gmpy.mpq(0.25)) self.assertEqual(t(), gmpy.mpq(1,4)) @pytest.mark.skipif(gmpy is None, reason="gmpy is not installed") def test_time_init_gmpy_advanced(self): t = param.Time(time_type=gmpy.mpq, timestep=gmpy.mpq(0.25), until=1.5) self.assertEqual(t(), gmpy.mpq(0,1)) t(0.5) self.assertEqual(t(), gmpy.mpq(1,2)) with t: t.advance(0.25) self.assertEqual(t(), gmpy.mpq(3,4)) self.assertEqual(t(), gmpy.mpq(1,2)) tvals = [tval for tval in t] self.assertEqual(tvals, [gmpy.mpq(1,2), gmpy.mpq(3,4), gmpy.mpq(1,1), gmpy.mpq(5,4), gmpy.mpq(3,2)]) class TestTimeDependentDynamic(API1TestCase): def setUp(self): super(TestTimeDependentDynamic, self).setUp() param.Dynamic.time_dependent=None self.time_fn= param.Time(time_type=int) class Incrementer(object): def __init__(self): self.i = -1 def __call__(self): self.i+=1 return self.i self.Incrementer = Incrementer class DynamicClass(param.Parameterized): a = param.Number(default = self.Incrementer()) self.DynamicClass = DynamicClass self._start_state = copy.copy([param.Dynamic.time_dependent, numbergen.TimeAware.time_dependent, param.Dynamic.time_fn, numbergen.TimeAware.time_fn, param.random_seed]) def tearDown(self): param.Dynamic.time_dependent = self._start_state[0] numbergen.TimeAware.time_dependent = self._start_state[1] param.Dynamic.time_fn = self._start_state[2] numbergen.TimeAware.time_fn = self._start_state[3] param.random_seed = self._start_state[4] def test_non_time_dependent(self): """ With param.Dynamic.time_dependent=None every call should increment. """ param.Dynamic.time_dependent=None param.Dynamic.time_fn = self.time_fn dynamic = self.DynamicClass() self.assertEqual(dynamic.a, 0) self.assertEqual(dynamic.a, 1) self.assertEqual(dynamic.a, 2) def test_time_fixed(self): """ With param.Dynamic.time_dependent=True the value should only increment when the time value changes. """ param.Dynamic.time_dependent=True param.Dynamic.time_fn = self.time_fn dynamic = self.DynamicClass() self.assertEqual(dynamic.a, 0) self.assertEqual(dynamic.a, 0) self.time_fn += 1 self.assertEqual(dynamic.a, 1) self.assertEqual(dynamic.a, 1) param.Dynamic.time_fn -= 5 self.assertEqual(dynamic.a, 2) self.assertEqual(dynamic.a, 2) def test_time_dependent(self): """ With param.Dynamic.time_dependent=True and param.Dynamic and numbergen.TimeDependent sharing a common time_fn, the value should be a function of time. """ param.Dynamic.time_dependent=True param.Dynamic.time_fn = self.time_fn numbergen.TimeDependent.time_fn = self.time_fn class DynamicClass(param.Parameterized): b = param.Number(default = numbergen.ScaledTime(factor=2)) dynamic = DynamicClass() self.time_fn(0) self.assertEqual(dynamic.b, 0.0) self.time_fn += 5 self.assertEqual(dynamic.b, 10.0) self.assertEqual(dynamic.b, 10.0) self.time_fn -= 2 self.assertEqual(dynamic.b, 6.0) self.assertEqual(dynamic.b, 6.0) self.time_fn -= 3 self.assertEqual(dynamic.b, 0.0) def test_time_dependent_random(self): """ When set to time_dependent=True, random number generators should also be a function of time. """ param.Dynamic.time_dependent=True numbergen.TimeAware.time_dependent=True param.Dynamic.time_fn = self.time_fn numbergen.TimeAware.time_fn = self.time_fn param.random_seed = 42 class DynamicClass(param.Parameterized): c = param.Number(default = numbergen.UniformRandom(name = 'test1')) d = param.Number(default = numbergen.UniformRandom(name = 'test2')) e = param.Number(default = numbergen.UniformRandom(name = 'test1')) dynamic = DynamicClass() test1_t1 = 0.23589388250988552 test2_t1 = 0.12576257837158122 test1_t2 = 0.14117586161849593 test2_t2 = 0.9134917395930359 self.time_fn(0) self.assertEqual(dynamic.c, test1_t1) self.assertEqual(dynamic.c, dynamic.e) self.assertNotEqual(dynamic.c, dynamic.d) self.assertEqual(dynamic.d, test2_t1) self.time_fn(1) self.assertEqual(dynamic.c, test1_t2) self.assertEqual(dynamic.c, test1_t2) self.assertEqual(dynamic.d, test2_t2) self.time_fn(0) self.assertEqual(dynamic.c, test1_t1) self.assertEqual(dynamic.d, test2_t1) def test_time_hashing_integers(self): """ Check that ints, fractions and strings hash to the same value for integer values. """ hashfn = numbergen.Hash("test", input_count=1) hash_1 = hashfn(1) hash_42 = hashfn(42) hash_200001 = hashfn(200001) self.assertEqual(hash_1, hashfn(fractions.Fraction(1))) self.assertEqual(hash_1, hashfn("1")) self.assertEqual(hash_42, hashfn(fractions.Fraction(42))) self.assertEqual(hash_42, hashfn("42")) self.assertEqual(hash_200001, hashfn(fractions.Fraction(200001))) self.assertEqual(hash_200001, hashfn("200001")) def test_time_hashing_rationals(self): """ Check that hashes fractions and strings match for some reasonable rational numbers. """ hashfn = numbergen.Hash("test", input_count=1) pi = "3.141592" half = fractions.Fraction(0.5) self.assertEqual(hashfn(0.5), hashfn(half)) self.assertEqual(hashfn(pi), hashfn(fractions.Fraction(pi))) @pytest.mark.skipif(gmpy is None, reason="gmpy is not installed") def test_time_hashing_integers_gmpy(self): """ Check that hashes for gmpy values at the integers also matches those of ints, fractions and strings. """ hashfn = numbergen.Hash("test", input_count=1) hash_1 = hashfn(1) hash_42 = hashfn(42) self.assertEqual(hash_1, hashfn(gmpy.mpq(1))) self.assertEqual(hash_1, hashfn(1)) self.assertEqual(hash_42, hashfn(gmpy.mpq(42))) self.assertEqual(hash_42, hashfn(42)) @pytest.mark.skipif(gmpy is None, reason="gmpy is not installed") def test_time_hashing_rationals_gmpy(self): """ Check that hashes of fractions and gmpy mpqs match for some reasonable rational numbers. """ pi = "3.141592" hashfn = numbergen.Hash("test", input_count=1) self.assertEqual(hashfn(0.5), hashfn(gmpy.mpq(0.5))) self.assertEqual(hashfn(pi), hashfn(gmpy.mpq(3.141592))) param-1.12.3/tests/API1/testutils.py000066400000000000000000000031131434441564000171470ustar00rootroot00000000000000import datetime as dt import param import pytest from param import guess_param_types try: import numpy as np except ImportError: np = None try: import pandas as pd except ImportError: pd = None now = dt.datetime.now() today = dt.date.today() guess_param_types_data = { 'Parameter': (param.Parameter(), param.Parameter), 'Date': (today, param.Date), 'Datetime': (now, param.Date), 'Boolean': (True, param.Boolean), 'Integer': (1, param.Integer), 'Number': (1.2, param.Number), 'String': ('test', param.String), 'Dict': (dict(a=1), param.Dict), 'NumericTuple': ((1, 2), param.NumericTuple), 'Tuple': (('a', 'b'), param.Tuple), 'DateRange': ((dt.date(2000, 1, 1), dt.date(2001, 1, 1)), param.DateRange), 'List': ([1, 2], param.List), 'Unsupported_None': (None, param.Parameter), } if np: guess_param_types_data.update({ 'Array':(np.ndarray([1, 2]), param.Array), }) if pd: guess_param_types_data.update({ 'DataFrame': (pd.DataFrame(data=dict(a=[1])), param.DataFrame), 'Series': (pd.Series([1, 2]), param.Series), }) @pytest.mark.parametrize('val,p', guess_param_types_data.values(), ids=guess_param_types_data.keys()) def test_guess_param_types(val, p): input = {'key': val} output = guess_param_types(**input) assert isinstance(output, dict) assert len(output) == 1 assert 'key' in output out_param = output['key'] assert isinstance(out_param, p) if not type(out_param) == param.Parameter: assert out_param.default is val assert out_param.constant param-1.12.3/tests/API1/testwatch.py000066400000000000000000000634141434441564000171270ustar00rootroot00000000000000""" Unit test for watch mechanism """ import copy import param from param.parameterized import discard_events from . import API1TestCase from .utils import MockLoggingHandler class Accumulator(object): def __init__(self): self.args = [] self.kwargs = [] def __call__(self, *args, **kwargs): self.args.append(args) self.kwargs.append(kwargs) def call_count(self): return max(len(self.args), len(self.kwargs)) def args_for_call(self, number): return self.args[number] def kwargs_for_call(self, number): return self.kwargs[number] class SimpleWatchExample(param.Parameterized): a = param.Parameter(default=0) b = param.Parameter(default=0) c = param.Parameter(default=0) d = param.Integer(default=0) e = param.Event() f = param.Event() def method(self, event): self.b = self.a * 2 class SimpleWatchSubclass(SimpleWatchExample): pass class WatchMethodExample(SimpleWatchSubclass): @param.depends('a', watch='queued') def _clip_a(self): if self.a > 3: self.a = 3 @param.depends('b', watch=True) def _clip_b(self): if self.b > 10: self.b = 10 @param.depends('b', watch=True) def _set_c(self): self.c = self.b*2 @param.depends('c', watch=True) def _set_d_bounds(self): self.param.d.bounds = (self.c, self.c*2) @param.depends('e', watch=True) def _e_event_triggered(self): assert self.e is True self.d = 30 @param.depends('f', watch=True) def _f_event_triggered(self): assert self.f is True self.b = 420 class WatchSubclassExample(WatchMethodExample): pass class TestWatch(API1TestCase): @classmethod def setUpClass(cls): super(TestWatch, cls).setUpClass() log = param.parameterized.get_logger() cls.log_handler = MockLoggingHandler(level='DEBUG') log.addHandler(cls.log_handler) def setUp(self): super(TestWatch, self).setUp() self.accumulator = 0 self.list_accumulator = [] def test_triggered_when_changed(self): def accumulator(change): self.accumulator += change.new obj = SimpleWatchExample() obj.param.watch(accumulator, 'a') obj.a = 1 self.assertEqual(self.accumulator, 1) obj.a = 2 self.assertEqual(self.accumulator, 3) def test_discard_events_decorator(self): def accumulator(change): self.accumulator += change.new obj = SimpleWatchExample() obj.param.watch(accumulator, 'a') with discard_events(obj): obj.a = 1 self.assertEqual(self.accumulator, 0) obj.a = 2 self.assertEqual(self.accumulator, 2) def test_priority_levels(self): def accumulator1(change): self.list_accumulator.append('A') def accumulator2(change): self.list_accumulator.append('B') obj = SimpleWatchExample() obj.param.watch(accumulator1, 'a', precedence=2) obj.param.watch(accumulator2, 'a', precedence=1) obj.a = 1 assert self.list_accumulator == ['B', 'A'] def test_priority_levels_batched(self): def accumulator1(change): self.list_accumulator.append('A') def accumulator2(change): self.list_accumulator.append('B') obj = SimpleWatchExample() obj.param.watch(accumulator1, 'a', precedence=2) obj.param.watch(accumulator2, 'b', precedence=1) obj.param.update(a=1, b=2) assert self.list_accumulator == ['B', 'A'] def test_triggered_when_changed_iterator_type(self): def accumulator(change): self.accumulator = change.new obj = SimpleWatchExample() obj.param.watch(accumulator, 'a') obj.a = [] self.assertEqual(self.accumulator, []) obj.a = tuple() self.assertEqual(self.accumulator, tuple()) def test_triggered_when_changed_mapping_type(self): def accumulator(change): self.accumulator = change.new obj = SimpleWatchExample() obj.param.watch(accumulator, 'a') obj.a = [] self.assertEqual(self.accumulator, []) obj.a = {} self.assertEqual(self.accumulator, {}) def test_untriggered_when_unchanged(self): def accumulator(change): self.accumulator += change.new obj = SimpleWatchExample() obj.param.watch(accumulator, 'a') obj.a = 1 self.assertEqual(self.accumulator, 1) obj.a = 1 self.assertEqual(self.accumulator, 1) def test_triggered_when_unchanged_complex_type(self): def accumulator(change): self.accumulator += 1 obj = SimpleWatchExample() obj.param.watch(accumulator, 'a') subobj = object() obj.a = subobj self.assertEqual(self.accumulator, 1) obj.a = subobj self.assertEqual(self.accumulator, 2) def test_triggered_when_unchanged_if_not_onlychanged(self): accumulator = Accumulator() obj = SimpleWatchExample() obj.param.watch(accumulator, 'a', onlychanged=False) obj.a = 1 self.assertEqual(accumulator.call_count(), 1) args = accumulator.args_for_call(0) self.assertEqual(len(args), 1) self.assertEqual(args[0].name, 'a') self.assertEqual(args[0].what, 'value') self.assertEqual(args[0].old, 0) self.assertEqual(args[0].new, 1) self.assertEqual(args[0].type, 'set') obj.a = 1 args = accumulator.args_for_call(1) self.assertEqual(len(args), 1) self.assertEqual(args[0].name, 'a') self.assertEqual(args[0].what, 'value') self.assertEqual(args[0].old, 1) self.assertEqual(args[0].new, 1) self.assertEqual(args[0].type, 'set') def test_untriggered_when_unwatched(self): def accumulator(change): self.accumulator += change.new obj = SimpleWatchExample() watcher = obj.param.watch(accumulator, 'a') obj.a = 1 self.assertEqual(self.accumulator, 1) obj.param.unwatch(watcher) obj.a = 2 self.assertEqual(self.accumulator, 1) def test_warning_unwatching_when_unwatched(self): def accumulator(change): self.accumulator += change.new obj = SimpleWatchExample() watcher = obj.param.watch(accumulator, 'a') obj.param.unwatch(watcher) obj.param.unwatch(watcher) self.log_handler.assertEndsWith('WARNING', ' to remove.') def test_simple_batched_watch_setattr(self): accumulator = Accumulator() obj = SimpleWatchExample() obj.param.watch(accumulator, ['a', 'b']) obj.a = 2 self.assertEqual(accumulator.call_count(), 1) args = accumulator.args_for_call(0) self.assertEqual(len(args), 1) self.assertEqual(args[0].name, 'a') self.assertEqual(args[0].old, 0) self.assertEqual(args[0].new, 2) self.assertEqual(args[0].type, 'changed') obj.b = 3 self.assertEqual(accumulator.call_count(), 2) args = accumulator.args_for_call(1) self.assertEqual(len(args), 1) self.assertEqual(args[0].name, 'b') self.assertEqual(args[0].old, 0) self.assertEqual(args[0].new, 3) self.assertEqual(args[0].type, 'changed') def test_batched_watch_context_manager(self): accumulator = Accumulator() obj = SimpleWatchExample() obj.param.watch(accumulator, ['a','b']) with param.batch_watch(obj): obj.a = 2 obj.b = 3 self.assertEqual(accumulator.call_count(), 1) args = accumulator.args_for_call(0) self.assertEqual(len(args), 2) self.assertEqual(args[0].name, 'a') self.assertEqual(args[0].old, 0) self.assertEqual(args[0].new, 2) self.assertEqual(args[0].type, 'changed') self.assertEqual(args[1].name, 'b') self.assertEqual(args[1].old, 0) self.assertEqual(args[1].new, 3) self.assertEqual(args[1].type, 'changed') def test_nested_batched_watch_setattr(self): obj = SimpleWatchExample() accumulator = Accumulator() obj.param.watch(accumulator, ['a', 'c']) def set_c(*events): obj.c = 3 obj.param.watch(set_c, ['a', 'b']) obj.param.update(a=2) self.assertEqual(obj.c, 3) # Change inside watch callback should have triggered # second call to accumulator self.assertEqual(accumulator.call_count(), 2) def test_simple_batched_watch(self): accumulator = Accumulator() obj = SimpleWatchExample() obj.param.watch(accumulator, ['a','b']) obj.param.update(a=23, b=42) self.assertEqual(accumulator.call_count(), 1) args = accumulator.args_for_call(0) self.assertEqual(len(args), 2) self.assertEqual(args[0].name, 'a') self.assertEqual(args[0].old, 0) self.assertEqual(args[0].new, 23) self.assertEqual(args[0].type, 'changed') self.assertEqual(args[1].name, 'b') self.assertEqual(args[1].old, 0) self.assertEqual(args[1].new, 42) self.assertEqual(args[1].type, 'changed') def test_simple_class_batched_watch(self): accumulator = Accumulator() obj = SimpleWatchSubclass watcher = obj.param.watch(accumulator, ['a','b']) obj.param.update(a=23, b=42) self.assertEqual(accumulator.call_count(), 1) args = accumulator.args_for_call(0) self.assertEqual(len(args), 2) self.assertEqual(args[0].name, 'a') self.assertEqual(args[0].old, 0) self.assertEqual(args[0].new, 23) self.assertEqual(args[0].type, 'changed') self.assertEqual(args[1].name, 'b') self.assertEqual(args[1].old, 0) self.assertEqual(args[1].new, 42) self.assertEqual(args[1].type, 'changed') SimpleWatchExample.param.unwatch(watcher) obj.param.update(a=0, b=0) def test_simple_batched_watch_callback_reuse(self): accumulator = Accumulator() obj = SimpleWatchExample() obj.param.watch(accumulator, ['a','b']) obj.param.watch(accumulator, ['c']) obj.param.update(a=23, b=42, c=99) self.assertEqual(accumulator.call_count(), 2) # Order may be undefined for Python <3.6 for args in [accumulator.args_for_call(i) for i in [0,1]]: if len(args) == 1: # ['c'] self.assertEqual(args[0].name, 'c') self.assertEqual(args[0].old, 0) self.assertEqual(args[0].new, 99) self.assertEqual(args[0].type, 'changed') elif len(args) == 2: # ['a', 'b'] self.assertEqual(args[0].name, 'a') self.assertEqual(args[0].old, 0) self.assertEqual(args[0].new, 23) self.assertEqual(args[0].type, 'changed') self.assertEqual(args[1].name, 'b') self.assertEqual(args[1].old, 0) self.assertEqual(args[1].new, 42) self.assertEqual(args[0].type, 'changed') else: raise Exception('Invalid number of arguments') def test_context_manager_batched_watch_reuse(self): accumulator = Accumulator() obj = SimpleWatchExample() obj.param.watch(accumulator, ['a','b']) obj.param.watch(accumulator, ['c']) with param.batch_watch(obj): obj.a = 23 obj.b = 42 obj.c = 99 self.assertEqual(accumulator.call_count(), 2) # Order may be undefined for Python <3.6 for args in [accumulator.args_for_call(i) for i in [0, 1]]: if len(args) == 1: # ['c'] self.assertEqual(args[0].name, 'c') self.assertEqual(args[0].old, 0) self.assertEqual(args[0].new, 99) self.assertEqual(args[0].type, 'changed') elif len(args) == 2: # ['a', 'b'] self.assertEqual(args[0].name, 'a') self.assertEqual(args[0].old, 0) self.assertEqual(args[0].new, 23) self.assertEqual(args[0].type, 'changed') self.assertEqual(args[1].name, 'b') self.assertEqual(args[1].old, 0) self.assertEqual(args[1].new, 42) self.assertEqual(args[0].type, 'changed') else: raise Exception('Invalid number of arguments') def test_subclass_batched_watch(self): accumulator = Accumulator() obj = SimpleWatchSubclass() obj.param.watch(accumulator, ['b','c']) obj.param.update(b=23, c=42) self.assertEqual(accumulator.call_count(), 1) args = accumulator.args_for_call(0) self.assertEqual(len(args), 2) self.assertEqual(args[0].name, 'b') self.assertEqual(args[0].old, 0) self.assertEqual(args[0].new, 23) self.assertEqual(args[0].type, 'changed') self.assertEqual(args[1].name, 'c') self.assertEqual(args[1].old, 0) self.assertEqual(args[1].new, 42) self.assertEqual(args[1].type, 'changed') def test_nested_batched_watch(self): accumulator = Accumulator() obj = SimpleWatchExample() def update(*changes): obj.param.update(a=10, d=12) obj.param.watch(accumulator, ['a', 'b', 'c', 'd']) obj.param.watch(update, ['b', 'c']) obj.param.update(b=23, c=42) self.assertEqual(accumulator.call_count(), 2) args = accumulator.args_for_call(0) self.assertEqual(len(args), 2) self.assertEqual(args[0].name, 'b') self.assertEqual(args[0].old, 0) self.assertEqual(args[0].new, 23) self.assertEqual(args[0].type, 'changed') self.assertEqual(args[1].name, 'c') self.assertEqual(args[1].old, 0) self.assertEqual(args[1].new, 42) self.assertEqual(args[1].type, 'changed') args = accumulator.args_for_call(1) self.assertEqual(len(args), 2) self.assertEqual(args[0].name, 'a') self.assertEqual(args[0].old, 0) self.assertEqual(args[0].new, 10) self.assertEqual(args[0].type, 'changed') self.assertEqual(args[1].name, 'd') self.assertEqual(args[1].old, 0) self.assertEqual(args[1].new, 12) self.assertEqual(args[1].type, 'changed') def test_nested_batched_watch_not_onlychanged(self): accumulator = Accumulator() obj = SimpleWatchSubclass() obj.param.watch(accumulator, ['b','c'], onlychanged=False) obj.param.update(b=0, c=0) self.assertEqual(accumulator.call_count(), 1) args = accumulator.args_for_call(0) self.assertEqual(len(args), 2) self.assertEqual(args[0].name, 'b') self.assertEqual(args[0].old, 0) self.assertEqual(args[0].new, 0) self.assertEqual(args[0].type, 'set') self.assertEqual(args[1].name, 'c') self.assertEqual(args[1].old, 0) self.assertEqual(args[1].new, 0) self.assertEqual(args[1].type, 'set') def test_watch_deepcopy(self): obj = SimpleWatchExample() obj.param.watch(obj.method, ['a']) copied = copy.deepcopy(obj) copied.a = 2 self.assertEqual(copied.b, 4) self.assertEqual(obj.b, 0) def test_watch_event_value_trigger(self): obj = WatchMethodExample() obj.e = True self.assertEqual(obj.d, 30) self.assertEqual(obj.e, False) def test_watch_event_trigger_method(self): obj = WatchMethodExample() obj.param.trigger('e') self.assertEqual(obj.d, 30) self.assertEqual(obj.e, False) def test_watch_event_batched_trigger_method(self): obj = WatchMethodExample() obj.param.trigger('e', 'f') self.assertEqual(obj.d, 30) self.b = 420 self.assertEqual(obj.e, False) self.assertEqual(obj.f, False) class TestWatchMethod(API1TestCase): def test_dependent_params(self): obj = WatchMethodExample() obj.b = 3 self.assertEqual(obj.c, 6) def test_multiple_watcher_dispatch_queued(self): obj = WatchMethodExample() obj2 = SimpleWatchExample() def link(event): obj2.a = event.new obj.param.watch(link, 'a', queued=True) obj.a = 4 self.assertEqual(obj.a, 3) self.assertEqual(obj2.a, 3) def test_multiple_watcher_dispatch(self): obj = WatchMethodExample() obj2 = SimpleWatchExample() def link(event): obj2.b = event.new obj.param.watch(link, 'b') obj.b = 11 self.assertEqual(obj.b, 10) self.assertEqual(obj2.b, 11) def test_multiple_watcher_dispatch_on_param_attribute(self): obj = WatchMethodExample() accumulator = Accumulator() obj.param.watch(accumulator, 'd', 'bounds') obj.c = 2 self.assertEqual(obj.param.d.bounds, (2, 4)) self.assertEqual(accumulator.call_count(), 1) args = accumulator.args_for_call(0) self.assertEqual(len(args), 1) self.assertEqual(args[0].name, 'd') self.assertEqual(args[0].what, 'bounds') self.assertEqual(args[0].old, None) self.assertEqual(args[0].new, (2, 4)) self.assertEqual(args[0].type, 'changed') def test_depends_with_watch_on_subclass(self): obj = WatchSubclassExample() obj.b = 3 self.assertEqual(obj.c, 6) def test_watcher_method_deepcopy(self): obj = WatchMethodExample(b=5) copied = copy.deepcopy(obj) copied.b = 11 self.assertEqual(copied.b, 10) self.assertEqual(obj.b, 5) class TestWatchValues(API1TestCase): def setUp(self): super(TestWatchValues, self).setUp() self.accumulator = 0 self.list_accumulator = [] def test_triggered_when_values_changed(self): def accumulator(a): self.accumulator += a obj = SimpleWatchExample() obj.param.watch_values(accumulator, 'a') obj.a = 1 self.assertEqual(self.accumulator, 1) obj.a = 2 self.assertEqual(self.accumulator, 3) def test_untriggered_when_values_unchanged(self): def accumulator(a): self.accumulator += a obj = SimpleWatchExample() obj.param.watch_values(accumulator, 'a') obj.a = 1 self.assertEqual(self.accumulator, 1) obj.a = 1 self.assertEqual(self.accumulator, 1) def test_untriggered_when_values_unwatched(self): def accumulator(a): self.accumulator += a obj = SimpleWatchExample() watcher = obj.param.watch_values(accumulator, 'a') obj.a = 1 self.assertEqual(self.accumulator, 1) obj.param.unwatch(watcher) obj.a = 2 self.assertEqual(self.accumulator, 1) def test_priority_levels(self): def accumulator1(**kwargs): self.list_accumulator.append('A') def accumulator2(**kwargs): self.list_accumulator.append('B') obj = SimpleWatchExample() obj.param.watch_values(accumulator1, 'a', precedence=2) obj.param.watch_values(accumulator2, 'a', precedence=1) obj.a = 1 assert self.list_accumulator == ['B', 'A'] def test_priority_levels_batched(self): def accumulator1(**kwargs): self.list_accumulator.append('A') def accumulator2(**kwargs): self.list_accumulator.append('B') obj = SimpleWatchExample() obj.param.watch_values(accumulator1, 'a', precedence=2) obj.param.watch_values(accumulator2, 'b', precedence=1) obj.param.update(a=1, b=2) assert self.list_accumulator == ['B', 'A'] def test_simple_batched_watch_values_setattr(self): accumulator = Accumulator() obj = SimpleWatchExample() obj.param.watch_values(accumulator, ['a','b']) obj.a = 2 self.assertEqual(accumulator.call_count(), 1) kwargs = accumulator.kwargs_for_call(0) self.assertEqual(len(kwargs), 1) self.assertEqual(kwargs, {'a':2}) obj.b = 3 self.assertEqual(accumulator.call_count(), 2) kwargs = accumulator.kwargs_for_call(1) self.assertEqual(kwargs, {'b':3}) def test_simple_batched_watch_values(self): accumulator = Accumulator() obj = SimpleWatchExample() obj.param.watch_values(accumulator, ['a','b']) obj.param.update(a=23, b=42) self.assertEqual(accumulator.call_count(), 1) kwargs = accumulator.kwargs_for_call(0) self.assertEqual(kwargs, {'a':23, 'b':42}) def test_simple_batched_watch_values_callback_reuse(self): accumulator = Accumulator() obj = SimpleWatchExample() obj.param.watch_values(accumulator, ['a','b']) obj.param.watch_values(accumulator, ['c']) obj.param.update(a=23, b=42, c=99) self.assertEqual(accumulator.call_count(), 2) # Order may be undefined for Python <3.6 for kwargs in [accumulator.kwargs_for_call(i) for i in [0,1]]: if len(kwargs) == 1: # ['c'] self.assertEqual(kwargs, {'c':99}) elif len(kwargs) == 2: # ['a', 'b'] self.assertEqual(kwargs, {'a':23, 'b':42}) else: raise Exception('Invalid number of arguments') class TestWatchAttributes(API1TestCase): def setUp(self): super(TestWatchAttributes, self).setUp() self.accumulator = [] def tearDown(self): SimpleWatchExample.param['d'].bounds = None def test_watch_class_param_attribute(self): def accumulator(a): self.accumulator += [a.new] SimpleWatchExample.param.watch(accumulator, ['d'], 'bounds') SimpleWatchExample.param['d'].bounds = (0, 3) assert self.accumulator == [(0, 3)] def test_watch_instance_param_attribute(self): def accumulator(a): self.accumulator += [a.new] obj = SimpleWatchExample() obj.param.watch(accumulator, ['d'], 'bounds') # Ensure watching an instance parameter makes copy assert obj.param.objects('current')['d'] is not SimpleWatchExample.param['d'] obj.param['d'].bounds = (0, 3) assert SimpleWatchExample.param['d'].bounds is None assert self.accumulator == [(0, 3)] class TestTrigger(API1TestCase): def setUp(self): super(TestTrigger, self).setUp() self.accumulator = 0 def test_simple_trigger_one_param(self): accumulator = Accumulator() obj = SimpleWatchExample() obj.param.watch(accumulator, ['a']) obj.param.trigger('a') self.assertEqual(accumulator.call_count(), 1) args = accumulator.args_for_call(0) self.assertEqual(args[0].name, 'a') self.assertEqual(args[0].old, 0) self.assertEqual(args[0].new, 0) self.assertEqual(args[0].type, 'triggered') def test_simple_trigger_when_batched(self): accumulator = Accumulator() obj = SimpleWatchExample() obj.param.watch(accumulator, ['a']) with param.batch_watch(obj): obj.param.trigger('a') self.assertEqual(accumulator.call_count(), 1) args = accumulator.args_for_call(0) self.assertEqual(args[0].name, 'a') self.assertEqual(args[0].old, 0) self.assertEqual(args[0].new, 0) # Note: This is not strictly correct self.assertEqual(args[0].type, 'changed') def test_simple_trigger_one_param_change(self): accumulator = Accumulator() obj = SimpleWatchExample() obj.param.watch(accumulator, ['a']) obj.a = 42 self.assertEqual(accumulator.call_count(), 1) obj.param.trigger('a') self.assertEqual(accumulator.call_count(), 2) args = accumulator.args_for_call(0) self.assertEqual(args[0].name, 'a') self.assertEqual(args[0].old, 0) self.assertEqual(args[0].new, 42) self.assertEqual(args[0].type, 'changed') args = accumulator.args_for_call(1) self.assertEqual(args[0].name, 'a') self.assertEqual(args[0].old, 42) self.assertEqual(args[0].new, 42) self.assertEqual(args[0].type, 'triggered') def test_simple_trigger_two_params(self): accumulator = Accumulator() obj = SimpleWatchExample() obj.param.watch(accumulator, ['a','b']) obj.param.trigger('a','b') self.assertEqual(accumulator.call_count(), 1) args = accumulator.args_for_call(0) self.assertEqual(args[0].name, 'a') self.assertEqual(args[0].old, 0) self.assertEqual(args[0].new, 0) self.assertEqual(args[0].type, 'triggered') self.assertEqual(args[1].name, 'b') self.assertEqual(args[1].old, 0) self.assertEqual(args[1].new, 0) self.assertEqual(args[1].type, 'triggered') def test_sensitivity_of_widget_name(self): # From: https://github.com/holoviz/param/issues/614 class ExampleWidget(param.Parameterized): value = param.Number(default=1) class Example(param.Parameterized): da = param.Number(default=1) date_picker = param.Parameter(ExampleWidget()) picker = param.Parameter(ExampleWidget()) @param.depends( "date_picker.value", "picker.value", watch=True, ) def load_data(self): self.da += 1 # To trigger plot_time @param.depends("da") def plot_time(self): return self.da example = Example() example.picker.value += 1 assert example.da == 2 example.picker.value += 1 assert example.da == 3 param-1.12.3/tests/API1/utils.py000066400000000000000000000061601434441564000162540ustar00rootroot00000000000000import logging class MockLoggingHandler(logging.Handler): """Mock logging handler to check for expected logs. Messages are available from an instance's ``messages`` dict, in order, indexed by a lowercase log level string (e.g., 'debug', 'info', etc.). This is typically used by using a setUpClass classmethod and a setUp method on a test case. The setUpClass classmethod can be configured as follows after calling super (with cls): log = param.parameterized.get_logger() cls.log_handler = MockLoggingHandler(level='DEBUG') log.addHandler(cls.log_handler) The setUp method then just needs to call self.log_handler.reset() between tests (typically after invoking super). This is necessary to make the tests independent where the tests can use the self.log_handler.tail and self.log_handler.assertEndsWith methods. """ def __init__(self, *args, **kwargs): self.messages = {'DEBUG': [], 'INFO': [], 'WARNING': [], 'ERROR': [], 'CRITICAL': [], 'VERBOSE':[]} super(MockLoggingHandler, self).__init__(*args, **kwargs) def emit(self, record): "Store a message to the instance's messages dictionary" self.acquire() try: self.messages[record.levelname].append(record.getMessage()) finally: self.release() def reset(self): self.acquire() self.messages = {'DEBUG': [], 'INFO': [], 'WARNING': [], 'ERROR': [], 'CRITICAL': [], 'VERBOSE':[]} self.release() def tail(self, level, n=1): "Returns the last n lines captured at the given level" return [str(el) for el in self.messages[level][-n:]] def assertEndsWith(self, level, substring): """ Assert that the last line captured at the given level ends with a particular substring. """ msg='\n\nparam.log({level},...): {last_line}\ndoes not end with:\n{substring}' last_line = self.tail(level, n=1) if len(last_line) == 0: raise AssertionError('Missing param.log({level},...) output: {substring}'.format( level=level, substring=repr(substring))) if not last_line[0].endswith(substring): raise AssertionError(msg.format(level=level, last_line=repr(last_line[0]), substring=repr(substring))) def assertContains(self, level, substring): """ Assert that the last line captured at the given level contains a particular substring. """ msg='\n\nparam.log({level},...): {last_line}\ndoes not contain:\n{substring}' last_line = self.tail(level, n=1) if len(last_line) == 0: raise AssertionError('Missing {method} output: {substring}'.format( level=level, substring=repr(substring))) if substring not in last_line[0]: raise AssertionError(msg.format(level=level, last_line=repr(last_line[0]), substring=repr(substring))) param-1.12.3/tests/README.md000066400000000000000000000010471434441564000153260ustar00rootroot00000000000000In 2018, we moved most of `Parameterized` onto a `param` namespace object, expecting this to be the public API of param 2.0, and cleaning up the namespace of user classes. The new API has been in use for a while within holoviz projects, but we're still changing it. Meanwhile, the previous API remains available. The original API's tests were copied into an `API0` subdirectory, while tests in `API1` use the new API. (Probably not ideal to just copy everything, and cleaning up would be great, but this explains the two directories you see here.) param-1.12.3/tests/__init__.py000066400000000000000000000000001434441564000161440ustar00rootroot00000000000000param-1.12.3/tox.ini000066400000000000000000000031251434441564000142170ustar00rootroot00000000000000[tox] envlist = py36,py37,py38,py39,py310,py311,pypy3 [gh-actions] python = 3.6: py36 3.7: py37 3.8: py38 3.9: py39 3.10: py310 3.11: py311 pypy-3.7: pypy3 [testenv] description = run test suite under {basepython} deps = .[tests] commands = pytest tests --cov=numbergen --cov=param --cov-append --cov-report xml [testenv:with_numpy] description = run test suite with numpy under {basepython} deps = {[testenv]deps} numpy setenv = PARAM_TEST_NUMPY = 1 [testenv:with_pandas] description = run test suite with pandas under {basepython} deps = {[testenv]deps} pandas setenv = PARAM_TEST_PANDAS = 1 [testenv:with_ipython] description = run test suite with ipython under {basepython} deps = {[testenv]deps} ipython setenv = PARAM_TEST_IPYTHON = 1 [testenv:with_jsonschema] description = run test suite with jsonschema under {basepython} deps = {[testenv]deps} jsonschema setenv = PARAM_TEST_JSONSCHEMA = 1 [testenv:with_gmpy] description = run test suite with gmpy under {basepython} deps = {[testenv]deps} gmpy setenv = PARAM_TEST_GMPY = 1 [testenv:with_all] deps = {[testenv:with_numpy]deps} {[testenv:with_pandas]deps} {[testenv:with_ipython]deps} {[testenv:with_jsonschema]deps} {[testenv:with_gmpy]deps} setenv = {[testenv:with_numpy]setenv} {[testenv:with_pandas]setenv} {[testenv:with_ipython]setenv} {[testenv:with_jsonschema]setenv} {[testenv:with_gmpy]setenv} [testenv:flakes] description = run flake8 under {basepython} deps = flake8 skip_install = true commands = flake8 param-1.12.3/tox27.ini000066400000000000000000000030021434441564000143620ustar00rootroot00000000000000[tox] envlist = py27 [gh-actions] python = 2.7: py27 [testenv] description = run test suite under {basepython} deps = .[tests] commands = pytest tests --ignore tests/API1/testparamdepends_py3.py --cov=numbergen --cov=param --cov-append --cov-report xml [testenv:with_numpy] description = run test suite with numpy under {basepython} deps = {[testenv]deps} numpy setenv = PARAM_TEST_NUMPY = 1 [testenv:with_pandas] description = run test suite with pandas under {basepython} deps = {[testenv]deps} pandas setenv = PARAM_TEST_PANDAS = 1 [testenv:with_ipython] description = run test suite with ipython under {basepython} deps = {[testenv]deps} ipython setenv = PARAM_TEST_IPYTHON = 1 [testenv:with_jsonschema] description = run test suite with jsonschema under {basepython} deps = {[testenv]deps} jsonschema setenv = PARAM_TEST_JSONSCHEMA = 1 [testenv:with_gmpy] description = run test suite with gmpy under {basepython} deps = {[testenv]deps} gmpy setenv = PARAM_TEST_GMPY = 1 [testenv:with_all] deps = {[testenv:with_numpy]deps} {[testenv:with_pandas]deps} {[testenv:with_ipython]deps} {[testenv:with_jsonschema]deps} {[testenv:with_gmpy]deps} setenv = {[testenv:with_numpy]setenv} {[testenv:with_pandas]setenv} {[testenv:with_ipython]setenv} {[testenv:with_jsonschema]setenv} {[testenv:with_gmpy]setenv} [testenv:flakes] description = run flake8 under {basepython} deps = flake8 skip_install = true commands = flake8