pax_global_header00006660000000000000000000000064146363633630014527gustar00rootroot0000000000000052 comment=14771aba95594ec3e2c7a27f214a2e91fd2dff6c param-2.1.1/000077500000000000000000000000001463636336300126305ustar00rootroot00000000000000param-2.1.1/.flake8000066400000000000000000000010371463636336300140040ustar00rootroot00000000000000[flake8] # TODO tests should not be excluded (one day...) include = param numbergen exclude = .venv, jupyter_execute, .git, __pycache__, .eggs, *.egg, doc, dist, build, _build, tests, .ipynb_checkpoints ignore = E114, E116, E126, E128, E129, E2, E3, E4, E5, E731, E701, E702, E703, E704, E722, E741, E742, E743, W503, W504, param-2.1.1/.gitattributes000066400000000000000000000000321463636336300155160ustar00rootroot00000000000000 __init__.py export-subst param-2.1.1/.github/000077500000000000000000000000001463636336300141705ustar00rootroot00000000000000param-2.1.1/.github/FUNDING.yml000066400000000000000000000001101463636336300157750ustar00rootroot00000000000000# These are supported funding model platforms open_collective: holoviz param-2.1.1/.github/workflows/000077500000000000000000000000001463636336300162255ustar00rootroot00000000000000param-2.1.1/.github/workflows/build.yaml000066400000000000000000000053231463636336300202130ustar00rootroot00000000000000name: 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.9" CHANS: "-c pyviz" CONDA_UPLOAD_TOKEN: ${{ secrets.CONDA_UPLOAD_TOKEN }} steps: - uses: actions/checkout@v4 - name: Fetch unshallow run: git fetch --prune --tags --unshallow -f - uses: actions/setup-python@v5 with: python-version: "3.9" - uses: conda-incubator/setup-miniconda@v3 with: miniconda-version: "latest" - name: Set output id: vars run: echo "tag=${GITHUB_REF#refs/*/}" >> $GITHUB_OUTPUT - name: conda setup run: | conda update --name base conda conda install anaconda-client conda-build pip install hatch - name: conda build run: | VERSION=`hatch version` 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: | anaconda --token $CONDA_UPLOAD_TOKEN upload --user pyviz --label=dev $(VERSION=`hatch version` 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: | anaconda --token $CONDA_UPLOAD_TOKEN upload --user pyviz --label=dev --label=main $(VERSION=`hatch version` conda build --output conda.recipe) pip_build: name: Build PyPI Packages runs-on: 'ubuntu-latest' defaults: run: shell: bash -l {0} steps: - uses: actions/checkout@v4 - name: Fetch unshallow run: git fetch --prune --tags --unshallow -f - uses: actions/setup-python@v5 with: python-version: "3.9" - name: env setup run: | python -m pip install --upgrade pip python -m pip install build - name: pip build run: | python -m build - name: Publish package to PyPI if: github.event_name == 'push' uses: pypa/gh-action-pypi-publish@release/v1 with: user: ${{ secrets.PPU }} password: ${{ secrets.PPP }} packages_dir: dist/ param-2.1.1/.github/workflows/docs.yaml000066400000000000000000000042461463636336300200470ustar00rootroot00000000000000name: 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} steps: - uses: actions/checkout@v4 - name: Fetch unshallow run: git fetch --prune --tags --unshallow -f - uses: actions/setup-python@v5 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 hatch - name: build docs run: hatch -v run docs:build - name: Deploy dev uses: peaceiris/actions-gh-pages@v4 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: holoviz-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@v4 with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: ./builtdocs cname: param.holoviz.org force_orphan: true param-2.1.1/.github/workflows/downstream_tests.yaml000066400000000000000000000012271463636336300225200ustar00rootroot00000000000000name: 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: holoviz-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-2.1.1/.github/workflows/test.yaml000066400000000000000000000053061463636336300200740ustar00rootroot00000000000000name: tests on: push: branches: - main pull_request: branches: - '*' workflow_dispatch: schedule: - cron: '0 13 * * SUN' concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true jobs: pre_commit: name: Run pre-commit hooks runs-on: 'ubuntu-latest' steps: - uses: actions/checkout@v4 with: fetch-depth: "1" - name: set PY run: echo "PY=$(python -VV | sha256sum | cut -d' ' -f1)" >> $GITHUB_ENV - uses: actions/cache@v4 with: path: ~/.cache/pre-commit key: pre-commit|${{ env.PY }}|${{ hashFiles('.pre-commit-config.yaml') }} - name: pre-commit uses: pre-commit/action@v3.0.0 test_suite: name: Test ${{ matrix.python-version }}, ${{ matrix.platform }} needs: [pre_commit] runs-on: ${{ matrix.platform }} strategy: fail-fast: false matrix: platform: ['ubuntu-latest', 'windows-latest', 'macos-latest'] python-version: ${{ ( github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || ( github.event_name == 'push' && github.ref_type == 'tag' ) ) && fromJSON('["3.8", "3.9", "3.10", "3.11", "3.12", "pypy3.9"]') || fromJSON('["3.8", "3.10", "3.12"]') }} timeout-minutes: 30 steps: - uses: actions/checkout@v4 with: fetch-depth: "100" - name: Fetch unshallow run: git fetch --prune --tags --unshallow -f - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} allow-prereleases: true - name: env setup run: | python -m pip install --upgrade pip python -m pip install hatch - name: temp patch pip for blosc2 if: contains(matrix.platform, 'ubuntu') && matrix.python-version == '3.8' run: echo "PIP_ONLY_BINARY=blosc2" >> $GITHUB_ENV - name: run unit tests run: hatch -v run +py=${{ matrix.python-version }} tests:with_coverage - name: run examples tests # A notebook fails on Windows (UNIX path used in an example) # No need to run these tests for PyPy really if: contains(matrix.platform, 'ubuntu') && !startsWith(matrix.python-version, 'py') run: hatch -v run +py=${{ matrix.python-version }} tests_examples:examples - name: Upload coverage reports to Codecov if: github.event_name == 'push' || github.event_name == 'pull_request' uses: codecov/codecov-action@v4 with: fail_ci_if_error: false verbose: false env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} param-2.1.1/.gitignore000066400000000000000000000006011463636336300146150ustar00rootroot00000000000000*.py[cod] #*# *~ *.egg *.egg-info *.swp *.DS_Store *.so *.o *.out *.lock .ipynb_checkpoints .vscode dist/ .venv/ # Docs builtdocs/ jupyter_execute/ doc/Reference_Manual/ doc/user_guide/data.pickle doc/user_guide/output.csv doc/reference/generated # Unit test / Coverage report .coverage coverage.xml # Versionning param/_version.py # asv benchmark benchmarks/.asv benchmarks/param param-2.1.1/.gitmodules000066400000000000000000000000001463636336300147730ustar00rootroot00000000000000param-2.1.1/.pre-commit-config.yaml000066400000000000000000000012731463636336300171140ustar00rootroot00000000000000# This is the configuration for pre-commit, a local framework for managing pre-commit hooks # Check out the docs at: https://pre-commit.com/ default_stages: [commit] repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.3.0 hooks: - id: check-builtin-literals - id: check-case-conflict - id: check-docstring-first - id: check-toml - id: detect-private-key - id: end-of-file-fixer exclude: (\.min\.js$|\.svg$) - id: trailing-whitespace - repo: https://github.com/PyCQA/flake8 rev: 6.0.0 hooks: - id: flake8 - repo: https://github.com/hoxbro/clean_notebook rev: v0.1.10 hooks: - id: clean-notebook param-2.1.1/CODE_OF_CONDUCT.md000066400000000000000000000004541463636336300154320ustar00rootroot00000000000000# Code of Conduct For the code of conduct, see [HoloViz/HoloViz - CODE_OF_CONDUCT.md](https://github.com/holoviz/holoviz/blob/param-gov/CODE_OF_CONDUCT.md). The Param Project’s equivalently named documents take precedence over any external materials referenced within this linked document above. param-2.1.1/LICENSE.txt000066400000000000000000000027421463636336300144600ustar00rootroot00000000000000Copyright (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-2.1.1/README.md000066400000000000000000000053001463636336300141050ustar00rootroot00000000000000 | | | | --- | --- | | Build Status | [![Linux/MacOS/Windows Build Status](https://github.com/holoviz/param/workflows/tests/badge.svg)](https://github.com/holoviz/param/actions/workflows/test.yaml) | Coverage | [![codecov](https://codecov.io/gh/holoviz/param/branch/main/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/holoviz-dev.github.io/param.svg?label=dev%20website)](https://holoviz-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/main?labpath=doc) | | 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-2.1.1/benchmarks/000077500000000000000000000000001463636336300147455ustar00rootroot00000000000000param-2.1.1/benchmarks/README.md000066400000000000000000000056741463636336300162400ustar00rootroot00000000000000# Benchmarking `Param` uses ASV (https://asv.readthedocs.io) for benchmarking. ## Preparation Install `asv` into your environment with for example: ``` hatch shell cd benchmarks pip install asv ``` ASV then runs benchmarks in isolated virtual environments that it creates using `virtualenv`. ## Running benchmarks To run all benchmarks against the default `main` branch: ``` cd benchmarks asv run ``` The first time this is run it will create a machine file to store information about your machine. Then a virtual environment will be created and each benchmark will be run multiple times to obtain a statistically valid benchmark time. To list the benchmark timings stored for the `main` branch use: ``` asv show main ``` ASV ships with its own simple webserver to interactively display the results in a webbrowser. To use this: ``` asv publish asv preview ``` and then open a web browser at the URL specified. If you want to quickly run all benchmarks once only to check for errors, etc, use: ``` asv dev ``` instead of `asv run`. ## Adding new benchmarks Add new benchmarks to existing or new classes in the `benchmarks/benchmarks` directory. Any class member function with a name that starts with `time` will be identified as a timing benchmark when `asv` is run. Data that is required to run benchmarks is usually created in the `setup()` member function. This ensures that the time taken to setup the data is not included in the benchmark time. The `setup()` function is called once for each invocation of each benchmark, the data are not cached. At the top of each benchmark class there are lists of parameter names and values. Each benchmark is repeated for each unique combination of these parameters. If you only want to run a subset of benchmarks, use syntax like: ``` asv run -b ShadeCategorical ``` where the text after the `-b` flag is used as a regex to match benchmark file, class and function names. ## Benchmarking code changes You can compare the performance of code on different branches and in different commits. Usually if you want to determine how much faster a new algorithm is, the old code will be in the `main` branch and the new code will be in a new feature branch. Because ASV uses virtual environments and checks out the `param` source code into these virtual environments, your new code must be committed into the new feature branch locally. To benchmark the latest commits on `main` and your new feature branch, edit `asv.conf.json` to change the line ``` "branches": ["main"], ``` into ``` "branches": ["main", "new_feature_branch"], ``` or similar. Now when you `asv run` the benchmarks will be run against both branches in turn. Then use ``` asv show ``` to list the commits that have been benchmarked, and ``` asv compare commit1 commit2 ``` to give you a side-by-side comparison of the two commits. You can run `asv` for single tags and compare them ``` asv run v0.0.1^! asv run v0.0.2^! asv compare v0.0.1 v0.0.2 ``` ``` asv run v0.0.2..HEAD param-2.1.1/benchmarks/asv.conf.json000066400000000000000000000015441463636336300173610ustar00rootroot00000000000000{ "version": 1, "project": "param", "project_url": "https://param.holoviz.org", "repo": "..", "dvcs": "git", "repo_subdir": "", "branches": ["main"], "install_command": ["in-dir={env_dir} python -m pip install {wheel_file}"], "uninstall_command": ["return-code=any python -m pip uninstall -y {project}"], "build_command": [ "python -m pip install hatch hatch-vcs", "PIP_NO_BUILD_ISOLATION=false python -mpip wheel --no-deps --no-index -w {build_cache_dir} {build_dir}" ], "environment_type": "virtualenv", "install_timeout": 600, "show_commit_url": "https://github.com/holoviz/param/commit/", // "pythons": ["3.8"], "benchmark_dir": "benchmarks", "env_dir": ".asv/env", "results_dir": ".asv/results", "html_dir": ".asv/html", "hash_length": 8, "build_cache_size": 2 } param-2.1.1/benchmarks/benchmarks/000077500000000000000000000000001463636336300170625ustar00rootroot00000000000000param-2.1.1/benchmarks/benchmarks/__init__.py000066400000000000000000000000001463636336300211610ustar00rootroot00000000000000param-2.1.1/benchmarks/benchmarks/benchmarks.py000066400000000000000000000444531463636336300215630ustar00rootroot00000000000000# Write the benchmarking functions here. # See "Writing benchmarks" in the asv docs for more information. import param class ImportSuite: def timeraw_import_param(self): return """ import param """ class ParameterSuite: def time_instantiation(self): param.Parameter() class ParameterizedSuite: def time_class_bare(self): class P(param.Parameterized): pass def time_class_with_1_parameter(self): class P(param.Parameterized): x0 = param.Parameter() def time_class_with_10_parameter(self): class P(param.Parameterized): x0 = param.Parameter() x1 = param.Parameter() x2 = param.Parameter() x3 = param.Parameter() x4 = param.Parameter() x5 = param.Parameter() x6 = param.Parameter() x7 = param.Parameter() x8 = param.Parameter() x9 = param.Parameter() def time_class_with_100_parameter(self): class P(param.Parameterized): x0 = param.Parameter() x1 = param.Parameter() x2 = param.Parameter() x3 = param.Parameter() x4 = param.Parameter() x5 = param.Parameter() x6 = param.Parameter() x7 = param.Parameter() x8 = param.Parameter() x9 = param.Parameter() x10 = param.Parameter() x11 = param.Parameter() x12 = param.Parameter() x13 = param.Parameter() x14 = param.Parameter() x15 = param.Parameter() x16 = param.Parameter() x17 = param.Parameter() x18 = param.Parameter() x19 = param.Parameter() x20 = param.Parameter() x21 = param.Parameter() x22 = param.Parameter() x23 = param.Parameter() x24 = param.Parameter() x25 = param.Parameter() x26 = param.Parameter() x27 = param.Parameter() x28 = param.Parameter() x29 = param.Parameter() x30 = param.Parameter() x31 = param.Parameter() x32 = param.Parameter() x33 = param.Parameter() x34 = param.Parameter() x35 = param.Parameter() x36 = param.Parameter() x37 = param.Parameter() x38 = param.Parameter() x39 = param.Parameter() x40 = param.Parameter() x41 = param.Parameter() x42 = param.Parameter() x43 = param.Parameter() x44 = param.Parameter() x45 = param.Parameter() x46 = param.Parameter() x47 = param.Parameter() x48 = param.Parameter() x49 = param.Parameter() x50 = param.Parameter() x51 = param.Parameter() x52 = param.Parameter() x53 = param.Parameter() x54 = param.Parameter() x55 = param.Parameter() x56 = param.Parameter() x57 = param.Parameter() x58 = param.Parameter() x59 = param.Parameter() x60 = param.Parameter() x61 = param.Parameter() x62 = param.Parameter() x63 = param.Parameter() x64 = param.Parameter() x65 = param.Parameter() x66 = param.Parameter() x67 = param.Parameter() x68 = param.Parameter() x69 = param.Parameter() x70 = param.Parameter() x71 = param.Parameter() x72 = param.Parameter() x73 = param.Parameter() x74 = param.Parameter() x75 = param.Parameter() x76 = param.Parameter() x77 = param.Parameter() x78 = param.Parameter() x79 = param.Parameter() x80 = param.Parameter() x81 = param.Parameter() x82 = param.Parameter() x83 = param.Parameter() x84 = param.Parameter() x85 = param.Parameter() x86 = param.Parameter() x87 = param.Parameter() x88 = param.Parameter() x89 = param.Parameter() x90 = param.Parameter() x91 = param.Parameter() x92 = param.Parameter() x93 = param.Parameter() x94 = param.Parameter() x95 = param.Parameter() x96 = param.Parameter() x97 = param.Parameter() x98 = param.Parameter() x99 = param.Parameter() class ParameterizedInstantiateSuite: def setup(self): class P1(param.Parameterized): x0 = param.Parameter() class P10(param.Parameterized): x0 = param.Parameter() x1 = param.Parameter() x2 = param.Parameter() x3 = param.Parameter() x4 = param.Parameter() x5 = param.Parameter() x6 = param.Parameter() x7 = param.Parameter() x8 = param.Parameter() x9 = param.Parameter() class P100(param.Parameterized): x0 = param.Parameter() x1 = param.Parameter() x2 = param.Parameter() x3 = param.Parameter() x4 = param.Parameter() x5 = param.Parameter() x6 = param.Parameter() x7 = param.Parameter() x8 = param.Parameter() x9 = param.Parameter() x10 = param.Parameter() x11 = param.Parameter() x12 = param.Parameter() x13 = param.Parameter() x14 = param.Parameter() x15 = param.Parameter() x16 = param.Parameter() x17 = param.Parameter() x18 = param.Parameter() x19 = param.Parameter() x20 = param.Parameter() x21 = param.Parameter() x22 = param.Parameter() x23 = param.Parameter() x24 = param.Parameter() x25 = param.Parameter() x26 = param.Parameter() x27 = param.Parameter() x28 = param.Parameter() x29 = param.Parameter() x30 = param.Parameter() x31 = param.Parameter() x32 = param.Parameter() x33 = param.Parameter() x34 = param.Parameter() x35 = param.Parameter() x36 = param.Parameter() x37 = param.Parameter() x38 = param.Parameter() x39 = param.Parameter() x40 = param.Parameter() x41 = param.Parameter() x42 = param.Parameter() x43 = param.Parameter() x44 = param.Parameter() x45 = param.Parameter() x46 = param.Parameter() x47 = param.Parameter() x48 = param.Parameter() x49 = param.Parameter() x50 = param.Parameter() x51 = param.Parameter() x52 = param.Parameter() x53 = param.Parameter() x54 = param.Parameter() x55 = param.Parameter() x56 = param.Parameter() x57 = param.Parameter() x58 = param.Parameter() x59 = param.Parameter() x60 = param.Parameter() x61 = param.Parameter() x62 = param.Parameter() x63 = param.Parameter() x64 = param.Parameter() x65 = param.Parameter() x66 = param.Parameter() x67 = param.Parameter() x68 = param.Parameter() x69 = param.Parameter() x70 = param.Parameter() x71 = param.Parameter() x72 = param.Parameter() x73 = param.Parameter() x74 = param.Parameter() x75 = param.Parameter() x76 = param.Parameter() x77 = param.Parameter() x78 = param.Parameter() x79 = param.Parameter() x80 = param.Parameter() x81 = param.Parameter() x82 = param.Parameter() x83 = param.Parameter() x84 = param.Parameter() x85 = param.Parameter() x86 = param.Parameter() x87 = param.Parameter() x88 = param.Parameter() x89 = param.Parameter() x90 = param.Parameter() x91 = param.Parameter() x92 = param.Parameter() x93 = param.Parameter() x94 = param.Parameter() x95 = param.Parameter() x96 = param.Parameter() x97 = param.Parameter() x98 = param.Parameter() x99 = param.Parameter() self.P1 = P1 self.P10 = P10 self.P100 = P100 def time_1_parameters(self): self.P1() def time_10_parameters(self): self.P10() def time_100_parameters(self): self.P100() class ParameterizedParamAccessSuite: def setup(self): class P1(param.Parameterized): x0 = param.Parameter() self.P1 = P1 self.p1 = P1() def time_class(self): self.P1.param def time_instance(self): self.p1.param class ParameterizedParamContainsSuite: def setup(self): class P1(param.Parameterized): x0 = param.Parameter() x1 = param.Parameter() x2 = param.Parameter() x3 = param.Parameter() x4 = param.Parameter() x5 = param.Parameter() self.P1 = P1 self.p1 = P1() def time_class(self): 'x5' in self.P1.param def time_instance(self): 'x5' in self.p1.param class ParameterizedSetattrSuite: def setup(self): class P1(param.Parameterized): x0 = param.Parameter() self.P1 = P1 self.p1 = P1(x0=0) def time_class(self): self.P1.x0 = 1 def time_instance(self): self.p1.x0 = 1 class ParameterizedDependsSuite: def time_declarative_1_parameter(self): class P(param.Parameterized): x0 = param.Parameter() @param.depends('x0') def foo0(self): pass def time_watch_1_parameter(self): class P(param.Parameterized): x0 = param.Parameter() @param.depends('x0', watch=True) def foo0(self): pass def time_declarative_10_parameters_separate_cb(self): class P(param.Parameterized): x0 = param.Parameter() x1 = param.Parameter() x2 = param.Parameter() x3 = param.Parameter() x4 = param.Parameter() x5 = param.Parameter() x6 = param.Parameter() x7 = param.Parameter() x8 = param.Parameter() x9 = param.Parameter() @param.depends('x0') def foo0(self): pass @param.depends('x1') def foo1(self): pass @param.depends('x2') def foo2(self): pass @param.depends('x3') def foo3(self): pass @param.depends('x4') def foo4(self): pass @param.depends('x5') def foo5(self): pass @param.depends('x6') def foo6(self): pass @param.depends('x7') def foo7(self): pass @param.depends('x8') def foo8(self): pass @param.depends('x9') def foo9(self): pass def time_declarative_10_parameters_shared_cb(self): class P(param.Parameterized): x0 = param.Parameter() x1 = param.Parameter() x2 = param.Parameter() x3 = param.Parameter() x4 = param.Parameter() x5 = param.Parameter() x6 = param.Parameter() x7 = param.Parameter() x8 = param.Parameter() x9 = param.Parameter() @param.depends('x0', 'x1', 'x2', 'x3', 'x4', 'x5', 'x6', 'x7', 'x8', 'x9') def foo(self): pass def time_watch_10_parameters_separate_cb(self): class P(param.Parameterized): x0 = param.Parameter() x1 = param.Parameter() x2 = param.Parameter() x3 = param.Parameter() x4 = param.Parameter() x5 = param.Parameter() x6 = param.Parameter() x7 = param.Parameter() x8 = param.Parameter() x9 = param.Parameter() @param.depends('x0', watch=True) def foo0(self): pass @param.depends('x1', watch=True) def foo1(self): pass @param.depends('x2', watch=True) def foo2(self): pass @param.depends('x3', watch=True) def foo3(self): pass @param.depends('x4', watch=True) def foo4(self): pass @param.depends('x5', watch=True) def foo5(self): pass @param.depends('x6', watch=True) def foo6(self): pass @param.depends('x7', watch=True) def foo7(self): pass @param.depends('x8', watch=True) def foo8(self): pass @param.depends('x9', watch=True) def foo9(self): pass def time_watch_10_parameters_shared_cb(self): class P(param.Parameterized): x0 = param.Parameter() x1 = param.Parameter() x2 = param.Parameter() x3 = param.Parameter() x4 = param.Parameter() x5 = param.Parameter() x6 = param.Parameter() x7 = param.Parameter() x8 = param.Parameter() x9 = param.Parameter() @param.depends('x0', 'x1', 'x2', 'x3', 'x4', 'x5', 'x6', 'x7', 'x8', 'x9', watch=True) def foo(self): pass class ParameterizedDependsInstantiateSuite: def setup(self): class P1Declarative(param.Parameterized): x0 = param.Parameter() @param.depends('x0') def foo0(self): pass class P1Watch(param.Parameterized): x0 = param.Parameter() @param.depends('x0', watch=True) def foo0(self): pass class P10DeclarativeSeparate(param.Parameterized): x0 = param.Parameter() x1 = param.Parameter() x2 = param.Parameter() x3 = param.Parameter() x4 = param.Parameter() x5 = param.Parameter() x6 = param.Parameter() x7 = param.Parameter() x8 = param.Parameter() x9 = param.Parameter() @param.depends('x0') def foo0(self): pass @param.depends('x1') def foo1(self): pass @param.depends('x2') def foo2(self): pass @param.depends('x3') def foo3(self): pass @param.depends('x4') def foo4(self): pass @param.depends('x5') def foo5(self): pass @param.depends('x6') def foo6(self): pass @param.depends('x7') def foo7(self): pass @param.depends('x8') def foo8(self): pass @param.depends('x9') def foo9(self): pass class P10DeclarativeShared(param.Parameterized): x0 = param.Parameter() x1 = param.Parameter() x2 = param.Parameter() x3 = param.Parameter() x4 = param.Parameter() x5 = param.Parameter() x6 = param.Parameter() x7 = param.Parameter() x8 = param.Parameter() x9 = param.Parameter() @param.depends('x0', 'x1', 'x2', 'x3', 'x4', 'x5', 'x6', 'x7', 'x8', 'x9') def foo(self): pass class P10WatchSeparate(param.Parameterized): x0 = param.Parameter() x1 = param.Parameter() x2 = param.Parameter() x3 = param.Parameter() x4 = param.Parameter() x5 = param.Parameter() x6 = param.Parameter() x7 = param.Parameter() x8 = param.Parameter() x9 = param.Parameter() @param.depends('x0', watch=True) def foo0(self): pass @param.depends('x1', watch=True) def foo1(self): pass @param.depends('x2', watch=True) def foo2(self): pass @param.depends('x3', watch=True) def foo3(self): pass @param.depends('x4', watch=True) def foo4(self): pass @param.depends('x5', watch=True) def foo5(self): pass @param.depends('x6', watch=True) def foo6(self): pass @param.depends('x7', watch=True) def foo7(self): pass @param.depends('x8', watch=True) def foo8(self): pass @param.depends('x9', watch=True) def foo9(self): pass class P10WatchShared(param.Parameterized): x0 = param.Parameter() x1 = param.Parameter() x2 = param.Parameter() x3 = param.Parameter() x4 = param.Parameter() x5 = param.Parameter() x6 = param.Parameter() x7 = param.Parameter() x8 = param.Parameter() x9 = param.Parameter() @param.depends('x0', 'x1', 'x2', 'x3', 'x4', 'x5', 'x6', 'x7', 'x8', 'x9', watch=True) def foo(self): pass self.P1Declarative = P1Declarative self.P1Watch = P1Watch self.P10DeclarativeSeparate = P10DeclarativeSeparate self.P10DeclarativeShared = P10DeclarativeShared self.P10WatchSeparate = P10WatchSeparate self.P10WatchShared = P10WatchShared def time_declarative_1_parameter(self): self.P1Declarative() def time_watch_1_parameter(self): self.P1Watch() def time_declarative_10_parameters_separate_cb(self): self.P10DeclarativeSeparate() def time_declarative_10_parameters_shared_cb(self): self.P10DeclarativeShared() def time_watch_10_parameters_separate_cb(self): self.P10WatchSeparate() def time_watch_10_parameters_shared_cb(self): self.P10WatchShared() class WatcherSuite: def setup(self): class P(param.Parameterized): x0 = param.Parameter(0) @param.depends('x0', watch=True) def foo0(self): pass self.p = P() def time_trigger(self): self.p.x0 += 1 param-2.1.1/binder/000077500000000000000000000000001463636336300140735ustar00rootroot00000000000000param-2.1.1/binder/environment.yml000066400000000000000000000001671463636336300171660ustar00rootroot00000000000000name: param channels: - pyviz - defaults dependencies: - python=3.9.7 - aiohttp=3.7.4 - panel=0.12.4 - pip param-2.1.1/binder/postBuild000066400000000000000000000000501463636336300157560ustar00rootroot00000000000000pip uninstall param --yes pip install . param-2.1.1/conda.recipe/000077500000000000000000000000001463636336300151625ustar00rootroot00000000000000param-2.1.1/conda.recipe/meta.yaml000066400000000000000000000021021463636336300167670ustar00rootroot00000000000000{% set pyproject = load_file_data('../pyproject.toml', from_recipe_dir=True) %} {% set buildsystem = pyproject['build-system'] %} {% set project = pyproject['project'] %} {% set name = project['name'] %} {% set version = VERSION %} package: name: {{ name|lower }} version: {{ version }} source: path: .. build: noarch: python script: {{ PYTHON }} -m pip install . -vv requirements: build: - python {{ project['requires-python'] }} - pip {% for dep in buildsystem['requires'] %} - {{ dep }} {% endfor %} run: - python {{ project['requires-python'] }} test: requires: {% for dep in project['optional-dependencies']['tests'] %} - {{ dep }} {% endfor %} source_files: - pyproject.toml - tests imports: - param - numbergen commands: - python -c "import param; ver = param.__version__; assert ver != '0.0.0' and ver != 'unknown'" - pytest tests about: home: {{ project['urls']['Homepage'] }} summary: {{ project['description'] }} license: {{ project['license']['text'] }} license_file: LICENSE.txt param-2.1.1/doc/000077500000000000000000000000001463636336300133755ustar00rootroot00000000000000param-2.1.1/doc/Promo.ipynb000066400000000000000000000262331463636336300155420ustar00rootroot00000000000000{ "cells": [ { "cell_type": "markdown", "id": "6f716cf0", "metadata": {}, "source": [ "" ] }, { "cell_type": "markdown", "id": "fbaa7540", "metadata": {}, "source": [ "# Introduction" ] }, { "cell_type": "markdown", "id": "cddd48c6", "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", "id": "aee0b3e3", "metadata": {}, "source": [ "# Example: A Parameterized Class" ] }, { "cell_type": "markdown", "id": "3d689222", "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", "id": "0e23597c", "metadata": {}, "source": [ "### **Pythagorean Theorem Class**" ] }, { "cell_type": "code", "execution_count": null, "id": "5e1b9718", "metadata": {}, "outputs": [], "source": [ "import param, math, time" ] }, { "cell_type": "code", "execution_count": null, "id": "d32f4629", "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", "id": "6644582b", "metadata": {}, "source": [ "### **Pythagorean Theorem Object**" ] }, { "cell_type": "markdown", "id": "90d79024", "metadata": {}, "source": [ "**Lets try to use the model**" ] }, { "cell_type": "code", "execution_count": null, "id": "dc521631", "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", "id": "f8cf6a71", "metadata": {}, "source": [ "### **Using the Parameters**" ] }, { "cell_type": "markdown", "id": "92c716bf", "metadata": {}, "source": [ "We will now **take a closer look** at what these few lines of code provide us:" ] }, { "cell_type": "markdown", "id": "3701b783", "metadata": {}, "source": [ "#### **Param Provides Parameter Validation**" ] }, { "cell_type": "code", "execution_count": null, "id": "c35d3cb2", "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, "id": "8f4f72ac", "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", "id": "504d07f4", "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", "id": "0a90d64b", "metadata": {}, "source": [ "#### **Param Provides Constant Parameters**" ] }, { "cell_type": "code", "execution_count": null, "id": "47e2e544", "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", "id": "7b79b3b7", "metadata": {}, "source": [ "#### **Param Provides Default Values**" ] }, { "cell_type": "code", "execution_count": null, "id": "1ea6f981", "metadata": {}, "outputs": [], "source": [ "print( f\"{pythagoras.param.a.name} = {pythagoras.param.a.default}\")" ] }, { "cell_type": "markdown", "id": "31978876", "metadata": {}, "source": [ "#### **Param Provides Documentation**" ] }, { "cell_type": "code", "execution_count": null, "id": "5254cd8f", "metadata": {}, "outputs": [], "source": [ "?pythagoras" ] }, { "cell_type": "code", "execution_count": null, "id": "1f403192", "metadata": {}, "outputs": [], "source": [ "# more extensive documentation\n", "help(pythagoras)" ] }, { "cell_type": "markdown", "id": "b2968d3e", "metadata": {}, "source": [ "#### **Param Provides Events**" ] }, { "cell_type": "markdown", "id": "c3b96a84", "metadata": {}, "source": [ "You can **use events to react to parameter changes.**" ] }, { "cell_type": "markdown", "id": "aeaddec1", "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, "id": "92cdbf02", "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, "id": "f0677602", "metadata": {}, "outputs": [], "source": [ "for _ in range(3):\n", " pythagoras.b += 1\n", " time.sleep(1)" ] }, { "cell_type": "markdown", "id": "881f0acb", "metadata": {}, "source": [ "We can also **stop watching** again:" ] }, { "cell_type": "code", "execution_count": null, "id": "7ff52ca7", "metadata": {}, "outputs": [], "source": [ "pythagoras.param.unwatch(watcher)" ] }, { "cell_type": "markdown", "id": "811ee5fc", "metadata": {}, "source": [ "## **Param Makes it Easy to Create GUIs**" ] }, { "cell_type": "markdown", "id": "5cd8053b", "metadata": {}, "source": [ "On top of param you can **quickly build interactive applications and graphical user interfaces.**" ] }, { "cell_type": "markdown", "id": "2fe8fe4e", "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", "id": "f5987dd3", "metadata": {}, "source": [ "Let's use **[Panel](https://panel.holoviz.org/) to illustrate how powerful this is.**" ] }, { "cell_type": "code", "execution_count": null, "id": "72a42b5c", "metadata": {}, "outputs": [], "source": [ "import panel as pn\n", "pn.extension()" ] }, { "cell_type": "code", "execution_count": null, "id": "dd27729a", "metadata": {}, "outputs": [], "source": [ "pn.Param(pythagoras)" ] }, { "cell_type": "markdown", "id": "7a0df37c", "metadata": {}, "source": [ "# Visit the Param Website" ] }, { "cell_type": "markdown", "id": "5ffe786d", "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", "id": "ef3afcd5", "metadata": {}, "source": [ "[](https://discourse.holoviz.org/)" ] } ], "metadata": { "language_info": { "name": "python", "pygments_lexer": "ipython3" } }, "nbformat": 4, "nbformat_minor": 5 } param-2.1.1/doc/_static/000077500000000000000000000000001463636336300150235ustar00rootroot00000000000000param-2.1.1/doc/_static/favicon.ico000066400000000000000000000353561463636336300171600ustar00rootroot00000000000000 h6  ¨ž00 ¨%F(  v4i‚¨øÿІ‚‚‚‚‚ƒ{0?ëÿÿÿÿÿÿÿÿÿÿÿÿÿ‰Yq”ðý¿trrrrrrj'J^:ˆC‡Œ¸w¸w»w’|P…L†mºw¸w¸w¸w¸w¸w¸w¸w¸w¸v¸w¸w¸w¸w¸w¸w¸w¸w¸wK¸w»¸w̸wj¸w¸w¸w¸w*¸w¼¸wݸwÛ¸wÛ¸wÛ¸wÛ¸wÛ¸wÛ¸wî¸wÿ¸wÿ¸wö¸wݸwÔ¸wb¸w/¸wɸwè¸wç¸wç¸wç¸wç¸wç¸wæ¸wó¸wÿ¸wÿ¸wø¸wç¸wá¸wm¸w¸w¸w)¸w(¸w(¸w(¸w(¸w(¸w'¸wP¸w»¸w˸wm¸w(¸w#¸w¸w¸wºx˜d)U>[P;^tODºx¸w¸w¸w¸w¸w¸w¸w¸w ’ ’ ’ ’ ’J ’^ ’ ’ ’ ‘.'x7,qŒ ’ ’ ’ ’ ’J ’` ’‹ ’ñ ’ý ’¼ ’d ’a ’a ’a ’a ’a ’a ’Y ’ ’> ’é ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’þ ’‡ ’ ’w ’’ ’± ’ø ’ÿ ’Ó ’• ’“ ’“ ’“ ’“ ’“ ’“ ’‹ ’9 ’ ’ ’ ’ ’v ’ ’5 ’ ’ ’ ’ ’ ’ ’ ‘ ’( @ Ž 0>)‹Œ’ÓßËsŒ,0/8¡üÿÿÿóŒ3/0000000000000%"ŸÕÙØÝøÿÿÿÿÿôÚØØØØØØØØØØØØØÙÎs fùÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿØ5^ôÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÐ0‚¾ÃÂÆíÿÿÿÿÿçÄÂÃÃÃÃÃÃÃÃÃÃÃÃÄ´ZŽuìÿÿÿÜaŒŽ ]£µ˜B•ŒŽŽ ¸w¸w¸w¸w¸t¸t¸t¸tŒŒŒŒŒ¸w¸w¸w¸w¸w¸w¸w¸w¸w¸w¸w¸w·w·w·w·wŒŒŒŒ¸w¸w¸w¸w¸w¸w¸w¸w ¸x¸w¸w¸w·u·u·u¸w¸w¸w¸w¸w¸w¸w¸w¸w¸w¸w¸w¸w¸w¸w¸w¸w¸w¸w¸w¸ww¸w¹¸wŸw¥¸wN¸w¸w¸w¸w¸w¸w¸w¶t ¸w¸w¸w¸w¸w¸w¸w¸w¸w¸w¸w¸w¸w¸w¸w¸w¸w¸w1¸w©¸wù¸wÿ¸wÿ¸wÿ¸wà¸wb¸w¸w¸w¸w·w¸w¸w¸w¸wƒ¸w̸wÒ¸wѸwѸwѸwѸwѸwѸwѸwѸwѸwѸwѸwѸwиwÛ¸wû¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wë¸wѸwѸwÒ¸w¿¸wW¸w¸w¸wZ¸wñ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸w̸w-¸w¸wb¸w÷¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÕ¸w2¸w¸w¸w£¸wâ¸wæ¸wå¸wå¸wå¸wå¸wå¸wå¸wå¸wå¸wå¸wå¸wå¸wå¸wå¸wê¸wü¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wó¸wå¸wå¸wæ¸wÙ¸wt¸w ¸w¸x¸w¸w0¸w5¸w4¸w4¸w4¸w4¸w4¸w4¸w4¸w4¸w4¸w4¸w4¸w4¸w3¸wB¸w«¸wù¸wÿ¸wÿ¸wÿ¸wà¸wk¸w3¸w4¸w5¸w(¸w¸w¸w¸w¸w¸w¸w¸w¸w¸w¸w¸w¸w¸w¸w¸w¸w¸w¸w¸w¸w¸w¸ww¸w¹¸wŸw¥¸wN¸w¸w¸w¸w¸w¸wºy¸w¸w¸w¸w ’ ’ ’ ’ ’ ’ ’ ’¸w¸w¸w¸w¸w¸w¸w¸w ¸x¸w¸w¸w¸w¸w¸w ’ ’ ’ ’ ’ ’ ’ “ ’ ’ ’¸w¸w¸w¸v¸w¸w¸w¸w¸w¸w¸w¸w ‘ ‘ ‘ ‘ ’ ’ ’ ’  ’ ’ ’ ’ ’ ’ ’¸w¸w¸w¸w¸v¸v¸v¸v ‘ ‘ ‘ ’ ’ ’ ’ ’ ’ ’ ’ ’] ’£ ’µ ’˜ ’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` $ŒŽŽŒ‘„ŽŽZ¦˜bŽŽ!µýÿÿÿþÂ9ŒŒŒ»ÿÿÿÿÿÿÿß>ŒŒŽhŒŒ‹¶ÿÿÿÿÿÿÿÿÿÒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŒŽhŒ.ÍÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÍ.ŠÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿŠ¥ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ¤wÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿw£ïüüüüûþÿÿÿÿÿÿÿÿÿÿüüüüüüüüüüüüüüüüüüüüüüüï£7UUTTSwïÿÿÿÿÿÿÿþ™STTTTTTTTTTTTTTTTTTTTUV7ŽŠˆmôÿÿÿÿÿú˜ŒŒŒŒbÇëöïÌm ŒŒ-?4ŒŒŒ¸w¸w¸w¸w¸w¸r¸t¸v¸v¸w¸w¸w¸w ¸w¸x¸w¹w¸w¸w¸w¸~¸w.¸w…¸wº¸wƸw±¸w}¸w(·w¸w¸w·w³v ¸w¸w¸w¹x¹x¹x¹x¹x¹x¹x¹x¹x¹x¹x¹x¹x¹x¹x¹x¹x¹x¹x¹x¸w¸w¸wu¸wâ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÚ¸wQ¹w¹x¹x¹x¸w¸w¸w²n ·u¸w¸w¸w¸w¸w¸w¹x¹x¹x¹x¹x¹x¹x¹x¹x¹x¹x¹x¹x¹x¹x¹x¹x¹x¹x¹x¹x¸ws¸wú¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wä¸wE¹x¹x¹x¸w¸w¸w¸w¸w¸w¸w¿x¸w>¸w±¸wÕ¸wÓ¸wÓ¸wÓ¸wÓ¸wÓ¸wÓ¸wÓ¸wÓ¸wÓ¸wÓ¸wÓ¸wÓ¸wÓ¸wÓ¸wÓ¸wÓ¸wÓ¸wÓ¸wÓ¸wÓ¸wÓ¸wÓ¸wò¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wç¸wÒ¸wÓ¸wÓ¸wÓ¸wÔ¸w±¸w>µ{¸w¸w¸w=¸wâ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wâ¸w=¸w·v¸w”¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸w“µx»w¸w¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸w¸x¸w¸wU¸wö¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wö¸wT¸w¸w¸w¸wx¸wܸwñ¸wñ¸wð¸wð¸wð¸wð¸wð¸wð¸wð¸wð¸wð¸wð¸wð¸wð¸wð¸wð¸wð¸wð¸wð¸wð¸wð¸wð¸wð¸wû¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wö¸wð¸wð¸wð¸wñ¸wñ¸wܸwx¸w¸w¸w¸w¸v¸w¸w7¸w7¸w6¸w6¸w6¸w6¸w6¸w6¸w6¸w6¸w6¸w6¸w6¸w6¸w6¸w6¸w6¸w6¸w6¸w6¸w6¸w6¸w4¸w}¸wù¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wä¸wX¸w4¸w6¸w6¸w7¸w7¸w¸w¸w¸w¸w¸w¸w¸w¸w¸w¸w¸w¸w¸w¸w¸w¸w¸w¸w¸w¸w¸w¸w¸w¸w¸w¸w¸w¸w¸w¸w¸wv¸wâ¸wÿ¸wÿ¸wÿ¸wÿ¸wÿ¸wÚ¸wQ¸v¸w¸w¸w¸w¸w¸w¸w¸w¸w¸w¶ ¸w.¸w…¸w»¸wǸw²¸w~¸w(¸w¸w¸w¸v¸v¸w¸w¸w¸w ¸w¸w¸w¸w¸w¸w¹w¸w¸w¸w¶v¸v ’ ’ ’ ’ ’ ’ ’ ’ ’ ’ ’ ’ ’ ’- ’? ’4 ’ ’ ’ ’ ’ ’ ’ ’a ’Æ ’ë ’ö ’ï ’Ì ’l ’ ’ ’ ‘ ’ ’ ’ ’ ’ ’ ’ ’ ’m ’ô ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ú ’˜ ’ ’ ’ ’ ’ ’ ’ ’ ’ ’ ’ ’ ’ ’ ’ ’ ’ ’ ’ ’ ’ ’ ’ ’ ’ ‘ ‘ ’ ’ ‘ ’ ’* ’* ’* ’* ’( ’_ ’ð ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’Œ ’) ’* ’* ’* ’* ’* ’* ’* ’* ’* ’* ’* ’* ’* ’* ’* ’* ’* ’* ’* ’* ’+ ’+ ’ • ’ ’ ’ ’ ’v ’Ñ ’é ’é ’è ’è ’è ’ô ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ú ’é ’è ’è ’è ’è ’è ’è ’è ’è ’è ’è ’è ’è ’è ’è ’è ’è ’è ’è ’è ’è ’é ’é ’Ñ ’v ’ ’ ’ ’a ’ü ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ü ’a ’ ‘ ’¡ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’¡ ’{ ’— ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’— y ’ ’D ’é ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’é ’D ’ “ “ ’@ ’š ’½ ’½ ’½ ’½ ’¼ ’Ð ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’à ’¼ ’½ ’½ ’½ ’½ ’½ ’½ ’½ ’½ ’½ ’½ ’½ ’½ ’½ ’½ ’½ ’½ ’½ ’½ ’½ ’½ ’½ ’½ ’š ’A “ ’ ’ ’ ’ “ ’ ’ ’ ’ ’ ’ ’» ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ÿ ’ß ’B ’ ’ ’ ’ ’ ’ ’ ’ ’ ’ ’ ’ ’ ’ ’ ’ ’ ’ ’ ’ ’ ’ “ ” ’ ’ ’ª ‘ ’ ’ ’ ’ ’ ’ ’ ’" ’µ ’ý ’ÿ ’ÿ ’ÿ ’þ ’ ’9 ’ ’ ’ ’ ’ ’ ’ ’ ’ ’ ’ ’ ’ ’ ’ ’ ’ ’ ’ ’ ’ ’ ’ “ “ ’ª ‘ • ’ ’ ’[ ’ ’¦ ’™ ’b ’ ’ ’ ’  “ ’ ’ ’ ’ Ž ’ ’ š ‘ÿÎçÿÿÿÿÿÿÿð ¿ÀÿÿþàÀÀ€ÀÀ ŸÀÿÿüÿ  ÿÿÿÿØ3ÿÿÿÿïïÿÿÿÿÿÿÿÿÿÿÿÿý÷ÿÿÿÿ÷ÿÿÿÿìÿßÿÿðü°ÀÀÀ€ÀÀ ŸÿÿðüÿÿÿÌÿÿÿÿçÿÿÿÿùóÿÿÿÿÿÿÿÿïçÿÿÿÿØ7ÿÿÿÿ  ÿÿÿŸÀÿÿý°ÀÀ€ÀÀÀ°ÏàÿÿùÿÿÿÿÿÎçÿÿÿparam-2.1.1/doc/_static/icon.png000066400000000000000000000122441463636336300164640ustar00rootroot00000000000000‰PNG  IHDRâæÈYÁ pHYsÕ篶tEXtSoftwarewww.inkscape.org›î<1IDATxœíÝy”\e™ðç½Õkuw:]]NX$,#‚Â茇å¸0dNÒY‰[˜9Nt1ƒY»³T<=ž™„Î dÐ\FÃh$!‹àAT@=˜ †ˆ†D³twuW¯ÕétÝï?B4`j¿U·ªîóû÷.ß“œ~ÎwëÖ­ïDDDDDDDDDDôâv€|°¹¦¦Þ.^i‰u™õ‹J($"j†DqBKKïì{}`»—Š'‹¸qlÅDŸ¯dDo†âƒ“Ìp>‘'ÍÚçæhÌ"výï"˜LDžSßôÿm¬1‹¨j½-;yˆ¼I1;ï3âYÈBäaVÌNÅžE+²†(_8ùƒ ÄŒ¨?Ö¶˜E´'³‡(_äöiM 8g[,ó "JGìNÅ,âÈiÙ.ŒK䔨ŒHÌ÷¢Ä,â²*žËN&"ÏyvÁÀ@w¬qŸ¬€ 9B¶ÇÛ·ˆ>ŸÿA€7mˆ2tb¤dð¡x;Ä-âü®®A@¾ìl&"oQÑÖ刻PXŸAõt~ À>ÇRyËÓáPäë‰vJê‹” cÆJJ£ÏrE湈ñMIöÍÑI¯â6¿«ÿµ’hÉ ö¤È »l»ô†ù]ý¯%HŠÍꪘU¼T%z“£*úŸáPäÁVÀ¤r`ÚÛµe`ÕĞb* } |¢ôôC°K!†Cƒ?hN§sGžzý*Pms‰ [!æíĶÅê¶ÔœrbŒs)0ÀL§Ï›cÝbé]02âvJLŪ´Ôl ‚ýë(Ä:^ÞÙwt0šéù r±à¯^ÿÈHÕoLt;KºDå£ {q;凂|-Û¼ãˆ(t‰Û9Ò¦x‚%¤sd`qwd ¸# >>ãvÊ/[D¨©‰´ø•Û9R…Èm󻆸ü.½IAñS¯ã”-z ‡ÝÎ’…ê¼E¡Á»„òOAšC‘¬©:ÝÎ+õ$~漩à‹ B‡ìûF@“~’!‡TËv­q;寂üú"–AÿŸÊãþÞí,oˆ¨Ê§÷ ns;å·¢˜ÏjEN nè}Ú‘ëµòþÚA1¸%¤dÕŒx®ÍAÿ-Fe3€Kr<ô(íåC_šw‘Mªh‹œ}§ú €.P•ƒ!ŸR#͋Ã/ç`,*"E]ijÖÕÔËËt‰B? ÎáÓ(v*̪Å=Ã?uøÜäž(âYíW \zªoQÁ¿:éÏ’ Å~|×R³u~ÏðÌIÞã©"ž«( 4Ô\cß•«!¸g>OÖáM7±t@D)pÐ2ò|I)öÝÙ1˜×ß[¼¯^ÿÕÖ:} KDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDŽð캦”5K–”—·¢ê‡…164ª°Ú×?dþˆMÓGÜΘXDrLÝ]?ªÌ:I׿±hs Î!6€×}ED~lÛØ×Wõó—ÐÚjr“8°ˆ”™…O–*}3ÕÒ¹PLPšÙ å£ÿ^Õä™—ù°ˆ”–Æe»«F|ú,ô¬ ¢úÄj ·5=ž•óç‘R¤X±ëÓ*r/€`NFž*Q]Z3íÕ\Œç‘’\±óJò ntaøÓ"hë©(ÿ2ZoŽº0~V±ˆ””ÀŠÝUÑÔº™CÕ–ÛúÖ5v3‡ÓXDНµÕ ß°^%nG9GHÔÌìY3ýçnq ‹H±µî+ ü·Ÿr;ÊyDÌ ·M}Âí N°ïBžÔÚjÕ lÍÓ€À£•»¦ºÄ ,"W`ø† æ¸#RU<¸{§7ÅKSú+uwíš`³Û9’¥@G©±ßݵvÆI·³¤‹3"½IݲײÖí© 1jùÆÇ¶ùÜÎ’.‘þ¢u[|ò=@+ÝŽ’†›Ç^6fÛ!Ò•ñ¥ipÅŽc•M5ƺZ`.€ÈÅ€\ ˜@†¡d؉°”] \.‚¿q;G¢€þ*§œ=­VB$h¥@Fxª ë¸e™W,szWhͬLFH»ˆµ+wO²T—˜ <“Dn*O«eÖöÞ;í™tNrßxÌé>Šâ¶1‘£Tžôù¢Í¡Õ3¥rXJE ܵ³I!ß06¥pDÞ2ÈÇSùÕHÒ7k+vÞ®'À%R裕»?ìI͈uËw}žP–v4"ïUESïš©û혰ˆõ_xüB-ÝøKÑù…|>\Z5õD¼^šÚÑÒ/ƒ%$JWжñï‰vŠ;#î~òj5Ö~ûÄQˆZ–\Ó½ºé`¬âψÆú,XB¢L•ØFCÜ"*p«³yˆ¼I€Yñ¶Ç,býŠ]àRÇyÓåÁ;¯Œµ1f˜+²“‡È›l.µ-fE0!;qˆ¼IŒÄ\ÿ5fU•OÐ9HºXÛâ]Ù‰CäMªÒk[Ì"Zâ> @D©±€ãq¶ŸÆüò‘ˆR§bÅ|e@Ì"öÞ;íu@d%‘÷¼n›|4ÖÆ¸_è‹èvçóyˆîˆ·=n-c}gVT&¢´É0Œ>o¸E ­™r"÷;ŠÈ[Dt]Ϛ銷Oâ_è«Õà÷N…"ò~g™‘„ëÄ&,b¸mrŸeÉ-úIFä>”ÌR‹I­YÓ½ºé ¨|ìQb–àÃ=mS“ÌÎ)­âV÷ùÝï‚­¿Ê ŠCÿ [“-!â’ûáUM/Û§ñ^…n0’r>¢â6¢ŠõFì÷¦RB ƒ•¾Ç®Ü9ŠE™„3¤f2QžÓ?(d;¢v{ïúGÒ9ƒ#í¨ûüîwÁ˜kÖx£z‰(®S‘A Ú¯ÀI±‡²I«Ue. ¥n'É„²ÇÉsªªO€ €Ô¨È(Œ9 –t(ÌIXÖ𪦗3ƒÓýYÝŠwCd•Û92ÐïSëªÐš)1®ÎW|-ýYØ?°ÀoÝΑ.U½§K°ˆt®Ö9§û¶B|ž@÷öØâvŽt±ˆô&á¶,u;G*èðóq|NÁÞ‹`鯄ۚà>·s$i@Eft­qÒí ™àÍŠA¥î®Ýßð ·“Ä1*Й=mÓv»$Sœ)Ñpeùí |Ýí$1 ÁÒÙÅPB€3"%¤2vÅîµ"yõ¹±Ë²df÷ê¦çÝâ‘’2vÅ΋ÈCpùEµ ¼ÑÛÎ,åR •{hÌÑ ;mc-î_;åw9/çXDJÏÂ'ËU¾Oªê=.ÊʪÏËúRß½M{³rþ<Â"RfZ·•=U3ÃRk®Bg(ËðŒG!²UÄþNÏêé¯8±°ˆä˜àŠ5¶”PI\à‚q‰ pXWTñŒÝ^7õ š£ÈyƒE¤¬Ó¼+PRaÆ‹J µ¶mFÕçD=}Õ}ÇÎ<ßJDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDj\ÛXU»º¶Êå&)7¼û‡Ö~EùØhï pT®àí ¹ÐÀ[öŒèp‚ƒ }¡$*{»Vtôj)¿xªˆ['VôºUsøG•œîe|Ï6%ßî]vâˆSÉ›è<·s¤¡Ñ¨<ävÊ/Ž|}1vãø‰¾S|2^ z D|ú 8 ubœ³T´Tw¨uò¼9ö ¯¸‚’ *ŒÇ™¿·ËȋƧGÔÖ“*¥/;ñ¬qÚElXÓ0Þ¶äNX:Šk3 BTÀ~ Èöè\ÒÙ‘Î R.b°-XcJ¬å€¶¨JgP¢"5Èz_…®KõñÆ”ŠX»fÜå>4Ÿ¾4'Ê7‡,ŸÞZz5Ù’.bݺ†÷‹ÈŽóüp–ˆÞJÑ­>™nî|.™Ý“*bíÚÆKK,ó¼ ™¥#òéñ‰^ßÕÒõZ¢=~}Ѱ¥¡Úg™'XB¢TiÀVÝÞ¸¶1á½”„E´OÉ2W9’‹Ès䣖ݒp¯x×6޵ÌkjËEä=ƒ%>¹"ÞWqgÄQŸýo` ‰2Um½#Þñ/M|ÈÑ8D¥ŠÙñ¶Ç¼4 ¬\ øŽÄÛ‡ˆ’¦"æâî–îcçÛ{FÔ’«Á9EÔøb>»ˆ–^•8D%q:³ˆj„ß9È@ÆÅÚ{FÓ—•4D%jzcm‹SDÏJ"R»S±‹hûg% ‘G‰˜˜Š{W4°¾á0 ca&¢<§GzZB—BpÞÕ*â~¡¯À³ŠÈcD¶Ç*!¨ˆ™׎QúŒÚòõx;Ä-bos×K}ØÙLD£øvxyçþx»$þ”–~@¿c¡ˆ¼¥WK£_J´SÂ"ö.;q*sØŽÄ"ò£Ð¹áÅᣉvLêÝÃ{†~_9¥ª‚&ðùS¢d¨Í=KCßNfç”JXßð1ßD^ƒMTÀN©êgÂËB[“= ¥%÷{–v}ßXx€¤V¦"ò gÕÈõ©”Èà2³~Cã,U³À(àwh9Àø©]Zz,düyïumnp ãÕÂxQ¹ Ðr@G Ò A$ÓqÞDñNH/á!Ppüÿ†§ ¿ˆŒh¹ÊüVG!8 à@©±~ر¼£3“1 òÆKpcðPÀ/ZU`Kxi×·sP~(ÈKÊPsèW |Åíè4§OßãvÊYD(©ÀJÝΑ•y}w÷…ÝBù£`‹Ø5¿kPÅš`Øí,)QÙн¬s»Û1(¿l ÜÒq@Uïb?ÕžWTŸê©é¼Ûí” öfÇY§öDø›ªB¦»%_Z6¦GG k§œ(ø"ÀðžÈ‹þ)U£LB~Þ þåéRß´Þ¥ü\HçUE€á=‘gýS«^p òé’[d/Jíé}‹ºXBŠ)þ`ÐÝÒõ-Ëèd'Ü΂3Ÿ[Û{ú;§õ,êáÏÈ(®|¼ŒËXÚ†ñ¶ß0Õ¥*zG¸%ô¸KãS)Ê"žU·!8SÔÚ èÛr4¤QѭѲòæǺs4&¢."pæÇöˆÎƒÊR²4Œ-ïªFWõ,ëy%KcP+ú"ž5±ubÅ@ÍÐ?àÜG>ËQ…n5¶<Ô·¢ó÷™Ÿ¼Ê3E<×™WÎYS¡2 ‚›üLy À‹€ìU ? ÷uþ ­\åŽ2çÉ"¾UíêÚ:_YÅ•ªö¥Ô(¬±K0Å ªœ°Uõ uañˆˆˆˆˆˆˆˆˆˆˆ²íÿ·•G¼SíMIEND®B`‚param-2.1.1/doc/_static/icon.svg000066400000000000000000000304431463636336300165000ustar00rootroot00000000000000 image/svg+xml param-2.1.1/doc/_static/logo.png000066400000000000000000000353701463636336300165010ustar00rootroot00000000000000‰PNG  IHDR4LEð@3 iCCPICC ProfileH‰•–TçÇ¿™í¶ËÒaé½wºô"½ŠÊ² K]`©bG‚ˆ("" ª‚ÑPcAD±°$(1X• ’zrÞ{çýϹçþæÎÌýîÌ7çÌR4+)) —Ê÷u¶g‡„2p °€Ä€2‹’dçííý‘ÿ©ÅQ­æ;Ú«½þ}þ¿J˜™ÂòF8‘“ÂN@¸av?°bFjÒ*«!Lã#"¼n•¹k¼z/-b9_®ñ÷e"œ žÌbñ¹s:#ÍEúkÖãqbxßEØšÍBî#ÑÖJHH\e[„Õ"þÖ‡ûžöd±¸òÚ³|‘(31>‘Ïð`:0˜¬ø˜>+5’ó¾›ÿ©„ø´?Ö[Ýr$/Àoun$¤$‚x$ø€<#$3 ©Å€¤Ê© pR#3SW0“¶ðc¸Ñ© ;d#®<¶ŽÃ@O߀Õobm™7ô/+Aô›Õ’‘ý2ÏCŠÜ¿j,E:Ÿ@]ü«¦øñÙiüôµÚêö A@@("Ók`,-pnÀ øƒ° °A4H@æÎÛÀn òÁp”JpÔƒÓà,hçÁep ܃`<ã` ¼ó`,C„ƒ(’€ä eH2€Ì kÈò€|¡(âB<( Úíò¡"¨ ª‚ ï Nè2t‚îCÐ,ôú£`2Lƒe`X6ƒí`wØÞsád8 Î÷Ã¥p5| nƒ/÷àx~/ Š„¢£äQÚ(3å… EE¡ø¨¨Þ>å>Ï|õ}·ùöùQý6û5ú-úÛûú? P H è  l\ r* Ö Þ|+D2$&¤#Zº°ÁqÃá SaÆa¹a£U7fn¼±IrSü¦ ›7³6Ÿ Ç„…7†dy±ªY ®ól&ûûÇ–SÌ™´Š,ŠœŽ²Š*ŠšáZqqg£m¢K¢çb˜1e1¯b]b+c—â¼âêâVâƒâ[ð á <^¯7Q613q(I3)7i<Ù"ùpò<ß_›¥lLéH¥!?ßþ4µ´¯Ò&Ò­ÓËÓßefœËÎäeöoÑØ²oËt–SÖ·[Ñ[Ù[{¶ÉoÛ½mb»ÝöªÐŽˆ=;wæìœÚ弫~7qwÜî³õ²‹²ßî ÚÓ•#“³+gò+篚rrù¹c{-÷V~þ:æë}†ûŽîûœÇÉ»™¯—_’ÿ±€]póýoJ¿YÙµ Ð¤ðøìÞу6ë‹„‹²Š&yj+fç¿=¼ùð£’Ê#Ä#iGÆK=J;Ž*=pôcYtÙH¹}yK…tžХcœcÃÇm7WÊTæW~8sâ^•sU[µJuÉIìÉô“Ïjkú¾5û¶¡V²6¿öS¯n¼Þ·¾·Á´¡¡Qº±° nJkš=vjð´ÃéŽfíæªzKþp&íÌóï¿=ë~¶çœÙ¹æï•¿¯h¥¶æµAm[ÚæÛ£ÛÇ;B:†:Ý:{º,»ZÐù¡î¼üùò ¢ //æ\\¹”ui¡;©{î2÷òdÏæž‡W‚¯Üíõé¸ê~õú5§kWúìú.]·º~þ†ÅΛf7Ûo™Üjë7îoýÑøÇÖ“¶Û¦·;Í»†Ö ]¶¾|Çáε»®wo¬ ½766~soæ~üýWÒ,?Üõó(ï±Ðã’'ÒOªRÿ©eÜdü„ÃDÿS¿§'Ù“/~NùùãTÎ3ʳ’i¹é†ƒ™ó³N³ƒÏ7<Ÿz‘ôby.÷á_*^ª½üþWÛ_ûçƒç§^ñ_­¼.x#ñ¦î­ÑÛžï…'‹ ‹ËKyï$ÞÕ¿7{ß÷!èÃôrÆGÜÇÒOꟺ>»~´’°²’Äâ³¾XpT¯ë „ ÞañUkžíwýÍíü^óu_d@-»ð@<Êq$”&#yÕ2úÛØÐðÏø])Q†k½È|Äš¼[Yy#® €Oü••åc++Ÿÿˆº@wòšW\qÐ'$V©Lè_>í7èÁ[¶‘ pHYs%%IR$ðiTXtXML:com.adobe.xmp 332 308 1 Ü0ɾ.vIDATxíœUÕµÿ×Þ˜¡ a1  Š{K¢±ñ©‰%öŽbM,¨ ½é0Ò†:La†)÷¿ÖG®—)÷´}Êþ-¸Ÿ{ïÜsvù®}wŸ}ö^;e#€„€@Rê€*€€€A‚†† ´Ð¸€ ¡ €„†-4®DE@R€ ¸ꪫé@ñª>PJ5åå””œL©Ù9”Ù±#åäw£H~¯‚ë]”Ü šjSºi#múç Ú6{í\0ʶ~KÔ̬›¤´4jWPH]GŒ¤ü“FQï3΢ôÜæ¦u=–Nýï©mA?7³AÚ  ŒMêÖ3’ËË/–ÿã‰Öv舔¬,óÀŸiàOæPŠH¼#AóŽýr®ÜUBïüä"*Y¼èWõ¦èß®7„ wFUG>n€ ¹AÕdšr÷rúEçÐÍ›Lžéìá}'L¢ñOsÛR¼}=ZæA‹á#_^5»>q¦’’[™P´^eŒOƒþ»>™ó Í/ÇŽ~ç'Ѧg8–ž ùÃ_èØkop#i¤ ®€ ¹‚µõDlÙL/=‰(mý`Žè:òDºèIÒz˜ÕòÈÒ´V;ôlœ›Û³{HÍÖyf‹™Ñ¾üiˆ™Yp8Þs4]ÐoÒy4ü¶;<,Á‘Y'¥¥ÑÙO=G9ùÝŽüŸ€ yì îú5 ¾æ:Kѽ\^Žüê>j´/ʃB€€Y4³Ä\8~ìÿJCnüRN<ÉäŒ šðÌKT0ñœÄO‘ à3¸)à#‡,yüQcãØúÚZ¥¥ÊêÜ™&²˜u6Bi¾È œ&Asš¨Íôv.ZH3n¸šJ7n°™Rb§÷>ël:ý¡G)³CÇÄNÀQ àc4:§öàAZðÐ_hÁÔ¿R]U•+%lÓ½|ïQáyº’>/@м ž`žåÛ¶Ò¢G¦ÒŠçŸ¦šòòÏjù0™.2ä¦ÿ A—_E)™™-ŒOA ` hpX5O¾]÷ötZûæ4ÚúÕL2;Æ––›G}Ïž@…\B=Og,Ê@µQD0M‚f™·'ÔTVÒŽùsiçÂù´ý:*ݼÉXmPSQAŽ’šmŒ‡µíS@í ¦®#N Žƒ‹0IÖ[·!wE hŠ@#÷ `šûŒ‘€€"4E ‘ €€û hî3F Š@ÐF6 ¹Ï9€("ASÙ€¸O‚æ>cä  ˆMhd à>šûŒ‘€€"4E ‘ €€û hî3F Š@ÐF6 ¹Ï9€("ASÙ€¸O‚æ>cä  ˆMhd à>šûŒ‘€€"4E ‘ €€û hî3F Ф(ÊÙ€€§TÕÒŽUTq¨Žªkë)35™rÒ“©[^eðkX8@ÐÂáGÔ"†@}}”fmÜG_lØK³ùyÕÎ2ÚSQsÄ_æç¦SQ·\:©O;Û¯åçþð¼ ìúW¡ ­X¿»‚Ÿµ…Þ\ºƒv•jíðf?/è˜E—ͧkFö Ž9i͇üG‚æ?Ÿ D& ¬))§ûf¬¥÷W–P4jòäÏHI¢Ë‡w£;Æõƒ°µÀÉOAÐüä ”ÅŠCµô_3ÖÑã³·P_fºe¹)tçéýèç'÷¤H$âV6H×4 " õ–n=@×½´„Öí®T–ùè‚öôèeEÔ57CYžÈÈš9^8ÚdŒìÆW–Ò¡:÷zeÍU³©½rõq4¤{^s‡àyY›'ð·¯6Ó]ï¬6¢ƒgd§%Ó³?B§vt0U$åš‘†ÏÌ-¦[ÞX©$¯Ö2ÉLM¢7®F#{µkíP|®V („¬¬˜±ª„n}Ób&µ8XSO—ýc!­ÝUa½R8Óq4Ç‘"A§ ï;Hÿþê2G§d8QFY}pÍ ‹©ª¦Î‰ä† h@DîˆòIJë_ZJ¥kÝËÄFÊ+w”Óo>Xc#œê$š“4‘–㞟¿•ænÙïxºN&øσ[ÂÓH`ÞÀMHoà³u{èÝå;ùKTJ[öVRY5.'b)}%ž–Áÿƒ1™U¦8?é¶ / ïÙ>‹FôÌ£Iƒ;kMƒÁCiSù>3Úw(>[»‡/¾¡eÛʾ‡ƒÞpG"\¬“¬»ry%AQ~º÷ìþ4¶°ƒ‹ nÒÚ š,™™òÞjúÛW[‚ëÅЖMÓ8Z*ãS£hÒfÜñÈÖªuÄÚXØS –†?^Ë"õ Œ 5lèÂ÷7]Ö3ì)ÐrÛ„ Åð‘/v}Šâ‹—êz=vª+37Ä »>™ó Í/­˜ÀRÞïò´‡goÿà[ë˜FónEy™©¾-£.Óz M'¹žÇv˥ɧôöuþpÞ13Ÿx‚æG Í˜rV! ÷érµ«Gt§ ŽíÒ|áñ‰R4¥¸‘™©ÉIôÔåC¨{Û +§»vÎ ½ÛÒïÎÁ´ ×[H‚fNQO [^M»vµÏòÇ8Õà®mèå«£ŒT„¢RߚςÖ<|â3…²éÝŸð¼§6¢g[š~Ý0ÊÍð‡¸úÌMžw9=ÅÌ­Ø~ Š.f-æ; ªíü¢ÎôÈ¥E虩Ÿ`~´Aá0¨©«§û>\K±IIÁ2S“Œñ²«FôP’2±F‚fÎò y[öÓ]o¯¦…ߺ·“פAGÑ}ûS/D:ö‰×›/­y6ø$ d…Ç[ËvÒÔ™iÑ·Î]†Êâï_ŽíC'ôn(& m TænÞOÓ–l§·—í e‡L×Mn<ȼ²‹‡t¥~ü,´`ù ¥5A`MI9ÍÞ´Ví(§ »+i{Y5UT×Rum=eòt‹6)ÆÓ¾²8‚q.اÉôXp @Ђë;”@ Žæ¡ÅÁ[à€ ×w(9€@Z¼.Zp}‡’ƒÄ€ ÅÁ[à€ ×w(9€@Z¼.Zp}‡’ƒÄ€ ÅÁ[à€ ×w(9€@Z¼.Zp}‡’ƒÄ€ ÅÁ[à€ ×w(9€@Z¼.Zp}‡’ƒÄ€ ÅÁ[à€ ×w(9€@Z¼.”à%—íÛJ*÷Siu%UÖTQr$‰²Ó2¨CF.åe`Ç"´ý@Ðäó]•¥ôɦE4{ë*Z¸cmÜ¿Ö6½U[»Œ*lßFæ » ¤Ñ=‹(#%-@µEQAÀ<ìúdž™Ò3jëëhúšYôâŠOèËâT­·”NZ&M(N×{ Ïïo) œ~'Aó©‡DÈž^ú=4o:m+ßãh)‡wíOwôc£×æhÂH <&AóØMeÿeñrºãÓ'é›=ÅM}ìØß&Œ N½–òÛtp,M$^€ yI?.Zúý¬—iêü·(ÊÿTX^z6=xÆtNá *²C à*š«xO¼´ª‚®xûcÀ?ñ³œ;òÃϧ)'_N‘HĹD‘(&AS ¼©ì¶óÙEÓî£5{¿mêce»dÀhzø¬É”œ”¬,OdNÀÄZ'iZHkïÁ2_ˆ™ýµÕ3é—?f¡8üA‚桪kkèÇÓçyÏ,ÁK+?¥û¿z1öOx !AóÐUwÏ|†'È®õ°MgýàÜ7è£ šþ€ yäœ×ϧ'—|èQî­g;yÆT’• 0šÞ:X[Mw~ú„9'žå¾ªrºç‹ç?G‚€à.gŒd±÷g[>§w×¾Gó¶Ï§-¥›©ìPyÌN½ìÅÓ#z9•˜kéÈ\¸ht1E¨Ìµ—ù ýÄ‚c~>ôzºoÌo1g0ލւ&bvÅ[WÒG?ŽÃâæÛãXrÜÌÀñ´£ÑÍœ¦<`~"pfŸ3èùóž…¨Å8Eë›Ò3S)fQÊ œ˜5´•Î1M/ýB@Ú®´aØaÚ šŒ™©»Ìl¡¶‡ÉèU$’Á¥•Ìo¤ K[†5ÐRÐän¦ÜPoyê³t,Ç`бcÕ÷qBÒ–¥MÈ´4™šáöÝ̦W°ÆÎ~XìQðCþy'mYÚ4LSA“yfÊ-!2.Ý”çìP†™¥ƒdÜ àI›v£"6ÓÔ²‡&“f•[$'¨²¨ÖÒ[r îI›ö!X-MV(·hÐcŒ½üÊ=®4COÚ´Ò&–™–‚VQS™' x$؈žÃ­N¶WÓò¤M»Z#k‰k)hÙ©YÖhÙ8+JÖ¶Ÿ³‘¥£§F©ÎÑô˜³¼hÓÎÖÀ™Ô´4Yì«Ü¢µÊ³t6Cš³DÛË÷ÚI&ásGuDž}3uÍéð98üJ‚æCϨ® ?~ý=±øª­w'(dÇÌ\úÕÉ?¥ËŸFI|#a Aó±×íÛFSçM§×VϤCuÎD»è”•G×™À³©Mºú½|ŒE ZœXR±Ÿ¦­þ‚Þøæ+Z´sé§%§Ð¸ÞCé‚þ'ÓÄ‚‘”ž’j: œA A ‚—bʸçàúzë*Z¸c-­ß·6—–P)_¢VÖTQrRe§fP{¾œìÛ¶ ¶ëF#º  ã»RFJZL*x á$A §_Q+Ð’Fƒµt;* á$A §_Q+Ð’MK·£Ò N´púµ- @дt;* á$A §_Q+Ð’MK·£Ò N´púµ- @дt;* á$A §_Q+Ð’MK·£Ò N´púµ- @дt;* á$A §_Q+Ð’MK·£Ò N´púµ- @дt;* á$A §_Q+Ð’MK·£Ò N´púµ- @дt;* á$A §_Q+Ð’MK·£Ò N´púµ- @дt;* á$A §_Q+Ð’MK·£Ò N´púµ- @дt;* á$A §_Q+Ð’MK·£Ò N´púµ- @дt;* á$A §_Q+Ð’MK·£Ò N´púµ- @дt;* á$A §_Q+Ð’MK·£Ò N´púµ- @дt;* á$A §_Q+Ð’MK·£Ò N´púµ- @дt;* á$A §_Q+Ð’MK·£Ò N´púµ- @дt;* á$A §_Q+Ð’MK·£Ò N´púµ- @дt;* á$A §_Q+Ð’MK·£Ò N´púµ- @дt;* á$A §_Q+Ð’MK·£Ò N´púµ- @дt;* á$¢ªZÑh”ÕE©¦®þ»G”Ò’“(;=™Rù v Ø4§â}i³<ö¤-ß=ï®8DeUµTV]Kä™UµõÍ–559BÙi)”Ãâ–&:ªMÝ)›Ž>*‡òœM¹©Í¦@@ Â=§h"^Y¸E«òâµµ´Š;;‘Z?¦K›tØ%‡ÎЉ& :Šº·Ílý$   „­ý3|¥(¿ Mx”ñ(ÊÏõ]ùP µ-h±¨†÷Ì£)gÒ)bÿŒ×  5últA{ú Ûðžmÿ„gM„îöâÌõ{é¬GæÐUÏ/6nJhâGT@€ „NнúÎò4á±¹´o\À@ô ZA÷­Ø^Fgþïׯ³îD-A@o¡4qí¶ÕÜS›C Š÷ëíiÔ4 zA–U×ÑM¯.§ê&÷jàkTBO@ A/®ÝUAúd}èŠ ‚€Î´4qòÿ|¶‘Vî(ÓÙߨ;„š€V‚V[¥ÛÞ\j‡¢r  3[‹Ó7¤[.]:4ŸÚe¥R^F e¤&SzJ’ñ¨JzhN`+äØhÞ8’&¿ºŒÞXºÃV’s·ì§’²j޹–n+‰'wO%&œÄ‚+ÞWe<—”WSEÊ9\O©8Tk·ÌåËmyÈew^fÃåwçÜtÖ£-ìÕ–zwð÷ep-×wÞJú†‡„ŸÄ»«à!ƒÆzʳñ” mÒSŒ!†îm3Œ0O=ø¹/×/Åá`žR)ÓŽÇW¼¿Áßý<Œ!ì¥Lå\F)g:çmðÏ0yf? à˜{#˜ÿq=ò(‹ãñùÑdÊÑV®_1×MÚ›Ôq/ÑÈ\ÉHý_ËpŽ ï´ãºÏüz`—6tRŸvsÐþWþ@U _9í§Åß–RIù!£-H ÄH$B8¯öYiÔ>[žS©3ÇNä|U…ú²_;…ÞG=|É`ú†EDVX5‰áöéÚ=tÙqù 'QÏ7–l;@sؑ˶ eÛÊø‹]njŒ¯qŒ0>S¹¤딓f|±Nïß‘.ã1GkôÊDfóåþü-¥ôÍÎr£®ëwW’ÜX±jÈSvJL»{·£38®]·¼Œ„“ÛÃ_š/7ì¥%[ø/cÈ*Q«¬ç/=ùw°6eÉI*êÚ†FönKW ëNƒøµjáÞ˹}K;“çµòb¢žÍ•9…ë7Œ£ÒŒí×.Ò•úvÌnîÐ#þ¾ˆÅëõÅÛ þR&³qp€ÖÓûw¢qܶÅ÷iü]vÔDÛ¸jDwúë…ƒ+ÿRnÐc§Î¶•Þ”3 éÖÓú¶šÆnnH·¼¹‚¾àEï}W•uÌN£NêIמ؃eÓ\Ï¶Š¿è³6î3ì—\×E̸Άx%ZàñÇt¢¯:®Åß™[LO°èÛùk1ƒf>wtGºyLoWCRÉZc'–ù±\Åb¦‚;k]Ä¢vÛ©}™¦H{}ñ6zfî·ÆxSÇXù[Ïv™tßÄþtÎàÎVNoñœ@õÐkr,¥ æ_Où¥°jrY˜ˆe¤&Ñ{|AµIþw¯£yîÜíãúÒ/Æô1ºôN–c?_²ÌX½‹Þ_YBÿúf·Ñ{q2ýDÒ’ë­Ùîª3)Ó¿Öì6Çó¥èc—Q‰Mku’Ï/~r>}ÂW ^˜üV½¶h»ÑëºrxwºR#ô½”E†Rûj3ý÷?×—ëN—O†g$Î)êë÷ç 0.‡ÊÃ~ŸS¥k!smªû®»ð2æ`沨…"[úH.‘~ûáZ£Èe S6sÝ*¼ÿSº‘Ç$%2‰äã… éž×j¶2~ê¥-(.¥qM±ø;iò£åµÉ¥£ôÀÆ>4Û“vqʃ³è7ï¯qEÌbë+W=c8ßé6ÇÄcÓ ¬ õjoo?D{hK6hñÚdª‰DÙÄàN˜ «¸´i­¬C¸s-ãn^›\~ýä™…ÜcÞàuQ\É_ÆGOçöuþói Q«2iƒ×¿¼”Þ^fïF_cy+hr÷ÄŽí*Kü×ÑëBc=WóàðåÏ."ï ƒñM1’áƒÖLvÿòƒIoFz˯ò†Aa4C¦MbQ»î¥¥<´c:U`Íî]’–¶Õ‹§î‡Bc™dÐxÊ{ß4¾ ôs!I%2 -ßþ—;À~±Û§¯¤ »ÕõbüRo7Ë!wÏo|eílæt¢yVÐìVÜÌÄ/=„F§>õu1…arðî­÷Îëì—^²”GæµIBÏaή÷qØŽVÐdB¥3%h>C‹¯ë½®‰ÿSàÞMà†@c¥úûÌ‹yZË[û4ÖÏD/.ØJ2çͪVоڰÏjóŽÊI| NVÈ s?™ â~±Þ›[þNq0×CóþÆ@|½ÿ1çÛø?á½ìôÒ)h²”Èî-ôNmÌÉø­‡ íæ™¡dbgQ×Ä/9ýp§9þ»:›'"¯áÕ"0g Ìäê½§´ø«Û‘ —¿}¹…Ì ê7•lg=49¿°SÍãe@M™¬Y“Gú9^Çó^Ö VóXKßö—q¿¥¼\gåŽrG¿Ãw…dr¬ šÍtÎ1µn²%A“%q²²Bø7ú¡á¹Á²äªœ×u–òúN™’ +Md¯ 'ì…ù[éÞ ýHÊVi\G¹ÁRÊíLåt¹r‘5½f—BµTY¹Û*¾r|·–kò³À š,ˆ}àŸëš¬Œ™?þÈÄ€´¤Ûø…žVØŽçŒÍocô2Ì ŠL¶d;Mý|“±°ÚL¹c•Xq ™Çi¼DG¥uæÞí„GñZÇ\ž=ŸeÜl“4C¨e|SÄ[%<=fÇ*ò2c£ÙßAlH·Ö'ÔÆÖI&7gñÚV™\ÄÜÇtà)Ì??—äni’tùL˜¬}rözaÁV[_FY¶ä…ýéüÌ?ÇXô݉żñ®¿LéYÁQ™—l-ãu¸ûée‡§˜ˆ~:¬Ò·= í‘k¬.+&YÛ,?²|îm¢ÙÈêœÐ Ú Ž‰v#ovbg´4¾.<&6Œ—³˜1Ys(*%r@ªÍˆòKzÕˆÆá›__AoÚ˜)-=>Õ‚vö1GÑŸ/Ô$>é-‰è÷kbî˜,ð_Í—hs9Ȧ¡“H\:Øø!qb冄¢zèâÁtáºÒÕ/,¶¼NW–ßI½Ì ª™º7uìÄAG‘,â?“€ÇsôyüÛ =ŒÅà7ñj»RG±€]Ïk‹å‡LñÇZvZ À ÎåqÃɽèï¼lê®wWÛú¡øtín’è.f£³¢‡&¿¦²¶ì‡~m&pc/ˆ“¹hNÏG“†ð¿=Dê'!p¬˜œ“/½„±‘‡;·¨‹•ÓZLä=Ž:²—C‹;aÇ0{ž]¾$z^fŠñCÚ’[BVoå ¹÷VÒ¢âRË1Ü(+Ò°O@‰ Ù/¦ºælæ/xù’“P@ןؓNâ0E¡‘[KoǸø»ÏZ;L‹Ïíþ ÈdÎ_ÙÏÉdfvü-o¬ gú1ÓÂQ>¬$-Î)¯ÚŒŠû‹1}èîñ…ŽÞm+b¨ßаÆË˜¬ZvôÊÕǽb«ià¼à€ Åùn-/n¶j·ŸÖ—þß™…VOÇyL`Ãnë½c«ýø¦HöQ…éIÀúº„òÚÎ;Y1YâñF÷¶r*Ή!`'úëÕ#{@ÌbXêø‚çu+Aå$ èæT’¸b†ö­UþD&XÃô&A‹ñ¿lDkuî™ÜAƒÙ'`µ‡,9Ëd˜Þ h1þOôŽdÌ)ß¿\µ»/}ÃÆ‹t{ž®âÕ0½ @Ðbü/xeî˜ÛSQÃñÑZ9çÄèdc@_vƒéM‚ç;_¨iKvÄ¥†·f Å`­šD¯•è0} @Ðâ|oç uïkèµEX‡ÔÔÛN9Ö§\Èž?µ€Ê°Ç‚)æa:‚çÍ£yý¨“pC·s”޹›÷ÛIFÛs mò—ËγC²w¿C˧´uF+ŽÛBqN›ÄëNŸ›·5¿•èªO}]lŸ-d‡š pù°îôâUC)‹—¥Á@ 5´VÉF./\9”~söÑÆtŽVÇÇ.8£'úhòHÓ¯½ ©#É0€ %àM¹ä‘ÐÚ‹ÿs4ÝÌAýpT¶÷;šc°]2´+Oܵ¾þ1êûâ]ÚÐ›× §7®fleè‡BÉ|EÙ€eü1üP” `b­‰fЖC<ß3¡?Ýrj_úø›]ôþŠúךݼv°ÎD*æ•/ÎÀ.9$;É{¯>9éú¹o,ï¢5¶ðDZÌ+ ÞçÝ¡>Xµ‹Vlw7t“üxôiŸeø@ø‹/Šòs©O/ù‹@$Êæ¯"«4²‡¤Ìj_SRAëx·(Ù1Jââïã`‘²aqs#S“#FO/ƒÇ†ÒyÎ[;KYöÓ­-?ø¹»ñœI=yù”¼‡5O@Â6-á5œk™½<IJ5]9ó—¥M²-ÞD¤¤§ž’l,‡’ÐQ]óÒc|ù2¨oÇ,å{ÃÆ—ï#AKŒ“å£êùËTQSGUüHMâ/¯%”/RD¾Q0%dÍlGà]kä/+B`á#A ŸOQ#Ж~¦´u=*á#A ŸOQ#ЖM[×£â >´ðù5m @дu=*á#A ŸOQ#ЖÀÿ0ÙàšÁ© IEND®B`‚param-2.1.1/doc/_static/logo_horizontal.png000066400000000000000000000215441463636336300207500ustar00rootroot00000000000000‰PNG  IHDRăuk pHYsÕ篶tEXtSoftwarewww.inkscape.org›î< IDATxœíÝ{|œu•øñÏyf’´Is™Iš–¶+-÷«U@äæb‹½./V´UÄU÷çï' k±H ìê¨ mz¡+UX]W]WÊòC¬„#»ˆÜ±µ —°¥m’IÒæ>ó=¿?R ¡i›äyfžIrÞ¯W Ëùžt:sæyžï÷{ÀcŒ1ÆcŒ1ÆcÌ~vY[QxTžÓéN¼"Mæõæ½zUkk2ìÜŒ1ÆŒN9QoÆø¼®¢;ÕKEäB”ŠCæ¥Ýg¾ÜÒµ=ˆ¼Œ1ÆŒM^ƒ®+/ªÕø.†rž‹DžüV¬øÿ±Œ1ÆŒUY/ˆß*/Z®PD [)ž{x]|ÂÂccŒC²zÊô[ñ¢¥"¬Éà="2gqã¾_gp cŒ1£PÖ âºXñùê¹_Ñ õ–‹ð%»ÛwexcŒ1£È ¢‚|«|üÙžzçª0IT/À“}8ÝŽH÷`Q4"Èe@Üwƃó"PŸ¥±B§¨ˆÊd`²ˆ4¢î9ð¶ækÞCW$“­açgŒ1#Áa âºø„…*î㲕 T· wööFKÛÚšÃNÆcrÙ€1^¬¼p We9“úŠùëÅM{·†‰1Æäªg™ÆÊ‹–[1MäXÅÕ}w҄ʰ31Ƙ\uPA¼­|üÙׇ‘ŒÉ¨£{SzkØIcL®:øQ½¯’#{œšÀ]ö‰%v=ØcЯ ÖTT#\V2&ã¼”¦ÿ&ì$Œ1&õ+ˆÚqPR.& DyØ9cL.êW#*ÙZ'hÂ"Cî*bŒ1cB¿‚è§*¶=þ; »D`ŒܤÅÛÊŠfJDÎwÂdTÏtŸ¨lz9N9Â4K[ táô_33fU˜LAhžVÕ—Ætl¼baåT¶¤¡L º·¥aå0{@ˆŽ÷ó=‰YûÂNÈެíYz[Å„»Pý?ÙKD¯\ÜØñ½lŒe­ø†MåQ—Ik[ÛûÈ÷[WÌùUØÉc²+kqmYYYÔK=£¢Çdt ¡vqcûñ¿†Ýø4 â;}aeó¸'îµSªÆŒ öCÌ„kZZZPý ™Áa~í^nÅÐø¥ÈYª²>ÖyÎSñêúóÂÎÇ“yY+ˆ‹“íÏ;t°3áŸÑÙWµ¶f²àš±ç Eÿ'VU÷Ýi׬v2ƘÌÉjAXÒÔñ”ˆž<XPáž‚‚ö 7vì,¦1ïòþ¾=¿ä™xu͸6f”ÊzAXÜØ±#5¾ýBT¿ìõê …/,nlÿT˜³)͘q²ÂoÊ«êÎ;cLðB)ˆKߤóêæŽo¸Ç ºhÂÓߨNo?ñ+Míÿn× M•:¡®tYý™a'bŒ VÖf™É¿MgÜÞ}……“Y§3€b4ЦÈK¢î94²©)¹÷· Ûo5çäY¦G¢°+ჷÌÍÄõpcLr¦ šÑg4Äý’¯¶}Œ{¥ÃNÄã_h§LfÅŽ)ÎÊfƘ̳‚hŒ‚|#žx°$ì<Œ1þYA4Æ…Jíˆ,;cŒv ÑdL€×‡êz„fšQÚD$íÄëtª]žÈ8OÝø4ZŒ“ 3<˜¡"3P= ]T ^OŽ|†mñfÌÈ ;cŽHy<¹rÞòa=7Ñ-íê™Qw¡â}ôô`“à}ñάê3Û“%VÍè–˜•j…g€g@ׯ—Õ_ª°åè ‡qx ±‚ḧfÑŒ!¢Í+¸wb¢¡>ÕÙóÐ…EÆÍ *Vñµ*¼HÁtA§‰¸IŠL(Bµì=£&Ò »=Ø©ŽyyÑ—vßrÑ® rÉ• 7ÔV)O)EoŸpâu ©d$¥Í{Š IÌJe:›é‰†q{»{¦«sG;d’‡TªjÂTóÞ~œˆt¨ÒH‹*;=OwyN_k,÷J6ò R<ñ`IªË«Œ:W¡â•‹h¡*¯oÒ˜j ¢êÒºÛÝ/Ê{c[bVF{ÎVÞ¸iRJ{!íMu¢…žÈ8 œK;š#"oßûÚŽÄÅìTf×MÆv Qù^råÜ+Hé]‰„WÖùá»ùdP!5•žÞ²fÁöÁ¿>¿¬k© 3QÞ/"Ç㘎0Ÿ×<v;Øúˆj¤®µð±gsåúféuõ3"Qwê½_Ñc€c¦ Ldp“üÒ(;¶#º]¼ˆ§/hZ·´=ùÚPβk8šˆ7d¦ÀÉðÎkpÔк~º~¯ð¤B}Þø‚‡Ãn>=%±¡°»+zjZådN@9ä} S€J`Ü0¾ ò8¸GS©Ô½{×\<ü÷{"á•vœw†‡ÎAô|à,úþ]‘Â.§ÇÄÓ†æåsòfVMÆätA¤ï±vz¿¦OE/mY1ï¾î+¿iÃT—ŠÎR• Eøp 7Ðc3`ðC'Ñï·®¸èÕ, ‰†hyW×U½ UôB”sò ޏxxTÅýWËŠùÏxç”ĆÂöŽè¹ñ>ªèùÀ€Xó9P½«yü3ýeâõLNyÑ3QýÂLàx ’Áa{‡@¾“ÿÛúÁþŒåËj?¤*—+| ˜L*úªª÷£ühä{C9cbÑdL®D€ø²ú/©êAÄäúæš9«ß{{lYíOP¹<ˆ1|r ?ui¾ÚºzÎk™ vcýi8]€r!pPœ‰qŽD`wsÍÜIïäU]?ôç@~ù¼ÇïUõÆ–•ó~H´…ë#±£‹O%âœî<Ì6b?²? $škæÜ3à‘ÚÂõ‘ø±¥ŸtªK2¸Y¾tŠpWOŠ›÷­ž³ûH¶uˆfL+ìný1ÐD,…ã¾Gr¥G§úY/¢[cUµ7°p½ÿ£……ë#±ëë>RVU·:V]÷2i}e0Š!€öM¤z‡v“ÅàT¹¿¬ºncùM¦ú ?¦äDd3èí ŸÍb}aîŽUÕÿ&vcýiÞ»¡v~ì˜ÒçTõg™-†:^U¯Î‹èbÕµŸ=Ò£­ š1í͵‹:Qy$`¢¸)[‰œ|Dn‰[úpñ ›|ÂŒSzÿ-µÀ±åç›Oøçq)¶’c]q.r©¼-ñêÚ9aç’1¹¤õ™Xuý••7n𝮻'dhùÓᔃü8V]w'‰†CN&µY¦›p]}eÔã<NR\¥(E"Ò¡°G„=ǬCBŽ}˜ï;Ž<Æ©nõ$¯N¨^ÕÔ#UÍn\9{X͵ÎDŒŒsNûÄ]«ç´ÇªëÞÞRJ‡WäñeµŸm^1ïž°“É|ÐÛ{Óé5Àøsù¿±Žž©ÉÄú“XÔóÞ;­ `b¢aBogÏßy¸Ï)z&û¯Í 4kT…tbÕu›Eø±ë*ø~Ë¿Ìj /s è. àrºBÑ@·GóR[]*[óg†ìÔ´¸±êç%k>6”ž¤9-•§¾U·öͪÌ9ùªòŸ±êºŽdÍÜÂN&s4ìbØGt~¬£ä‡Iôò÷^ß´S¦~$¢ñêÚ¥©Îîm‚Þ¦ÈY n¢ÒLUVKA÷öXUýM,~° Ó©šCS‘`Ö0éÀß~›n¾øÏ@[cdÈ)¨û)hÆËÎÎÂòÇ0’¤ðUµ'†È˜ \VVUwÐÄV‡©dYÝqñÎîÇYÃð§’— úϱñÞÓ¥Ë6åÂ…ð1Iqd7ä8¢‡\¤¬äÜuÄþDçÇ«ëGG++a€£CPäÅl§2D¥iñ~Èd'sD"²¼dY]¿‰pV‡!V]{ADyJáÌ@ §yšz¢lYíG‰g†D•Ê@âˆwÈÙªžh. °ªøÚ açá—jÿ¦ïÜ..ç_ÐsâÇ–~1ì,ƈˆ²êÀ¬ Q솇Ωʎøà¡©•‡úÖK™, æ4•;ôò U/·û”æEó¿v~‰×BÍÛòÒn$¼(z#_z:g/:2—”^_wÆÛ°‚8¥×ÕÏÀ¹ @a††È½'^]wj†â›÷ê;=õ‘ B©pÈÙÚ{K/¤è—§$6dêßwVäIÞ€Gˆ{V-x È•5¡‡¦+kZvc„xÂuoÿÁ â`-\ñ"úŸd~«§"…»I¬Ï•EÄ£Zì¸â9@<ˆX/ê¾HddD ¬«#ßÿ”ð¼~Ø­ºFÈà3a'0f/[ÒPƒXvQV]7S”Ëð8OU¦ ú@/J—ª¹µØ5c„ doßÃSc%»©ªÔÌDòEµP‘|Ï£Y• /!ü"9nï½­·1*hý² ¢‰¸Wu_SSù+±Xc/‡ß¿TÀ6”í {A’"Ò«¢ûö?¢´Xð¦©èA·±PÑ…À7ðèðû…­ ç!LÈvD·£ìhW¥´Oœ:'â&ˆ0QUNN#è]pD/*[ÒPf˲²bù=—ÿ~È‚«ÞX éÛË¥ï·>y… qÛ 5C„R tpD@•£€£@Î@ùt¬³äWU{EëÊyg0Û©¬ªn1*œ.ðÄm>äwžÕKuÝËôuT€¾ ·Ÿáyœlñ"©-íÛ†úå¥òÆM“Réôß*z‡Ü:nÈfõ-Áz·€á‘NAÿ€è ªÞVEw²G%ý–’ß*ô”ŠJxÅôuÈ88 ø ï¹–¯ |ýðûEÿxÀê’.àI„-¢lesº§à¥!¡ÄúüXgéD¿„²€`öˆÎ—¼ž €_눞•ûÞô<ÙéHí¡W›½bIæí‹¤»$=™HähQ÷ׂ·±+S0^}PÄûçÜï"Î{³#/2.?­•ê¥OQåRàš ÂEª VÞ¸iRo:ÕÀ»o^3rë‰Ô•U×}±¥fîÂN&W”UÕý­k ¹£qù‚—÷Aîuª=xî—-+æý.ˆ‚³ÿôàjëo+ë,^!Èü O¬¨ª;¡q%YY¦à©wfÓÊ c¨Jù ˜vz¶ÀÙ gG„'û xJà.UýeaajS }ô‹z’°ØÐ· ›üH `æ²çÎ'KQ !¹rÎ͇yÈ+ûýjJbÃ?vuå/WÕÅ× "-ð#ñ䎦åsžàþv  ø#poéuõ3¼ˆþøh@ãÏ‚N™.\éMõÞ‹ˆÃÑ#*ð¯ñêú—›kæ<v2ašxý“{½è? ú¿ ¶Û˯ô€æš9ÿàxý%õ´ÀÒXuýnÐå~Ã¥=΀ìÄámZÎVúÖxêË^ËŠy¿fÿk•‰s‘Í5óê+ªjÿ2-ò8>g¢+̲®€íÿñ•ø²Ú¤ª|-ˆ˜ž'碨uõœ×øÒÓ³cñ¦¨^è?Z|톊ƒ&ÕÄg‘óý`rLž¢·“HŒ½‰T‰†hÙ²ÚÆªê~–ò"Ûý"·>¸?ÈxÕ¬™]ƒø?ª•“‚Èg,j\9ïE|or JN¿Í+æ~]Ð_˹aLl»ó¬Þ^I/$ ï6yùÑãúpT‘ƒ¶³1£ÆÌ²ö³»f–‹Ê–4”•]ÿÐʪë.‰UÕß«®«‹uv'E¥áSd¦Pcs‡Ë‰‚¢húFÀWZEO(¡1©yÅÜ{•ß¾="aZn/uN¿L,÷—ÃyÖ¾åó÷ |' Òr\¿S¦}]½™Dp“›D¼y@0펲EøD¼ºîCÝ¥}=þJAòRè~·_–æ„ü„uó»³2Ø $k<«®{Œ¾½Ã#ÞQÁe46yªwªˆŸ~ÒÞ5~2ðjP9­eÕüGbÕu›ÁoÝ÷™y^t]o:u>—zÇ÷+ˆ®'ï[™8ÊIN4ª‰ }w¨SÛ=õVùaÙ%¢«Êð ¢ê¤#?ÈŽK»Mõ·-iÄõT’Ãq¿z|Da.í¾å¢]û‹ò°‹*€ªö?eêE°® £äH –QB„[‡ÛK0³<¿ûv½5á˜Ó²fÁvÀß,VÏËù×!ˆëˆ ïó7¿Aý/+™Ú/—ƒol(•?‡Âh!ð§È¸‚•aç14ìñ¾£ÑÏ““œlÀ| OŸ LAYË…%ú<ç;¥¨ß)Óæ‚'·Æ:Ïy ˜ì;¸ÉQî×ag0Jt9ç-jNÌÚ—©Ê®}àhÉÏ›&.=¡•2D& No€SÝåyºÛõ¸-{'íàγz"â:ÕW{ÃÜÿ ΆXõÆRçÒÇF#ÄÓNK™ ªð¤ÕOÕÑ®è/âvyéÞ7W^²÷€¾Ž=/÷¿˜ì[>O¬º®Ÿ å£ù½… sƨ ÛØ4­A$‘pZ]÷S¥~#›œÔ,ãõÁ°“T•«ZVÍþ] Ѿôt^¼|ÏY8ÎuÈ©ÀiÒ·)F1Ρï¼×UÙÿß¾«§ê‰FˆÅÓT×ížWç÷QÇÜl‚ÒëêgHD?Ìô4TN†ô_x8‘ý_0äí×CPU·_´«®K*¼,Âè°{¥ ª#¥7â6úv¶^M ûrާ²-í{L8ha~:Õ»<Íûmxlr‡Â7’‰ù¹Ü¹}$P„«Zjæþ›Ÿ Ó ãÚºz.qª …Æyê¤|OŠÓöÿ2ƒPZõÐY‚û´Ÿþî=âçň |eÀ™Ñ£‘ÂNñY½¼È° bcácoÅ:ÏÙÿmq¸´è ‚¸wÍÅåËê>å”9üFÄfD‘{Zjfßv#\·ˆ\Õ¼bΆ äšºx4_®ií쾘h[‡A%^õÐ'Tô:p;›Ñ@„¿g,½”Íe"ᨮï_“.ˆM+æn*«ª›#ÂÝvº»¸=™,¿&{›5JÛœz [kfvóèCJ$¼²Îs |UQ;û’òªº³ÓR¿NÁÏú@ó^ŽN¿§7Òê|΀×NÀOŒ¼Cv»hY9·¡lIà ^AϵŠû ŒÈõkcYð¨«i^9ÿñ°“Áz¾ãº ¾Þ:ÌVp´tE~L@ˆÍ0,\‰SòU7Ê ÚÞ™¡¡Óï·mO">w‘’N|¦öÆþ6(ÿüSÅuGõtò‘|9Fìô<íõ5òH#òWª. mŠ廂üd(OðT*z½Ô>MGßhëJ½žK»§Œ@NûE½›†×‰¡OYuÝLZúÚr™LO4Œkëìþ…KÃÎe´RÈzà¿& ú›Rã-swÒ×¼Ôßþ|#–þ6VU7'+Ÿ [’ãÚ–XcßP´£ú‘ˆ[s¤vNG¯®;Uû:+äüâêQ+ÑmíìþÀ¼°S1¹ÏN š¨¦¸\¢‘gÀßTê#Øë¹È§­fU*›€Ÿæ;~¾kõÜv¿'\W_©¢ V Ãëì¾ +†f¬ AËšÛã7Ô^¬N6`·æôˆxŸlªþ):3(½À ¨§ó{½Gv­žã»(?Â÷T9:ȘfhbÕõƒ^vfä°‚8DÍËçý¶¬ºî|_ï 0t“* “5³Œ9–9íÛ¾ìu×P^ôOi•çZ‹Ú^ÈäxÙ²Ú«êÇ3ßٔĆÂÎNý^Øy˜‘Å â0´ÔÌÝ\qcÝ9é÷"œ@ÈÍ.-o]=çµb: =Ñ»ºO$@Ú‘VOÚ4-m¤\SÛÚ¹ÍÙÍò€œTþ1ÃCôû€núÞåؚá~::ó¿(è” Ó 8޾dLÈðx&ì Sã-sw’H\ïúð'T¥˜1œ0«“ãÚÖÚ5ÃC啿šy÷„Ç`Īk/Î0d›ˆÞ§È㨾ÑÈ«…½E"ѯð”Ä†ÂžŽ‚2':Ý©žè Qø»óQ|ˆ#7ª|VD~—מ nmBqI@qº@¾žŸfÝ`&ùìH\ÜAßÚÓÀcñj·âdLÄø ž¢Žã W›vÞWÚVÍþÓ7ØÂ"1+µšèûõ<@¬ºîóXA1¬ à͵‹:ÛÛ‹¯ÝP‰DÏ‘“&‰êÚEÙ£[ó½¼ÇvßrÑ®°s6™âÍñ»8hsŽY­«æÑVgìÑH¯À-Éš¹7ÈŒ V¶wÍÅôM¸ùEع˜,K¬Ï§SOòFT¾hÅÐÕ÷ådÍœL_ 69f̵x1&SâÝŽÃÿä–­Í+gßD>c•ƒSüÆäÛûw챂hL@œJea¶bÄwCél®™ýÛ`²1#‰Dc"*lÖ oú1æùZþ èö¥dl²‚hL@TÔçný ª¶žÐ?_¯ƒ 6·bŒ²‚hL@<¥Ão 늟¯ƒL"‘°ÏÆ1È^tc¢N|DàcÄëüîK[k?'ó]mLα‚hL@\$ú绡v~qƲ~¨§W‘ˆY¬ Öq‘× ¢Qª“;J–Õç?£±Iáe¿1ùd¬ªöª ò1#‡Dc‚’˜•Ù@¤i割ªÚ«+ªî/ ޘ≼H ‘ïÄ—Õþ°¼jãÉÄ39ÏfS T9=€PqùVš‚å±êÚGùˆþ‰´¼é"Ò‘çܾ^Ï›—/*ENe¼ “¥NN hû²Ç¥¥A¼`~vUù¼Júó±êúçý"@Ýö´xÕ.Q‰DјâyªéJJ‡L&’ˆÉ +ˆÆHÅÕ£äõ§BÙ³Q 4ƒÃœœ×ÝóáO2€;Ïêm^1ï À—±äд¬˜w9aKع˜Ü5èeÅßžZž×“žŒsÓDÓ-•¡íÙèø¦ƒCÎpèæ–W,—µÜ…±rŠz‘] ô¾Ø\Ö¼‹+ØWÓ"Y3÷;å7mø¹Kå¯ý49´äi`2ê–5%o™ó‰†3Ë:{®½Iúf±ç4I‡ÃXrØ7eEME±ËãT.£»ç$T<4‡ß.ŠT©£*ì<²Î¹ý¯h„ø¾‰{YM­çéªÆ¥O‡š¦›/þ3ð·¥Ë6}-¢©«Ë*ÃÎ h6‹Èó8·%MdsqoËï“ag• ‰Y©X7=ÑpWkg×çù¢À‡ÂN‹¾‰?ì;‚Õç…ÈæHºwËžU Þ ;±±ä1¶zâNø/4÷¿E™#,r* ã·N¼­¹mÏu$²;±  ÝÞ“–q÷ø#¢Ï‘O®h]qÑ«À ×Å»zÿ—sn¶³€ÓÈìBòvàO(Äc3*[DÓÏ7¯œÿæA9>æ£ þNçuµùzþ0lKÌêîª=Ñ T½¿Bô žÁ¡ÓÀë(/"lAe ’Ú’LNÚÊg §oà<Ÿï3÷¼¿çë3à•ùŠàÜ._)ˆ<ˆ2ÑWˆn¬XUñQçyõ ¶Ëè!ÿÙ¼t÷åÈÞä2×-\)9¶dFDôqœ¤x'qЄBQ Ê@xßJ—@§B7°¤MU[E´Q‘·Dt7iyÓËï}uÿª9ƒÁåIDAT‚ 7<81Ïɉ¢œ¨"' h¡"ã=ˆ)‚¼û q­ êÙ«¢ûIªso©ÇNv{x¯6´¾Jb‘-ÉQÄâoO-ÏëîÙ T„É 7-ÝckãŒ1fÍÆÌëî^†ÃQI•¯UÔTX=cŒ@ÿ‚˜ÀùlH¹˜Ì«Hç± ì$Œ1&õ+ˆå¥å'b -G5ÏqaØ9cL.ê„è¢SBÊÃdØövÆ3€~Ñ©ë+“*t…ƒ1Æä¢~QÒém!åa²DÐ×ÂÎÁcrQ¿‚Ø\Ýü&às¦ÉeiáÁ°s0Ƙ\4À&غ.ûi˜¬¶´´5þOØicL.:¨ 6ïmü7àñr1™Õ«ðeäð.´Æžƒ¤ image/svg+xml Param param-2.1.1/doc/_static/logo_horizontal_white.png000066400000000000000000000211131463636336300221400ustar00rootroot00000000000000‰PNG  IHDRăuk pHYsÕ篶tEXtSoftwarewww.inkscape.org›î< IDATxœíÝy|\u¹?ðÏó=“6MæÌL–¦¥­Ð"ûb+‹B e¹¼¸ EÜ~ÜßåÂ¥e)‚KÜÀ ”¥€"×åâ È…–EðŠˆ¬¶ –µ-` m’Éœ3I“&çû¹ÌäÚÒ¤IsÎÌ™dž÷ëUµ™™çûÄÓ™gÎ9ßï÷”RJ)¥”RJ)¥”RJIÜ åºæºj,g[1õ@a¶¦¿æµss¹lܹ)¥”š˜*¢ .…)5½õ'[ò9Dó0O]KÁÃŽÅ-Ý¿= Êš¨RJ© +Ö‚xë ÔõöÕ]("l§ç%__ØÑýCXŠü”RJUØ â ÍÉyB~Àìp‘ø¸ ìÇÏëê]E^J)¥ª“‰cÐeMõ— ù BCìã¿Óߟh]äyq'£”R•lÈ‚Ø ˜†¦ºe9·Ìù¨’à«çŸvø«ãÎD)¥*Õ³Lšê¯Òb8‘È» »ü[Ó’-qg¢”R•j›‚xcÓ”C¸$ŽdTIíÒ?À¥q'¡”R•jÛ3Dš/¢Bö8U‘;ýæ©)½¬”RCت .inv!øp\ɨ’3 þ%î$”RªmU'³g/“cÊE•ï‰;¥”ªD[D‡R®u‚*.²Ã]E”Rª*lU­a{\‰¨Ñ ÛçJˆ ‘$¢”RÌV1!õ/è)5 ag;YbU$‰(¥Ô³UAüó€TÍ–gU(0ApOÜI(¥T%ÚvÙEÀ/°åOE•Ûæz×ćRJU¢m âù]Ý+)òõ8’Q%µ¶³¹4î$”RªR yKŠ€,k¬¿‚‹Ê*‰ÕŽqNüÞ+q'òNžç5‹Èô fˆÈt™F2mŒI‘LH‘Lc2@²@òqD¤«ð0=$ÛtcÞ ‚à5Y“J¥Ö‰Èæ²þ‚J©qc»s4nl®;”%ö-S>*Z=–“¯<¿³Ó‹-‰žž™ÖÚ½ƒ ØGDö°—ˆÌ!9 å]÷:௞%ùÉÇÒéôŸEDo(¥F7iñÆLý\qäp+˜ò0óBY  ”ã4Aði–iK8zaùŸ©š³L0‚O“|©vrσg·¡'®œ²ÙlÆqœµÒqå0 I> à7}}}¿hiiÉÇR*eÛ³ôÆæäm ÿµc‰ðœ…í=ß.ÇXjxžç5Ok[»Eä—¾ëºîoãNF)U^C¶*…` q‰P^+ù@‚ûÏkï¹µä㨉¨žäÇI>ìyÞS¾ï/ Y¶÷ˆR*^e{³_ØÕÕò_dK8Ì_ý‰3$ü†.JDòÎ|>ÿ”çy‡ÅŒRªôÊúíwa¶ûy ΰ¾áŸá1çær¥,¸ªÊ<Àïs¹Ü·HN‰;¥Té”ýrÐ=O‰ð OETp×äÉÝG.lïi‹,¦Rÿ`Däß}߯÷}q­Ô[#ॳ0%ÑSw D.àŽ1̾p~G÷íz™´òŒÃI5£‘#yL:~2îD”RÑŠ­ º¾¥~šð B>‰ÑOÏC€›ú§t/[ô&6•2?5v´ @ÖZ{t&“y&îD”Rщ½ úþlÔúùºÁÊ<ö0€+@À#ä%¡}têÈúlÕýV+Þ.ˆð¶ã8Ô××—â~¸R*SÕÄ3Á "<âºîÑ"ĈR*<]c¥ÔØÍËçóeÙlB)UzZ• äW:::Rqç¡” O ¢Rá´Lš4iaÜI(¥ÂÓ{ˆªd¢º‡HòÏ"r§ˆtè´ÖzŽãÖÚMÖÚ^cL­1fеÖ%™4ÆÌ!9…‰YïÃí¢¢$"¯'“É9Ú5C©ñ-wJ©Tꪱ¼d"—ËÍ5Æ àÓö4³Â;{žw4€QÇVJ•^2Ušˆ d2™gR©ÔR×uçŠÈ©Ö•`œQÇTJ•—ž!ªª!"p÷† VÔÖÖ~@”El^T<Ïk¶ÖÎ6ÆÌ‘i¦‘¬yÇS³’Œ1ëI®7Ƽ”L&ߎ*—r!)ù|¾™d“µ¶~ð28Xk7‰HVD:ëëëÛEd  ùÔú¾?›ä.¦‰H €f’I©|žˆôìÐ`=É·Ed뺯–#Ï(utt¤‰D €fMƘ:€ÁIc](ì¶ÁZÛ–J¥Þ÷œÍçóÓ¬µ»ŠÈLkm1¦@€‘N’où¾¿fÆŒ‘ô}Õ{ˆªd"¼‡øít:}N)mÓø¾€S£ŠÁ솆†Q}’œÔÕÕµ¯ã8s¼Àîfÿ„½ç¹ä*cÌï‚ XžN§Ÿ­”û›]]]sÇ9ÈZûcÌ®$ßÂï<£»bl‘u"²À‹$_°Ö®J§Ókvô÷Ìf³»8Ž3WDæØ›äìb>;íÐ/¶­^ù‹µöIYÑÛÛûpÜͧÛÚÚêêëë÷°7€=ì.";‹È ’-jÇöOˆÈc$ïN¥Rc~¿“4¹\î@Çqæ“<ÀA(ü»·< àqÖ?Qü¼C´ ª’©ä‚¾×ÔÔüÀ¬ˆBž’J¥îêžžž™ýýýóDäHØ@ÍPÏ-6?°Ö~7“É”¾'iÉ„çyç#  ©„Cæ< à± ~ÞÐÐðÜ–¶µµÕ¥R©Cƒ 8JDð^ %ÌgK›ÜKò¶T*õ`©¿ äóùéA "sI¾·XðwGጯTú<@òæT*µb´¿c.—;ØsɘQ.¯‰Èí"òí¹b¢Q•L¥Dð}ÿßHFÕPú’T*uÍcüˆä†ðkí3™ÌšR àûþ~$O@¡±oÜÖ†T*5mð/¹\î8ù€I1å³¥¿¸<•Jý*Š`$îîî}INò0‡Ø5ŠØ!üMDZ“Éä]C©‘tòùü©$8¤„yl‘Û|ÝuÝ #=Y'Õ¨ª–L& ;¢p» óóJéÑiœiŒYíyÞçH†>[ éxž÷Ïó®ñ<ï’Ïø€ã_1€­6^'¹•Q `_¿ÌåröôôÌ ,ŸÏÄZ»’ä-ÎDüÅ(\~¾Ã÷ý?ø¾¿ß–är¹ã}ߎäÏPÚbSHžOò¯¹\îÌ‘ž¬QU5Ùàw…îÃmuDñ£2 À•¾ï?\<‹3ß÷ïðß.ðî(’‹ɧ·ü{ooïjTX‹8ùðÀÀÀª\.7?î\JèP’Ïø¾N>ŸŸæyÞÝ"ò”`ùÓšD䇞ç}‡ä°“Iu–iÄ’¯hI&‚½Û"D½ˆôØ(‚ÁãíW«*Ë Ž Î“aDd5YQŸÅƒŽð»îîîcêëëÇÚ\{,1JND¶*ˆÓ§Oïö}ÿ ’;Ç•Ó0EäW¾ïŸéºî]q'S"“HÞBòZSbÎåÿù¾?“äÉ"²ùjAŒÀÔÖG’ý›6ÆÀ~’àûP¼7+`‹ÎÅ$@Ãâå+EðCÛ;ù»]×ÏëŠ/sUÕ2…ú¡~hŒYmmELòʾA<ØÙÙyXccc.îd¢â8ÎSCüx5€J+ˆ@¡`ü4—Ëõ¤ÓéßÄL Å] ŸÏç@òŒwÞßÔK¦a´>’h\|ÿ¢M}k¼‘ƒ0º‰JsI\#“ûÖ5\ºâ ,¼or©SUÑHÖ0a˜7|]]ÝßxQ û$‰Ÿœ(“ìÖÓ§òoeÏdôù±çy{ÆH5 yºçyÛìA¬qŒR—-ß­qSß„\‹±O%OAøµ†)æéôeUÂðªT\ô…í-R®´ûˆït¼ïû¥•ÕPg‡ðbY³Øqi?ˆb²“™1æª\.·ÕD8-ˆcаøþ#â)6ŽO°ŸáÀŸ2—ÝT$ñÔŽj‰(Îöf«VòÙÉ «=ÏkŽ;‰°Dä™aÇàýù|þ¬¸“¨$ëDäê-¦q5|îÃYm·Ñ «Y(4,^q\ÄqÕÈ¢ºLµ½‚Xégˆ&ùÙ¸“ËZûôP?‘ñp @òr’åÚ´¡ÚÔÕÕuàà_´ î€ôÅ+æÀÚ{Ô•hˆ€w5.^¾o‰â«w(^žú@Dá¶7{x\|‹Èymmm¥ú÷]Žã y†˜L&ßBå¬ Ýž]<Ï;-î$ª„8Žsñà_´ ŽÖ‚;ãð§(ýVOõî@땲ˆxBó€ÆˆÂ½²ÇÆEAq]7Š%(±(ö¦ÜÞ¬áqqDäãqçP-HžœÍf3À(–]d/Ÿ+Äé08Œ”™¾ @?ˆ^ô•µØµdI”oßÃ}zRpéòQÍL¤`’u„L2$Ö| ‚_eký»ÑzÚ6ëmT¡Ã‚ïû—EOD^î1×u_õ}¿Ûß¿”(œe®‘u$}Y’ý"2¸1tJD\’³`—¨òd­]àçQÇ-’ÃÝ?´…=U·ÇC¡EØ:ëE¤»ØÑb°ËB-É$€©"²?Éýý.8Îf³™††]–UzµŽãœ࿆-ˆ ‹LÁ-N‡@@þ£ ‚:ºj‰Ò(Ì:ÅSˆ@Ø ÀN€âc ›R¯ÚKï?;÷Íã.a¶ã’çy E$ªË¥ ¹r¸ÇD¤ßó¼WPè46Ü~’äóƘU$W¹®»v¨ÅÂÛSló gcø­ãvˆˆÌ#)cé0F›Dä¯$_°šd€ÖÚ·D$G2í8N=I׳«µv_Ù ÀØö^þ÷·°åÄš^OŠÈ*’«H®´Ö¾´£Eˆä$ß÷çø7' š=¢'9Žs€_Gk4ž‘_’|ÓZ»žäFÖÚlP[[;Ýqœ]Hþ3€Ó0ü®LQxUDîð’îééy³®®®¶Ø‚k§< ìû£c¸‚ØrùCÓúƒGð7¯¿ÞmD–g/?«kɱ·ÇL¥ÈårŸ‘k# Ù–J¥^ÚÞHÞmŒÙ<00ðëL&óç( Nñòà5$oô}ÿ.@ø䩾ïï2-S‘÷¹®»Ã3@‹gø{’<À!"rˆ1æÉÆzŠäm~Ï犢^ñK̽îÍåróEävD3sùp”¯ >âºî×·óø«Å?¿mkkû¼ëºW‘\ˆèDn'yk:þÓwè@á ÍÝ]]]sŒ1ßpTDãφºdºàN§ ÿnˆh1œ8ügãâ¯t.™ÿxÜÉÄ)ŸÏO·Ö~ ÀÿE„Ý^DäÑ‘ž“N§¿ÕxCŒ¿À"Ïó6¸*l<’¢Â×í¿P¬.þÕ—=×uðh©rJ§Ó+<Ïû €'r&ºˆD³¬+bÅ/ŸÍårYùR1I>L!R&“YCòß÷Da °fzž×¼Í¤šÆ9î§Qè¦&–‚· µµê&R‘Lø¾T.—û™µv€³}ë³_FoL\×]‚hÎ*öŠ FUJ¥R/ŠHèM¬µ} R©Ô—ü6¢p;<±MDúEd€¨î³î¾Í‡#E¶ÙÎFMs3݇DvϬe³ÙL>Ÿ¯çy'ù¾…ïûË}ßÏ’|DD>ŠÒ´jO&“QE„ƘËQè}ÆQäS­\×½Àv/ߎDDfUò¡ˆ|%¢XËë\×ÝHòæ(r ¹ÛV—L›®¸w¦ÀÜ(‚«Ê$bŽCtíŽÊBD>âûþÁC=f­5"’F¡Ð¥¸ƒi—«Ã„ˆüHDúÊ2Ø($“Éç=Ï{…½c"";E˜RµúÂõû“ºººé^‹(ŸÈ¹®û;ÏóVáꆈ0Ö×:޳ÌZû9„_F¸ûVÑn®ÙUW&Np"ãqÏÔ©$§õ€HìÓœ»1Wü´ò"ù°ˆŒ¹ ˜6òSÔöAðã„Û–´8³²b bÑ „,ˆ$Çü¹”L&ßÎår+ÃÕ¢­/™Úua¢VJ – äÒ½KÆvßΨ·&¬: ë"è¤RñÇd÷w&9æÓ1 ½¬LDfn•ÅVÜŒò÷¸S˜@^îëëûfÜI ccÈ×ë—ãh´‡y±ˆTdæ-cž Ìä®®®ÔX_Lò¹° ˆHýV—L;'?¹ºaÓûß0=lpU©ì£qg0AôcNkiiÉüÔ±Éf³»8Ž3‹dÒ“"™!™”‚<|À† Ú2™L›ˆô¾)äðÿA\icÌ»‰D#É”µ6iŒI’tE¤ ­µÝƘ6o÷öö¾1uêTðõ$C!ŠHÅ1q]w£çyݹP~Ò¤IuãŒQYfl Ðnëuˆ­­–‹—ÿD€Eaƒ«ŠÔ)Sx_ÜILpn2™üs$ÁÈß÷BaK±}ì‡Â¦.P¸O:8Ahðžé;' 9Žß÷ÏóÖxáÏ«n6AWW×ù€ˆÌ‘ý¬µ{‹È»`p¢Ö–ÇbËc2ø¿'Ož Ïó²^‘?‘k¯TÇ/½×¢ðowÌúûûÇ|;ÇqœµA„’Û,Ìú¯J$j>è6+"/‹Èç ¹ŒD•V.—;Ä÷ý'HÞàŸâÎg¢ˆàž5Œ1agÀ‡Í¡fØû]ß<ööMÞC _Xéë`Ô¶züBhË.9ö<|ç =ã›~¹>‚2™ÌH†”Ífwñ}ÿQ¹z+"$\.÷eùÂ-–WC‘Ð1‚°»H…Îa»ý»®Ÿ×à ¾Ð|ùò6o»œI²+­¬7†Õõ+ò!ÒF²MÑ(}K ?Ú‘Js¿È3H¼áõ¼ŽeÇWÌî)ãðK¹b,åóù¹ÖÚûèÎ/1!YëûþE䔸s™À*¡„ÎaÄÁƒÚ¯=’cKòÛv‰"–ÏóN‘sâÎC£¾dª :¯:î™ÅËàWvŽ0t‰Ù%Ç<aÌjfQ˜%ý:É5"²ÀËÖÚçÒéô ņº%áyÞÉN.U|5²bÛ¤1/QÕI ât-9veóåËß àn äJÈɹk毉 Ö„#"¸m˜Ç²@2‚À@G:î,gŽïÈéó%n=Õ ºÈ m¯jJ9àxS__€%&‡Â¯ŽA²Äã©Ó‚8FíW»­­G4öþÓGHY`ÎXÂ@pM¶Ö»NïÏZûj:¾+îà3æ1®H´Î à¿<@r¥ˆ¼âºî:Ùj!xqOÚ3ƒä$wp t©Í¸¡1ŒÖVÛ Ü5ëÂ;Ý]ã~"Ÿp0F¾7û¬ˆüÈ©tÛÆÖy%ÛZÅ⤈âôørOOϲÑÜל1cF g*m÷¬Ä¡…Ý8}“뺌$5®hAT*"ÖÚPÃ@DÞŒ"—*vùÃú¥¤:iAT*""v·~ˆˆ®' /ìqйUJ ¢R!E“TíŠ^Øã0¤~6V!=èJE$Š®á"rt¹T¹°ûÑÖû¾_ú®6ªâhAT*"ÖÚ¿Gf·\.w|qªY[1Î †g´ *‘t:ý:"hR*"·ær¹Ý"H©Z½AŒS}ß?7‚8jÑ‚¨TDŠ[w­Ž Ô,ùS.—;ãÆnñª É"Šs³çy?ð}ï(â©Ê§³©”Šˆ”/;“&½ÚpmË9 "i~¦TDdÉ âΣÚAðUëâÎC±Ħëšöî7|‹®]Nƒ€·4.zÇŒ[gÔE•ŸRa¥R©{D䦸ó¨f ]$?`sܹ¨ñ¡ì±ñº–CiÍFѳlЂMùþß7-mšaL¥BI&“üYÜyT³t:ý€ˆ;UùÊZ3×¶Ì…åý¢Ž-À¤yнifSÔ±•  R©Ô'üWܹT³T*u€ÓäãÎEU¶²ÄÌu™Œï.á0{×ômþZu9‰ª "ÒŸJ¥>-"çA?cS¼„}(€Uqç¢*רg™º7ÍlªÙL‡µ³„A—$dÇöl´øªæìp†;îØ&·ù*¹w”a¬ŠBã4  ú_ìÌt¾³Ãï«©¢áºîÍ===¿ø —ðbŸá=‚ ·¬ÉuÝH¾Ïó¼sDä ÓâÎi$Ƙ îªÉvß”ÍKš][ƒ A9}›÷@1`¿]¹”—ÆGÙY[<¢óS}\ƒûáÕí‹ÚŸŽ;5ÔÕÕýÀ'ººº¾ä8Îù$OÐw^:¬‘çI®²Ö®L§Ó‰;©R(î7»Œämù|þS$ÏppÜy¡0ñço(œÁ>o­]™H$V%“É·bΫª [®™z„ü¬üoQjH.§YʂƥSoìô6^ŒÖòN,èëëÛI@@;É·l‘7‰ÄkÅ3T5ß÷§’ÜÀžö ¹Kñÿû)Ƙ†âþ²“·x‰‘ ºDÄ‘lñ¬'¹AD^s]÷5Ñe j›‚èÞ4³©¦oójÍ1ä£JH ;mÔµqJ)5„mfcÖôõ]-†‰/5/iÖþzJ)5„­ b+ gÆ”‹*½æ 'Ä„RJU¢­ bSºiOhCË ÍXwJ)U‰¶>C´‰1å¡ÊFt{;¥”ÂVÑÒöÅ•ˆ* zãÎA)¥*ÑVQ‚`mLy¨2pMÜ9(¥T%Úª v.î|@¨šª²‚ûâÎA)¥*Ñ›`sYùÓPe!XÕåµÿ>î4”RªmS;ýöïx"†\Tiõ8­oÓmÌöIÌIDATf¥”ŠÂ¶gˆ­0†§x©üé¨ 8'»h£ž*¥Ô0†ìØ~aûú`óæ÷ƒ¸н/ǹµ œÐ¹hãwãND)¥*Ù›{o©ùšæ÷Y˜Óá ;Bô‘è“ÑÌ)ʺeÁN”µ»C\Ôˆ`2Á!zy‚ÕB¹·£&uÎE—Ó(¥ÔF,ˆ‘hE¢Ñú,€ýË1…?Ì.jÿd9ÆRJ)51 yÉ4r­€‘³²tp_?)p..Ã8J)¥&§\mZÑýæ”cê=Ž-ÕôY⤎K6üµTc(¥”š˜ÊV`Ó=šrL}‚F[€>gf/ÞxÔ±•RJM|幇ø K›?.”ï¨(d»žÚµ¨ýwÅSJ)Ueb)ˆÐp]Ëáby;€]C†zŒ‰3²ŸÍ¾E^J)¥ªSlf-5eû.p!©;ôbâE‚_Í^Ôþˆ®•TJ)N¬qÐìÖÙµžÛ}ˆSD0o;Åñ5@ü¬£»ãQ݆L)¥TT*¢ ¾SËõ-Ó‚~™Da1Û.}k²—es1§¦”RJ)¥”RJ)¥”RJ)¥ÔD÷?ÃŒÔx”}rIEND®B`‚param-2.1.1/doc/_static/logo_horizontal_white.svg000066400000000000000000000325051463636336300221620ustar00rootroot00000000000000 image/svg+xml Param param-2.1.1/doc/_static/logo_stacked.png000066400000000000000000000260211463636336300201700ustar00rootroot00000000000000‰PNG  IHDR,\r_ƒ´ pHYsÕ篶tEXtSoftwarewww.inkscape.org›î< IDATxœíÝ{|\uÿñ×ç;“´i®3MS¨\ÊÁ ¸\Å´Pw•ª«²?u× -´¸+ƒ 4-t\/«â¥(^ mR xCË¢­ËEîP m’IÒÜ“™óùý‘¢zI2gò3óy>üÑ$ó=o2™÷œsæ{¾Œ1ÆcŒ1ÆcŒ1ÆcŒ1¥E|0;÷åššd<>r(Πhµ µ(NžísÊæL}¼»kðù¾ó3¬° ÄuuSgÇãn®"§ ¼ØkŒPå½­ëèìù}Ê Ì)+,¾¼eýÓþ1@Îá„ó|<ò½l<ó…[Ÿa1pÁûÞÈèܧ 3@âŒÎübt’†¶a¥ !Úx~ô6Õ¡ð P®0E§JÌÁ…—€ÍÀ#ªÁ/:Ó÷§ŠìBø¢)¬›g1mp¨ò"‹€™¾óS¶×e*únX´‰ßaÂP…uC²êC"z½Â|g1¦mR‚u üÔw\EúSB™>½² á¿€ZßyŒ)P5‚Ì?cZyÅñ#÷ü2ÌS!“,Ò…5½¾êë@#E²§hL ðŽiSö^30|‡ï0Ùº¡¾ò”žs)ÂßÍ«(ïZ30ò€ï(É=“’' î·D¸pñ( ÁÛt <è;ÈxEs–“k±²2f¢b¨»Öwˆ‰ˆ\aÝT?í,TNòØHÞù•ÓæùŽ1^‘+¬@ù'ߌ)¢r®ï ã©sX7Ì:+Û€jßYŒ‰>í%Ñ_¿àI†|'«HíaIWÕ‰XY© º«Ž÷b<"UXªºŸï Æ“X­×T¤ K`–ï Æ™H½¦"UX*Rá;ƒ1ÅDE*}gH–([|g0¦˜Á˾3ŒG¤ ‚—|'0¦˜(¼è;ÃxD«°2ÁŸ|G0¦˜¸L°Áw†ñˆTa]Ø=ø B¤~Áư‡/è|ÎwˆñˆTa(ùEÈŒ)ª?óa¼"WX#CrÐí;‡1ù2I«ëu¹‘ØW'gSá‰\a]ÚÓÓÉ+͋ɸ^N ù‚žžŽIØT¨"WX™ŠþÀÿúÎaLD­¯ªî»Þwˆ‰ˆÔÅÏ;º±~Ú,T´O˜b¥äåº9¦Áqç§^èü‹äÀ…íý/©Ó³%bóHŒ«<”Õ¦¬ãŒ¨–D¸°´õ?œ=V ’ëS3yäþ¬èq Ûúþè;I."]X Ûû_.ïè{§Š~†Ñ»Þc^!´‹Ó½ï^ØÞ©Ëpv&²ç°væ«3fTÚ÷qUùp2Pî;“1 !Ü ú³˜TÞz~[[¯ï@a)ªÂÚQs}}õ”ìÀ.¦ûJ ‡ˆkÍÃ(EywøãNª.œ\‘Y²”)Rí4˜Ž“ Ü#ÏYya(VñLS{{ï|ùP´…5Ù¾V[›ÈÄ3 ¾³L”¨|äÂtï|ç0fW"«Pü[ww'p™ï9Xkee Vˆtô}Gá{¾sLÀVý¤ïÆì‰VÈ¥â߀¿øÎ1Y§|üÂö~[kÌ<+¬5µ·÷dãÙ3ˆÈ E^î»ËwcÆÂ +n|§ó(ðU%TäÊ‹:zoôر²ÂÊ“mýk ï ñPKUäÊ‹Ú{S¾ƒ36­!Ïnh¨=ˆlf5p¨ï,Û )|梎¾ÿñĘñ²=¬<[°µû©©8Fáû¾³Ï¡Á{¬¬LTÙÖ$úÊôÊX̘äM¨|ʤÓÛ&yÛÆ„Æ k’}¹¦&/Ë| 䟲üoQî‡ìE :Ìÿ¶ŒÉ/+,Onª›ºs—| ÈÇ­…rõ‚tßÚ<ŒmŒVXžÝœHÔËЇTäÀI@|¢c‰ÊÓ?ùÞ…=…—Ò˜Â`…U@nH&kœ|WÈ»ŽTÑÃ@f±üxÊc8y\ƒà— î¾°{ð™I lÌ$³ÂŠ€¯Î˜QýU™LP^”÷|¶»»K&ínPÆcŒ1ÆcŒ1ÆcŒ1ÆcŒ1ÆcŒ1ÆcŒ1ÆcŒ1ÆcŒ1ÆcŒ1ÆcŒ1ÆcŒ1ÆcŒ1ÆcŒ1¥ÈnóeJÏyë˪j:±x¬Šl¦Î•Ép0LŸ‹¹tgóiݾã™]³Â2Å-uo<94x¬r2pp8p P¶‹Gt+<îà‘@ù5Ùì=]+Î|nÒòšÝ²Â2EH¥®ií;sýP›ã€€|WâÜš¾jÎ !4d…eŠG*å’ƒÇPURÀyØB€°:Ü•ÝËN_Ÿ‡ñÍXa™¢hj=ô:à IØœ*ú§º0½lÞ¦IØžÙÎ ËDZ}ãÚYYü'ÊûØqÙ–|1f¼¬°ò¨æË5ɸN¹8Áw–×èW8§ó’¶Õ¾ƒ3VXyÒp}ÃÌLV ê;Ë.Œ€|4}ÉÖŸøbÌXYaåAò†dŽÄîx›ï,»'Ãg§m]ë;‰1cá|(:+‰1ân/ü²ÐrToK^›<ÂwcÆÂ +dÉ®y¯ïãPƒ¸•³nž5ÍwcöÄ +DõËëßú9ß9ÆOŽìË\ç;…1{R¼ç°R”×ÖÎÜÇ¡{»,3ƒÕYAÚ4üëìTã‚kf…>ödô‹ úŽbÆÆ!S­G©uêÏÆx6@^îž¶å>Èï|ùPT…5;5{jwUïÅÉPæU¾3ãAТ¢?ëŒ×ý¤˜.„/ŽÂJá’Õ3>,ôǘòª_Jï×þ æ“õ&W‘/¬êkgÕ—ËÈÞí;‹1Käž‘ò²ù=¼Øá;J."]XÉk“G ±UØ^•1{$ð.8»caGdÏSF¶°f,›±W6ƃÀ¾¾³ò|<Æq[/ÞºÅw’‰ˆæ´†ž’uÜŽ••1ã¤ûe²z;)<®2q‘,¬ÄH÷Å'úÎaLD½=YS¡ï¹Cºëêê\PþhÒwc"¬3#Co[´-í;ÈxDn+¦eXY“³D™NùŒïã¹ÂBùߌ)Jô^K‘:$L|%±ŸdâÏ±ÜÆ(%“Ý/Ý”Þä;ÈXEjË”½ ++cÂ"ZæŽöb<"UX°·ï Æ÷ß Æ#R…%Ê^¾3SL¢öšŠTa):à;ƒ1Å$j¯©H–yÉwcŠ‹¼è;ÁxDª°²‘ù4Ø(ˆA¤^S‘*¬ŠXÅï>ß9Œ)½U}U÷û1‘*¬M‹6 vK*c ´<›zvÐwŒñˆTaˆÈw}g0¦î{¾3ŒW$'a&W̸x»ïÆD–èé…í'ŽÞ|$:"·‡ Âb ðØˆ TÜ%Q++€˜ï1ØÚÿü´9•YàßYŒ‰U.Dí¾sLD$ Pdú—¾§è?úŽbLTú½ŽEíçFqï "zH€ ûn=W Ùwc"Aä–ŽžöOFµ¬ Ê{X;˜¾¢á\EWõ¾³ShÚÕ…—¶ßê;K®Š¢°’7$k4kå Æwc @7Ê”g—§¤·ù†¢)¬¿JQž¬j8YEÏF8rûÕè3™Zª@VT‚w§R¿O…!±O` ›"*êbˆà”à%…—GDäém[IŠaßqÃùX¡H^;ãC·ùΑ£Ÿ¦/i‹Ü²¹¦tD÷¤{I_Úöc`µï9è\l‘ïÆìŽVˆ²»èòc"DhêZ¸ùYß9ŒÙ+¬u_¶å=¢õ±±(«:¶}ÕwcöÄ +d‹ÚW¡z­ïãðh66Ù‰„¦´DòÒœB7ÐÚ¿®â÷•ûGùβ/?¥ë’ö-¾ƒ3¶‡•‚¦«ÚÎ~ê;Ên¼ŒfOïºôåç|1f¬¬°òå3Œ¤÷m;äfßQvâé˜ð®ô¥éG|1f<ì0ŸnCZûîœzUŸÀÉÆD눖Íë¼dK¤Öò6lâè¤I®hx;ðÐý‰›þŠ;¿sÑ–“¸McBg…åAòÚäHìóÀ9ä÷0ñÏÀ—Ò‹ÚVÚ´S ¬°<ª¿¾þ0ÍÈùh2¤a³À]"rKÇ­?³¢2ÅÄ «¤(Ÿ^Õ0O…y‚ž¢pÐ8GèVäWNô.—áÇmm›ó’ÓϬ° Ðô/Oƒ±#DôPU9G5Ö!J¯ ½^Vä/Èãûoy”ùd}ç6ÆcŒ1ÆcŒ1ÆcŒ1ÆcŒ1ÆcŒ1ÆcŒ1ÆcŒ1ÆcŒ1ÆcŒ1ÆcŒ1ÆcŒ1ÆcŒ1ÆcŒ1ÆcŒ1ÆcŒ1ÆcŒ1ÆcŒ1ÆcŒ1ÆcŒ1ÆcŒ1ÆcŒ1ÆcŒ1ÆcŒ1ÆcŒ1ÆcŒ1¦€È˜2•r3úŽmȨk F ŽjD\¢ ””ØöŸ®uÛŠdQ¶þC»Å¹ÐŽíMÛ¦n{žÔüáÿ¿Œ1Eèo…•º7^?Ô`&?BÞˆÈ{‹ê^ŠÌ€xžr(/"<%"‚€ÿÉüoçÓ}pÛülž¶iŒ‰I4µü䠇徽F—À=*Ü¥#Ù5]+Î|Îw cŒ?’hjQß!ÆH~‹ê­dWv/=«Ów cÌäŠRaí¨WD¾©êVt6Ÿö¼ï0ƘÉÕÂzÅèéÐÔÏw]r—ï0Ƙür{þ‘‚Vr¡LúK]cë?ùcŒÉ¯¨ïa½ŠÂO„ا;›Oëöž¨ïa½ŠÀ!û»úË[ööž¢*¬íŽÈdùåôÏ­zƒï Ƙpca!ph)_3+µjšï,ƘðeaÒ7÷”ßà;…1&×Ïó†&-Ì9+cUû×NÅcU;æ(ŸZ9Ò–:¹w׿°®%\9¡CÂݘ‘º·*30|è a)qÙ/}ÕœÆþ•ÚÅw ÁÈQ"r00•ý rhÅžFx퀠πla#ü&=ýõ¤þqAÃåëff2Ù·¨Óƒä@=P„”z HÂ_—Ã~E'Fu3 ϣ¼àÅ\|ãÖ«OÝ2Ñ<µ‹ïH@ÙÑNõ ³Áí¯èlÙÀŒ Ù‰ÈЪü¡<oÍ%_¨ÎY«]5;æä`U9D…itoUf"Lgô9(ÛÃH›Ý„ðð q÷ëΫçü9ç|ç­/««Ýúvqî$AŽ}ÓÎEwÏ <£ªëUÜ}UÃÝØtÝü‰l¾`  þò–½³YþH„1žˆÎO/=ã¶]}?ù¹Ö}ƒ}8ŽC9 x Pƶw£X§ð­®Š)w’:9“çí½ZêÞøô¡á¿ }7p"p °O¶Ô&°AÑ?¸ûj+Ë×=›:yðµ?4#uoUv`ð$DNRÕ£@Þ ì—‡<;Ràa¾?,Áwz¯™×–çí£{IÚçÞŠðV…·:Ñ£wäÞÇêàÖ²Xüã)è}®¬èŸR}*Q8 ¨Î1GÂí¾Ó±tÎÝãÙ;/èÂH6­Y¤ÈІû\góÜ«w¾ Î90¤íLÔK ˺*¶ýgÞnÊqÎÊXòÀÚã}ð.à$ j÷ ŸÀÖtÅý{“Jµ‹[ÿÎz#±ø=U1,ðý “M…º$w*åljÄNÞ.Ê 3žÁ„gHàY‰±{é©Oïê‡ê¯™²@Oµù‰"E¸*=õ÷·½ò·°ÛŸ.ôÂ=4Úä|] À·ÒÍs?õÚ¯'›Z®S¸8×ñCô¬ŸM/ÛÆ`³R«¦õ”æàý g3zHᕃ]ÍsåßµkqüÁg¦×VtVl»2×7ºÆ–“Eøùß[¯Eo*«˜úùÏ5Õ7®•• |’ÉzóP~ïˆ}ºcÙiîîÇ ~âh[êä^„{ÂM÷ßé—EvûKò`¶*«Mknà¼õ{:W±[u­W ”µ üLGÿ½—€ÀC;þ»|ZÙcŒšŠ)À剚ûk.[{HN#9­£ðÊ  L…™Á¡?'WŸÀ9+cɦ5‹²üø&sOW81ìÃÉÅ­¯Û¡ØQÁ€ŒN{ÈY€ìôØ;P cü È…‰DûO÷Y¸râç4$x!ì†MDÖïøï¶Ôɽ(›|åÙ£ ~SwÙÚ£|Ée÷«Ä5m?ýRé)ÉUýF²©åÊ]ý@$ åF²‹s5—-´=¬ÙW^óSR÷Ô”\i&Xÿº¯IA¾q 0S\pÏôÆ»Þè;K•oõ@áóɦ5‹vö½H–ª†3—Cv¾§±ýS¡ŽP¶‘sCÅ´TNgÕÔG^ûEWx‡æ;J’¹£ú’UqH]ìYžhj=ãµ_FaIH³Þ•]OÚT-Èw÷|¶®±õ¾C„BõáMßÐ xÜGœ±“ãñ²¯úNQ"è7Fçà½ê‹…Ï)ɆÚå g¥ ßÝá¦úÆŸç:Æ?áwöå(<ÀüÄe­gùQ"öv{Õù¬H–"‡4Ô. ˹Â<òjú†¬+ÿWß)r¥ÈëÎ_”iAŸKü§_õ1ªÉyÉϵîûÊ¿"QX'…4Nû®¾¥¢Ñx±¨,Šú‚„νþ„;@Ûò37Q¸!îQ‰¦µs}‡(SÈè%¯ü£à «æ²µ‡ ¼9Œ±Tõ©]}/вìa°W]…Ìñ"½é'{w}®J‰È‡Ú{'‰ÂÇH­,‡[­agb.XHH—/ˆò䮾×=õ·Ï&Nãu\# /ҩУ0"H€¢u¢Tᘅ²ó‰ª9Ç|àù;¹m~v—ßÓÑk÷Ha‹ÀKÀ€B¿@/Èè4Dªt:ÂaìùBáñΚyike1¬üõuýÕs»à]XÉ%kNÔ€óÂO¯û(ý¯R©€¦–¿ðê¹(/ƒnDÝŸTôÏâÜSÎ =ÛQöÐËc¹î©zɺéeš9VU>ú!BZãKTN=‡2©Kºd€'yTE_T¥M„­‚v R‡HU A¥C àÃÙÉìnUÝéáà_¿/úèkNõ)üŸÀŸÙèDÑç«§”?»³‹§_'µ²¼v°öÍp– ŸFØw›Ê‘¸œ¬ i¼=A×›ÝìÄm% u¤%›ÍŠ“½AöC8 å$ò{ô”Y¯èï}X…M™( ‚¾Q‘84Ì Šss(äÂJ6µ©·óúeL&j¨j¨çÝ xXU^D¸Üͧå4aµçšS;€ ¥áòu—Žd3ß^7·d¼ê—¬>¤ýþ’ëXc!ðátðóñ.ƒS³¸åà˜rœÀqª‡p´ìâ„û_©{ ôwJÜ‘žöàƒ;{só¤¹ÔüáîÑË€✕_JT½•«c¯KƒI+,EÔÕ<÷’=ÿ$Ëj.[{L)åï ’IDATHÌÿÍèÅíá6 Ü2’•Ûz¯³u7?¹$±dÍ<¹™°VþP= òP¥®qí¹Š~…0¯¹Okð¤›ç~2´í½Æèrzf¢±õk9Ò—ÍÆ…É)¬¬ÈSY³kÛÒ¹OOßà¼õe³gõÄ:wó˜Îæ9«€U º'·ÍÏvÂòÚÅ­9Õr,-…cCJªmËO‚TêäDÿ ßCøhcªò©®Šûÿg,Gל±ºzɺ£âAæà "^ßøóê‚9é^wñ½uuM-%šZÑoörª«CoBD;»ê z_ÎC9Þe"·32¦Ã¸<ë^:çU½4„¡ ÷9H¥‚Ê‘mŸFÉ}Ñ>§ÏŒµ¬^ÑsÍ©ïgt勜e\ÙÁ~ +•rÉÆÕû$K.nùBbq˯eÊÐfëÃò°ÅL,Æwó0îøÝr̈û‹E•ÃÈSªºžéùªæ¾‡º_N¦çÙ¦ëæ²Ë ‰Ç%wNäaéæ¹ÿ|'ŒNÝ!!ê1‰Æ57ïô;âb5ˆNA©öa€ýT(]18œ»%¬n¿zîË“°¥1I/›w¢©õþ\–ä af*9·ÍÏJSË ÀM9Œâz¦$÷v¹žoéi¿¿=1püÓ¹.N)Žã&úؘꊬHÎ÷U•° KDvþiž¼ÒH>W:·ÌãÖwJ$hU•Ö«×™á¥)MÙÀ­¹qå¼NLFfRÀ…E*°¸e5Ê9£L¸ðÚ—ñx²©å …œÖR0ç°òEÑw^szîçŒB;¿žn¦‡¤„m[~ú@O.chÆüó Üû(r@.—#©’sUÙ¿Ø «W¥¬Éwˆq1r½ÑAè7œ-Q»¼\k,$¬•Dò(×rE+ê.þå„?S—{A« pZCˆTþµ»y׋ì‡a業•dª‡b±*Aj%К˜#†HOFÉ@¬³z(ýòk§T¨¸ž“ËI¥Üx?¹)Jç­/«Mn®‚© ÑLµ8Wå4¨² 1é©ޏøæíóâ^k[.›V ¾°Ú¯ž³9Ñ´v¬WqìR¼|d½Ö3«Ïàr»`E‘Ê".,¹¹sÙœ[Ãm業•CñàX äMÀ‘GG£Ó!FLT·O5ÎÐW^C¢©¥Ø<‹Ê#’ekΧõ^:+%TXÛï<£ÄŽåDŽ„öÙhÜNÆ‚€ÀSEx!ÑÔ2¼„Èóª<é„Uƒê\®üR)ĹŒ¯% -ÏAnŸ,hfÂ…'®ìØåYc£ë–|¯óéîós¥fqËÁ±@ÏAÜû†ÑcD%žÃßvíöÿŽDôÌBºÛB!K¦V×hìl>¨ú !°½cÆýdL@õw«òi?wÙš| ›$ÇÂrN'|o€Î§;_LX£äò ŠoK‘ot=Ýý™Ý]`»‡¤®©õ}] Ê)ˆPX7s) õkË"ëŸ@´ÒžÜÈnÖ‚+§±‰RÞ6?Ëèn.‡ÐEUXƒ"rQçÒ9·Lt€Ä’µ'iÐúeã´DÞy MÕ’Õ3ʲî YáŸ)ÈKÇ¢JrÝ›Ìæ°‡µ]?¹Ö”âøƒP½OEÎï\:çOyøìÔ½S»†®".–¬V¬êšZÞïnQ¡Áw–b#È@®{©ŽXyŽ)r=Z‰za=ƒèç:›çþp¢K­Ô,lIv ­x{ØáÌØÕ5¶ü‡À•Z*'•&™"Ã9ŸÚÐlŽÏætmˆha <„ÈòôÔòŸììî+cU»øŽ„ øð¦ã™qJ4µ4¾s5Õ¢8ˡ’ý‘ÜÚ½|n®³Äá¼õeNÛoG¬¬|J4µ~ÔÊÊŒI!Ö ð{`½«cêý…9I2™lÿUÞÖxfü—®y èõ¾s˜è(€Â’Ðg€gžTþ(Aðpç¶rË1»¾ñi—®y‹*Kò1¶£TÊ1 ßbôéÆŒI8…%là•U%w& Oœ Ž ¤í§é¸”wŒ®Ä9ÉbîßA  ¬KW]ÿ‰ïCôm¾s˜h çE«<ÖÙ<·9”±ò¬nñšÙ¨~0Ì1R¸_D6€<+¼ Œôd]ÐŒh̹X-±X], Á~Š‚èÑ §‡™#JÄé¢çãnîAô¸'ÔñÄ:a°@F¤FÄÕ"R+1ÝKUÑCUåƒ@U¨ILÞ”Þ^F @B›kµRà ÛWUÜ×­¸¼õMducH9"¥áòu3G²™pnŽ Oªhc×Ô©«öð‰ñN—’O4µÅ«ï”d XÉ–ˆžÂTŸ@á“]ÍsCYúµÔ g²s%„7 A~:µbøã/¥Îî#—)|%8«[ŽÎuE¯°²š8'zTÃ<\SQþVV¥¥¤ «êÒÖ`FŽÃ¤§dÝuaä)U™ë"\Uwà1“«¤ «Œ`¯œQî²Û“çF”½s"HOÝ–Ÿ{š‚VR…EY¬:ç1„çBHRÚ$×OåäeRós¾.ÍDOIV͆°œ­–Î Ÿù“Ûó ö”ª’*¬˜¸ÝÞª~,Éý°Òäö<3H¥Jêo׌*©'=$çO”»þ0¹>S“CÇJ)%UXeŒla˜ÙÓ·œÂ8%KÐÜ/Ç r¿“°‰ž’*¬¶åó¶½¹Ž(ÿÕpù:»óò©òdÎcÀÿ«klý@yLt”Ta¨Â£! tÐH&ó‡ºÆÖع”ñS$Œç@DôÇɦ–+«—¬+ø»/›p”Þ¥9è¯@Ž a }ýibà„çi\Ó¢"÷Åœ<.YyaÄ tO]ß];xL-@0¢1WVQtJÖefŠÊ,²Á›Kv5`u¿DBù /¦ðùx½,Ñ´f"w;•G2NŸ2‰©•Ãôº¾A™£@«“x,. A ‚̵õã#¤ô Kbw©—†8ä~ˆœ'p^(ˆâ4Nbà„¿þ€‹š!}¥¤J´¬€®Êßm¨8a‹@H‡ÕZr¶ÀÙ*ºý¦¶qº†€²¿FÄGë¼òÛ·›‡EMÉΤ§þnMþô,• DÅ®Å4ãVr…E* LøÞ…&Y§·ð×{Ë36¥WX@EÅÈõ(/øÎQʶ-û$ðuß9L´”da½”:»§M¾s”º\¡0ùKd›È*ÉÂè\zƾé;G)ë½f^ÊG¬ï,&J¶°º*¶}¸ÓwŽRÖµlî½"rö‘ƒ’.,Ró‡+‡·jk+y”^:盟òr[7SLqÝ_¯{Ùéë&S«k‚~÷~q:•S çµÈÇ"꣈lý“à6:ÜØ¾ìý=N6*ÔåH³²-—ÇODÏŠ³Û©ÔêúŽ{'âÎá½À[Èÿ‘ÁÊÀFD6à‚ âÜÆôUs&4wO$¶U5Èéu&¹<^…ÇÈñµh¬#—ǃܧèS9[€ÒRuikC<Æá=LÑC9•†Ñ½0­©¦mÿ`¯|d/l# ‘^E{Dt«*[î%%Ø,ªOŽÄÊŸè¹æÔÿ(ŠÛ¬Ôªiý}Su.84Páp`J…Zþöû¥¤ùÛ §ƒ =z€.mо¤°ÕÁ˨<› x²û¹îç¹m¾Mµ(@ÿQh×­ k‰IEND®B`‚param-2.1.1/doc/_static/logo_stacked.svg000066400000000000000000000311661463636336300202110ustar00rootroot00000000000000 image/svg+xml Param param-2.1.1/doc/_static/param_help.png000066400000000000000000004100621463636336300176440ustar00rootroot00000000000000‰PNG  IHDRÐ\4É IDATxìÝû¯Óÿøñþ~õ“ŸúƒüÐ$‰H¤ù4"""¾ÞÑhA—¸u©û[½£îJC¡îBDÝZ‚*iQu¿¶UÕRZTç›ç¼ß¯±ötfŸ}™}Î>û˜]qÅÅ÷Œ{ÓM7et×Ütºä’KŠû,ŠaÓó2¿ñ8à€*×ë‘G9~…9½øâ‹-ûay¿½æšk2öçuëÖûÒÞrË-Å:>ÿüóÇ}þ£:Czå˜>}zaËñ÷ƒJ’ž|W­ZUlOûî»ïðÌ’( € ( € ( € ( € ( € (Ч@#ôßÿ½¸‘ꈣ«×x_>ûì³}.Bõä<“uË–-Ý{ßxãÅü«ÇöÓÉ"ðÇäë”õzøá‡çëÕzwko¯½ö*ö‡òþX~衇f_~ùew3ècì>ú(;ì°Ã²ýë_ÙŠ+úÈÉIË»víÊh‰~ñÅçëtæí¾Z^£õ~ûöíyï#zè¡ÑZ8—FP@P@P@P@P`J 4@Gð /Ìè8p³fÍÊN9å”lãÆ90­I>øà¼uÆ¡;uºZÿþûï¾î¼ó΢\Ÿ™37£>:_¯л#§•7AêØWÙÙ_ù›9sf±Æ÷ûì³OöË/¿t7ÇZ«¯¾:_÷ƒ §î«©†¯P@P@P@P@P@P`˜  ³÷ÜsO”£UzU¢µ9Zw’6mÚ”ñ¬Õ×_=Û°aC'“ì6ΨÐwìØ‘ÑB÷ÝwßÍ[×ï¶ ~@ËÁwÞy'[½zuV·ž:̪§Ñh™úá‡æëô«¯¾Êþúë¯"ŸnÊÓOPŽ.­×®]›½ýöÛÙk¯½–}úé§ŸušhÍKå ™žîÎY.Ÿ÷ÝwcfÅrûí·yKë7Þx#ý÷ßçÓQZ 2Ñí2ûâ 'œÐ2ÊEëo‚éD_¼xqË8ÃþfóæÍùöÅzI·¯ñ(÷×_½ôÒK-•ƒèƒmeÙ²eÙO?ýÔQ1šÚßË3›ŒôØ_yüÇÀØ×ÊË6êïÙv؆>þøãŒcP¤/¾ø"ßæØöÆJLÇøü޼úê«ù±xR«ñýDÿE9* € ( € ( € ( € ( € (C@çf>ÏP§elðbÈg<;½›`g§ôõë×·´¢§…&¯çŸ>;þøã3ZâRŽý÷ß?;õÔSóD@jÈüï¾ûî–r…Åž{î™uÖYy—æåùÓÅùŒ3 ?z µ?]cÇô1äØã"мß~ûí6Ö)ËqÌ1ÇäßÑå~'©—:ÒÖ×aÃk¯½6#pÙ.=øàƒ-¶1-ÃÔ|Û¶m•Ù0š7o^ÆúK§×Ñ“ÃX9}SÖÐ#*D™X?ƒJ§vÚnû:ÛÄÊ•+ÛÎòå—_ÎÒîè©Às™ËÛyñ(‰¦­òÓõÍú<ꨣ 3ìp£’eK†ôP•zÝßÓ¼86’?eÁ‡yÓ»½„üðÃY]¿(#Ó¼õÖ[E¶Tl‰í%ÆaÙ;M½ì«ä]ÝóÔY¦5kÖtZ„I7=´„5ëcöìÙÅ{>g;'`^^/ŒWu<çøÒI'í¶-Æ<˜ßX•Ô¨HSÞ–™þöÛooë;Œ¿Gm Üå—TÒyá…òã:ûMZ¹¡Ë¬]P@P@P@P@P`†.€N«8×qS¿nH0»Óîß;  ÓZµ<¿4ˆQþŽ÷—_~ùÀZ¸~þù绫Ê@@ã¹çžkÙœhaX·*ðãœ{î¹-Ó7ù†  b^c òw’º ÊQ1£A”‹Ç Ô¥'žx¢e9šFÀ;¦!-e«AÕ‡!ÓWÓ)ë SÞÊ-Ðcž碜gŸ}v|Üø )1ŸtøÌ3Ï´ÒñÇz=V~mgVñ%ÁƱæÙî{öÑ4õ³¿G>ôìëµjÞlgtP^îrîåéî¿ÿþÈ6?ÖVå×ikún÷Uåý„ùWí¿7ß|sQÎQz‘VΨ²o÷ûF9ÑëK»iøŽ <ÇëºtÉ%—Tæ1ÖïÇ0ýÕ-[¯Ÿã•ëÂ÷¸ãŽë5;§S@P@P@P@P@¡  oݺµ¥E)-éBÒ­57©i]J«Ã¸IÍëv7û÷Ó:ãÓuöE]TÌ#æEp‰ žåŸ3¼òÊ+cV iA˜¶¤å=ݰ¢5(Ñb;ÊB€4}ðÁy«Óøž!­9i)‡+²4 EwâƒHÿþ÷¿[¼N>ùäŒ@4Ý–ü*Pi!ÞIê6(Ƕ-– Ò?ðÀÙgŸ}–X_ºtiKÅo¾ùf·"Ð¥zx|LääQ.UµŽåq±>xv{ÚÕ;óä³ø~"è´¨¤µj”…õ5¨ôã?æÛ$==ÐR<æ9VÀ›2âyÈ!‡Ó0-ضè2 .‘šNt¥}ÕUWµÌƒýë¶Ûn+>cþÌ›®¸âŠâszÒˆÔÄþN·õiE Ž—^ziÞ:˜`tl»áQ sla?ˆïï»ï¾(^>¤GÖQº cµXŽ ºÝWo½õÖ¢,Ç*Zü“ø …}º<ôF0j‰c£b}0¤8ûb¹’ûMÚ:¼êJ/*äÁo¾ü†ð[ÂïDú›öè£ÖR2>ݾ³ðÇÓ±èd8,¿Gµ ×ãÏ>ûlË:ŠõEo&P@P@P@P@P@É)0°:gÀ忸¹\õ tºrŽï FV%‚fé³™-ZT5ZËgÝЙB”ƒ!Á†x6udL0; à¼ÿþûñU#Ã4HÅs㫺äeF £¬¢Ëé±Ç+¾'HR®ppà 7ß—[±—óêå=Aæ(CžåD—æi§*øSž†÷Ý嘆yì®J¬Ó(+AÐrJ—…nõ«A¥ÈƒÀp9Ýu×]Å÷t‘^•bÝdŠÇ̇nüù£õ=­'Ó@,û*n㑇ßXô(ÏgœQLC÷ä唨·lÙRþºï÷iЙ }¤ôQ?þx|\ôXpÎ9çŸÅ:gÙ{ÝßÓÞ2Î<óÌÝŽôØ‘v‡^ SŽ3á_ GaÓåD=m­Ìvøå—_Ƭ[†i9o»Í&`ë#íöþüùÅç¬ëHñZÕ½>>t¥^•6mÚTäG²NSC:  “çDÿuº\ÝŒ—þnĺbøÉ'Ÿt“ã* € ( € ( € ( € ( € ‘ÀÀèéäª×UôÚѪ­èMÍèæ=ò¤UÝX©Ÿ:]×¥‡z¨(GÓ]\§Ý÷¦­”Ëe!(’V( Ûí4¥ ÜʉVaI«Ù¦Óu×]WäÇwÔfOP;‚1´¬ï$õ@'_ZÚ.Y²$ûÏþ“wÞyLZS#,ªÊJ Øøó+Vä-8Ëe¥'€ª<ãñyäA«`‚èT )§åË—gMWÊ(Ï#¼£ "­8]ßsL/'*H¥ût—‡GqDyÒÚ÷q é%€>¿Gµ ÒçT®¢Bé¨$õçŸö™£“+ € ( € ( € ( € ( € L¤ÀÀèuDÂܰ/ÐÓPù†~»÷öÆJi€y¬qù>í½®»ïÈ'žß]°ˆqºÒ*9–9íž·.Ÿ´ YZ¦)  ×uÃ=È:Á/–¥IŸX¾nè´ŒÀaز Òeù¼yó²ÓO?=ÔÅwutž¿œ>§<Æ/ TÕõ@«ÿôùöåiã=]×µ$ƒ~†üâ™áU ³4˜I xЩŸ:•ªRÚõþDЩ‘¦r½‰ýG ÄvÃ3™Û%ºèfÜa  Ïš5«X–X¦N†<›{ÔR'ô¯¾úªXl‚äX•·TLJ ÙWèƒcÇÀ´“ñ  OÄïQå P@P@P@P@P@Ú M=íZ˜ÀáK/½Ôöïå—_Ο§=V«f–½ŸúXÏ1}´„o³Z¾Jƒ&åŠ-#þïÍ=÷ÜSGh—¦‰ G°àͯ¿þš­ï×ÝÐÓ. ÐU=ŸœE©.€…f»{ä‘Gò.àéæŸ 4­Ó£2ùD 㘦<¤×Z Ój‘@5ÏOYäQ÷¬õr^½¼+€NžT“¦SPUæ©@obO+ý<ðÀU´ÅgtÏ:+€NWêU)} Á žÎ¾ÛÏŽë·àµ×^˨$1Š)Ý6Ò.ÜÓècÐÓçÚS™ƒªRTn3€^¥ãg ( € ( € ( € ( € ( €SI`hètWA“º®˜{]1ýÐ/¿üòÚÙ¾òÊ+E™ ~6™¢«{²uÁÞ˜_>ðK»óåû‰ _}õÕ…Q9¸åO‡w: ´w@§kòؾêZ†ùå—Å8Utz# ÇòªKiÀ¹Üò˜iÞ{ï½¼õy»Ö²Ê¢¬u­ÃëæßÍçÐÓgk{ì±ÝdßÓ¸S1€T¿ûûš5kŠm†J+u‰õ•<ªèôxÛûnUâQ1Î è¾'AÐÓýŒcZ»Çh¬_¿>¯ÐBE§N+Þt½QNàMЗ/_^O.¾øâÝ–†JtçÎúäÏúnDm?øþûï »0d»¥R‚IP@P@P@P@P`r 4@§•4ÏRÈt»ûá‡7‘ ŒÑ.‚v<šïÓ”¶&nêd$ ÈßÇœ]sÍ5EK9Æ© òðLaºìŽ¿48ŸÅqË)í9–‡ÀÏ'ÆgËÉ÷tÝÝtâù°Ñ*5–“n{ &ðÀå”SNi)GùùÎÜØ¿òÊ+‹q¤ð ïH䕫芜àMßø'¸Ž yïû￟wþÖ[oåÏO¿g;*'–™ÖÛ±ÞÒãXGŠÊT–`ÛåFŠÇ°ý² ’šØßÓÊ2¬3ŽYTôÀ€zz<á{Ê…iºS–ðd¶EöU±±dÉ’–c/ßÓò–í>Mýî«ä•³)÷3Ï<“¼'áËû´2eYºtiZŒIÿš ¬;–?žWa½ÅçO<ñDöóÏ?çËÏ@ç;¶9*V¤•Røœß~‹ùžýœý&òbH0´ .WŽ wtä5ÇR¦a›.WîécX~šÞ0ÒãKêHå1“ ( € ( € ( € ( € ( Àäh$€ž¶’Ko óš€7÷ËŸÇ{‚ºiª ^Ǹå! òO-ÓËãõ>‚‘OZ†ã?¾m~ˆȧß!Ïõ-7ê–…›øiúôÓO+ËMÅ…Hi ,Í·én“ Òýw:º×'žxbK4ÊzÅWt4}äKkÜ4QCñ}Õ@ÛTúë?§ô»xÍ4iëßøœñË) ¶Æx ™¾i9ª^3þ º$NƒåUó­ú,íFþþûï¯\\pAÎC¯ uËÚ„+Áùª2R1”>6€ñ¨|C¢‹çt:‚þ¤~ö÷<ƒ,˨“æ]~GÙ„g¢§©\Q¤œGù},oäÑï¾J>õ£‚Ly~Uïi5M%„QJQÙ¬¼¼ŸþyO?§2‰ "éçQ'­Ü–~¯Ù&Ê,Òchú(‡˜f¬a:ý0ý5½P¹¤lç(öˆÐ´ù) € ( € ( € ( € ( €Ã*ÐH– U7û ¬Ð™‰3fÌØí&3­×hñ\N´ê=òÈ#w?nRóÌï‡~x·–“äC+õr€(¦«r“¿ÜR.  Ó‚ÖË<ã<‚Ù,Ág‚ÖÑú¸¼ M½§ÛeZâÖ-Á¸rkkæMKsÊY^æôYíUA¦¡uâ ŽaX.°v{žÝ[ž¦Ý{‚oåD Èr«Uò`¸îºëòÂiWï|G«ÏH´\Žy²½×­–…V»U)m%\µÏDþgŸ}v¶qãÆª,úþìÀ,–#æW²l¬+z[¨ê¥¡ïBdYnTgX.O¼§«óHiëÜøžüÒî÷©0ßÅ÷r«ëȳ›!džhy3¤ Òk¯½Ö2o¶Ò($ÞÏ™3§e=±Ü,3-þ·lÙÒrü‰ý‚^]Ò}Š^Hl«ô´v1dÜN8!£ZðÇç Ù^"1}9ÀžŽ[õšãL¤aü=в51äw#¶SŽ›œŸ˜P@P@P@P@P@&¯@#ôA->­†iNKU‚Q´¼ãFþ S9€žÎ¯îYäé8ƒzM—û<Ãû³Ï>«}.ú æßT¾肘õJ Þg"NWåìËÝOUº §’‰Š#l“,Ë«¯¾š/Ktß]—AA1_¶gZSÖ-Áê´ òº|ü|ôúÝßérŸc•˜Ìw›Ø¾©œÃöJ+ÛAWjW>ZûÇ1ƒ®±ÓÇ´›ÎïZ8Ö®X±"?öRqƒuljF€Š‚U•ÙšÉÝ\P@P@P@P@P@ÆS`¨è㠑Ϋ]=Ï× ( € ( € ( € ( € ( € ( € ( €£#`½b]@¯@ñ#P@P@P@P@P@P@P`Ä  '+xûöíÙìÙ³[žγÛyf2³fÍÊèþÛ¤€ ( € ( € ( € ( € ( € ( € (0zГuÊó¸§M›Ööçv›P@FS`éÒ¥ÙÓO?Ýóß’%K²ùóç÷<}Ì›çΛ²ì÷ßïÛ’u²pá¾óq¸E* € ( € ( € ( € ( €SCÀz²žwíÚ•q£ý–[n©ü{衇2Z©›P@FS`Ï=÷l[‰j¬JVM}¿`Á‚Ñîr©Ö­[7ëƒõê:érå9º ( € ( € ( € ( € (0I  OÒg±P@š¸üò˳3Î8£ç¿“N:)û¿ÿû¿ž§y/_¾¼ù…›„9þôÓO}[²Nþßÿû}çã:™„EV@P@P@P@P@z0€Þš“( € ( € ( € ( € ( € ( € ( € Œž€ôÑ[§.‘ ( € ( € ( € ( € ( € ( € ( @Ð{@sP@P@P@P@P@P@P@Ñ0€>zëÔ%R@P@P@P@P@P@P@èAÀzhN¢€ ( € ( € ( € ( € ( € ( € (0zÐGoºD ( € ( € ( € ( € ( € ( € ( €=@ïÍIP@P@P@P@P@P@P@FOÀúè­S—HP@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`ô  Þ:u‰P@P@P@P@P@P@P@z0€Þš“( € ( € ( € ( € ( € ( € ( € Œž€ôÑ[§.‘ ( € ( € ( € ( € ( € ( € ( @Ð{@sP@P@P@P@P@P@P@Ñ0€>zëÔ%R@P@P@P@P@P@P@èAÀzhN¢€ ( € ( € ( € (Q(3 IDAT € ( € ( € (0zÐGoºD ( € ( € ( € ( € ( € ( € ( €=@ïÍIP@P@P@P@P@P@P@FOÀúè­S—HP@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`ô  Þ:u‰P@P@P@P@P@P@P@z0€Þš“( € ( € ( € ( € ( € ( € ( € Œž€ôÑ[§.‘ ( € ( € ( € ( € ( € ( € ( @Ð{@sP@P@P@P@P@P@P@Ñ0€>zëÔ%R@P@P@P@P@P@P@èAÀzhN¢€ ( € ( € ( € ( € ( € ( € (0zÐGoºD ( € ( € ( € ( € ( € ( € ( €=@ïÍIP@P@P@P@P@P@P@FOÀúè­S—HP@P@P@P@P@P@P è= 9‰ ( € ( € ( € ( € ( € ( € ( Àè @½uê) € ( € ( € ( € ( € ( € ( € ô 0”ôµk×fï¾ûnËßÇÜÃâ9‰ ( € ( € ( € ( € ( € ( € ( € ]}Ñ¢EÙ´iÓ*ÿ ¢w¶RÇs¬]»vçìŠyMÔ|‹øBP@P@P@P@P@P@FN`èèÏ?ÿ|eðœ úêÕ«Gn j~þùçì†nÈæÎ[ùwùå—g·ÞzköÚk¯eÛ¶më©o¾ùf¶Ï>ûd7ÞxcÛéùå—lÁ‚óLËsÙe—e>ø`Ûi«¾d~{íµWöÊ+¯T}íg ( € ( € ( € ( € ( € ( € ( @OC@g)h]LЕ ðo¼QÔ  w¾Žxà­®E|¾Ç{d·ß~{ç™gYöí·ßfLG<òHÛi}ôѶeY¾|yÛéË_>üðÃy~Ìß^ Ê:¾W@P@P@P@P@P@P@^†2€ž.Ìš5kŠà«ôT¦ý믿þ:;æ˜còâ(ßÿý³Y³få3fÌ(\ãûûï¿¿}¦É·tPÄþý÷ß“ovùã?æe9øàƒ³™3gî6_ÊÙMÚ±cG¶çž{æù§IP@P@P@P@P@P@hB` tZ‘¯X±"ÿÛ°aCÞ²< ½}ûöxÙvØo}ýúõy×ïË–-ËV®\™ýôÓOmçWþ’r~öÙgyKx¦ß´iS>ÊÎ;3ÊÆp¬´uëÖ¼¥4Ý¥ðÁÙ¯¿þšOòÛo¿eŸ|òÉX“÷õý³Ï>[¬?ÿüó–¼X'wß}wñ=Ý¢w’è:=‚îóæÍëd’bœ¥K—ÓÌ|(K7éÚk¯-¦¥+y“ ( € ( € ( € ( € ( € ( € (Я@ãô¿ÿþ;»ë®»²éÓ§Î’òÙu×]W´Š¦{ö±R/ô?ÿü3»âŠ+*Ë@Y8à€lÕªUmgýÍ7ßd§œrÊnËÀô´~ŽÐ<×».½õÖ[Ù¡‡Z™ÇÞ{ï]|þòË/×eÑ÷çíè‘ù±Ç[”…Öâc¥Ã;¬ݺucÞòýìÙ³óii9N…„Ø6ÆzŽzK&Y–Wdˆi9äò×¾W@P@P@P@P@P@P@ºh4€NkïèÚ;‚›í†ò³z7rŽ«€ ( € ( € ( € ( € ( € ( €UÐwíÚµ[kkº÷~á…2ZXÓR›.Â#àÉð¦›nª*SËg½Ðy–we¯¼òÊŒîÛ·mÛ–}ûí·ÙÙgŸ]”á´ÓNk™W¼¡•|”óꫯÎèŠ>Ú4~ôÑGÇW-Ã4¸‹­âI‰/^Ü\¿óÎ;[¦mòÍXtZâG Ÿ}ösÖguVasýõ×9~:ÂÂ… ‹i£ûE‹ŸuÛÿ¶Ûn+¦=ñÄÓYùZP@P@P@P@P@P@èZ ±z%[Õºû«¯¾j ¢*€ŽÂæÍ›+1ôÓ…;rZ™W%Z•ó=Ý´ÿõ×_»ÂsÑ£kvÊU)‚Òu-Ô¿øâ‹¢‹ù'Ÿ|²*‹F>Kè§žzj6wîÜìÒK/ͨ<0sæÌ"Íòò|ò±Òþûï_LóñÇ5zË÷1íœ9sŠÏ ¤‡U]e„bäÒ ZùGEò6) € ( € ( € ( € ( € ( € ( €ý4@O[\¯^½º¶LtÓNº.ï¥zÌœÖâöyú™gž™]|ñÅÝÆGY)GUb¼ÌÞ}÷Ýye€r÷â¼üñdz 6Te‘E°˜ üóÏ?_Ðߺuköè£f;vì¨Ì£‰Óz,SÕ.Ô;yžy¬;ò «ûNÁö˜o9PϺ‰ïê<ëæÓխ˺éü\P@P@P@P@P@P@( 4@Àj'ÏÅ^¹revÇwd[¶l)—g·÷½Ð_|ñÅâÙÚ`­î6Ã,˼WÏ2î½÷ÞùsÞiÅMe€º4þüÚ–¯<¼êª«*³ «xZ²GÅ‚òtéûnŸý]9ÚÓ:]žW¥÷ß¿('ÝÛ×%zˆrwRQ"ò¡|ZâǴ톬¯rkÿȧj˜V âƒIP@P@P@P@P@P@èU >‚ÜEŽ‹#PÜô³¨»  G×éi ‚S¶r¢•sqËß•ß4¾öÚkó.àO>ùäŒ cz†ížþçŸfÈã¹ã'tRvÔQGµç .W•³\–^Þw@'_*Ä2ÕµŠã7Šq:è Ž‹Cö‘7vUÎcœn*vØaÅt/¼ðBÇerDP@P@P@P@P@P@P ,ÐHLÓ–ÀŸ~úiy>-ïi‘L7î ÇJÝÐ VG–`w]"ˆãU³xñâlæÌ™Ù×_]õuþÙÙgŸ]äÁøåtÆgd”aûöíå¯ò÷´²Nñ_|ñEåxý~Øiý¬³Î*–§®kúï¾û®‡®Ó;MG}t>Ý~ûíW;IÚ‹ãwšRÃ>ú¨ÓÉOP@P@P@P@P@P@ØM ±úm·ÝVW >ÿöÛo»ÍŒx>y´6æ9èc¥nèüñGQ†º–ð<ð@1Aôªtøá‡çãÐù/¿üR5J¶lÙ²"že^N '0]×%ù¼yóŠˆÔI}ýúõE‹øv­ái%ËUgW^†Ÿ~ú©˜¦Ê)ÿÄO,ÆÝ°aCúUíëèù€òÔmsµû… ( € ( € ( € ( € ( € ( € (TG“:}I˜ÀyX ’/Y²$[·n]öÕW_et¯v·ÍxwËéûï¿Ïè*<þî»ï¾"Ï{î¹§øœïW¬X±[+ö´E2-À—.]š„§,iù¢œ<7û³Ï>k)FZN–ƒ gi5Ï|Ó®âßyç–éyù3¤u>òhqÿ믿fÏ=÷\Ñí=Aຖê»eÜáÌ‹y^}õÕEY~øáÂeâýùçŸßò|òsÎ9§íhEËöóÏ?·—ësçÎ-Æì±Ç2ž·¾cÇŽ–é~ÿý÷¼¬i×úçw^V÷Ìö˜˜é¢,þM ( € ( € ( € ( € ( € ( € ( @?Ð)Ä7ß|ÓÒ-y7«†´X/§-[¶Ѫiª>»÷Þ{[²I[\WÏg´,/—v;-ÐËã¤-vì±Ç¶Ì?ޔNj÷{íµ×ny,\¸0&klxË-·ì6Ÿ(CÝpöìÙE¿® Çw\‘ï“O>Y7ZÞý}Ý|æÌ™Ó2Ý¡‡ZäYž&*.´Lð¿7Tˆñ©¤`R@P@P@P@P@P@P@úh4€NAhùLKâ´kír2<ýôÓ³ºgU3íŒ3Š h:]ÝëªVì=ôPK«ê˜– +- é< †ÔþᇠGž_Î4ŒÃ³¾cútÈw ,ÈxîzUŠé¦óJó µü3ÏÈ»‰§+óvØò´t N×îÑúyëÖ­ÙªU«òçž¿þúëÙ_|1fKmò+W®,žÎ{ºzõÕW³·ß~;ÆgŠO¶„c´¢'ÞIÐ}ËHe‹¨¤ÁufR@P@P@P@P@P@P@úh½Ÿ‚9íð ð,úh)^ÕÀx”œŠQ†yóæÇ,‡ ( € ( € ( € ( € ( € ( € Œ¸€ô_ÁƒX¼´:Ý¿OD:âˆ#ŠúÆ'¢ÎSP@P@P@P@P@P@FLÀúˆ­ÐñZœÅ‹ì÷Þ{o¼f›Ïç“O>)æ}çwŽë¼™ ( € ( € ( € ( € ( € ( € Œ®€ôÑ]·_²ÓN;-dÏ;wàóJgpÓM7åó={vú±¯P@P@P@P@P@P@P@¾  ÷Å7µ'Þ¹sgöÜsÏeï¿ÿþ¸B¬Y³&{úé§³]»vë|™ ( € ( € ( € ( € ( € ( € Œ¶€ôÑ^¿. ( € ( € ( € ( € ( € ( € ( @‡Ð;„r4P@P@P@P@P@P@P@Ñ0€>Úë×¥S@P@P@P@P@P@P@èPÀz‡Pަ€ ( € ( € ( € ( € ( € ( € (0ÚÐG{ýºt ( € ( € ( € ( € ( € ( € ( € @ïÊÑP@P@P@P@P@P@P@F[`èè›6mʦM›–͘1c´×„K§€ ( € ( € ( € ( € ( € ( € (0¡C@_¿~}@Ÿ>}ú„B9sP@P@P@P@P@P@P@Ñ0€>Úë×¥S@P@P@P@P@P@P@èPÀz‡Pަ€ ( € ( € ( € ( € ( € ( € (0ÚÐ'ñúݵk×$.½E&·¥aZ–EP@P@P@P@P@P`¢  @þÅ_ÌæÎ[ùwÙe—e×\sMöÈ#dëÖ­ëiîýõWvÎ9çd{ì±GÆënÒÇœQ†Ë/¿<Û¼ys7“ã>÷Üsù²ÝtÓMÅg¾˜¼7Þxc¶×^{e¯¼òÊä]K®€ ( € ( € ( € ( € ( € ( @Ð@,gA0rÚ´iýzè¡Ù—_~Y΢íû‹.º(Ï{ï½÷n;^Õ—‹/.ʵfÍšªQÆüì„NÈó €ošü?üp±>©`aR@P@P@P@P@P@P`ª @Àš¿å–[²Ã;¬Thž5kVþ7sæÌ¼åx`ßgŸ}²_~ù¥£’¼üòËE¾7ß|sGÓ¤#½ð Åô6lH¿êøuð)·iò ìØ±#ÛsÏ=óíâàƒžü ä( € ( € ( € ( € ( € ( € (У€ôá:™lß}÷̓’´ØNÝ®¯X±"#˜tZ†w’<ðÀbš­[·Ž9ɇ~˜tÒIÙòåËóqß{ï½bzʱsçÎì™gžÉ(c]~>úhvþùç]Î/X° σJ¤_ý5»óÎ;³ /¼pÌò41Ïëþâ‹/²wß}7{õÕW3–ñ·ß~k"ëÜà7ÞÈ~üñÇ1ó£Ò­øŸ¿ï¾û.ëæYâ?ýôS¶lÙ²ŒVßét,ÛK/½”}ýõ×c–aûöíÙgŸ}–Ïåʕ٦M›òiX¯”a'éÚk¯-¶‹7ß|³“IGP@P@P@P@P@P@‘0€>ÀUZ@Y®]»¶ZžuÖYñqíiÜçÌ™S;^úÏJi(ÏUW]U¼çæiwó<ð@:iñzÆŒÅ4‡~xƼɓüÎ=÷Ü–õiwôëׯÏèf>æ?}úô< ûüóÏgÇ|F v¾Ûÿý³SO=5ˆ3­xñý÷ßç•hÑy¦CÊUתžr1ÿt|ÊOúý÷ß3Èp@Ë÷Tp¨zÆ<]žï·ß~-ãF¾´ä~üñÇw+}˜1åŸ={vËôäGÀ<¶™ÈñþøãÝòûæ›o²SN9¥%˜†2D‹ržußI"ðÓrÈ!Lâ8 ( € ( € ( € ( € ( € ( € Œœ€ô®Ò†–[ Ç, ˜FÐòì³ÏŽk‡iÐõÙgŸ­/ýâØc-æóªP/§?ÿü³%È^7m|NKìHëÖ­ÛmÞi 9¦I‡|«‚ÖäIð>·ê5èªV׫V­ªœ–uV(çùØcÅâäôCyÜô}yý´›G:]Õë|°¥ ÔË|ò¯ªXpÌ1Ç´LÛî Ý·Çüm…ÞNÊïP@P@P@P@P@P@FUÀú×l»:AbºV€åO<Ѷ$´¾ŽqÒjºÓ´yóæŒgŸœNó àzã7f¯¿þú˜] õÕW]¹S ̓@îÝwß­^½º2ðýé§ŸfñÌôt:lh½~É%—dià–q®¼òÊÊE£Õ:ßÓRüÖ[oÍŠãðÁ´äA9«Òû￟wNkÿ(KÚBþÈ#Ì-Z”]pÁy nZqÓzšèni±;óÌ3óîïéîã{ï½·%ßt:ºwî¹çŠïɃnûYïå–ñ<òHþ]Ä;ì°4«ìºë®+ò¹úê«3º’Dà; Ö}ôÑñÕ˜ÃÛn»­È÷ÄOs|GP@P@P@P@P@P@P`Ô  pF®ÊçΛÿñ,ñãŽ;®èb›@*-«y–u»ôâ‹/ÁÍnZ§y,Àq y†x§‰ ¹8]ÀÜo—hÇüüþûï¿[&aù"`Ì8»Ë‰ç„ÿüóÏåó÷iäìÛ%×iyXOå@9­Ø«Z²“/ÏŠ¯k%O…€È›gç‰`|wË-·_ÍŸ?¿øœ |¤ÓN;-ÿœ€xšXÿäC€¿ªlK‡zh>N'ˆ¼?ÿüó¢t«oR@P@P@P@P@P@P`ª @àzMë†7ÜpCm°6Šw×]wÁÍ+®¸">îxÈs¹cþñ:è ü=AkZUw’Ò–Û´ÔŽ€7˹mÛ¶Ú,Ò:Ï:¯K=ôPQƺ.í龜g¨ß~ûíÙÅ_œQ¦Ë.»,»ÿþû‹J ÿú׿êf‘žÐiÍ^t·ø_Ò==]«Óœ2`J…ÔèÃ?lÉ*  ?õÔSÅwt{ë&í2þúë¯Ï?'Pž&–;Ƨõ?­ÛËxÏ:¯{&|š_ú:òeÝšP@P@P@P@P@P@˜jиÆ#€Îðæ›oÎÿxÎ8­Ñé¾Àh, hïØ±£¶4hcÜ… ÖŽWõEt;ÎôÌ“.¿éö<ò£y»y“gtŽ.Å ÞF<Ÿ½.¥ôrP¹+¦¡¥>Ÿ—ƒÙéø1]ŒG—ôlK—^zi¶víÚ"¯N_¤]Úoܸ±ÓÉOP@P@P@P@P@P@‘0€>ÀÕtž^•è"üä“O.‚§K—.­-ÿ쨣Ž*ÆãYÚݤõë×g<«œ +­¦#sÎ9ùg<'»ª+ð!AÛ^ó\såe\°`A:zËë4€þÛo¿µ|W~C×á N¿£Ëó4XLËqZ{Ï›7/oýMÀ=¾ï&€žÎc¬×tUQÆ9sæäÏqçùò<£ÊQŽ^èR<“¼@ç{ž“žÎ+æY^uÕU‘]GÃY³fåg›P@P@P@P@P@P@˜Jи¶#¸\@gÖt¿AϺnËïàƒ.Æëæ¹å±xt}NàçˆG¢ÛõåË—ÇÛ1‡tþî»ï¶Œ÷õ×_gkÖ¬iù¬ü&  òÉ'å¯[ÞG€šçÆG¢ìaDÐxõêÕñUË0žû=¨zú|sz¨jµ¿bÅŠ¢¬ƒ  Ç‚cqíµ×f<;Ê<=¶»0ë´‹~ò¤w˜î…^ˆÙ8T@P@P@P@P@P@P`J@àjŽ@f»úO?ýT,=öØÚÒœvÚiÅx¤'SJè´Ô®K¯¼òJ±ŒÇw\1ÚÊ•+‹ÏyNzU¢bÀôéÓóñ@Vî̧.ÑÂ?Ѓ  Ó=e¡òB]¢2F”ƒñ;M±Í2íG}Ôédާ€ ( € ( € ( € ( € ( € ( ÀH@àjŒ`d»úõ×__:ï½÷ÞÚÒÐEzDéÎ|2¥4€Î2Tµl¦ezÚ%9]”G¢•|,ûÅ_CºŸ§;÷gÐtæóã?óÌ£=ã *€~øá‡çËJ7ö<Ͼ*-[¶¬ðàYê¦x=å«»ýNót<P@P@P@P@P@P@&‹€ô¬)ž•ýÖ[oaº_ã7ò?›O=õTvÇwd‡rHä$`ùÍ7ßÔ–†îׇ?ºŸL©@gxîúí·ßžÝyçÙgœQ,ßÑxš6oÞÜòý•W^™Ñ*5Ž{ï½wË÷Ò–-[ò縳.¼ðÂbüX/1¤ ö?ÿü3&kžwÞyÅt<+œ Âbb IDATÿÚµk³_|1þy¬Ÿ.\¸0ûàƒòçËÿúë¯ùúïxv{tgϳÆãó'žx"ûùçŸóùÆ3ÐùŽe¥+{RÚÍ:­á™»©LÀ2ijä™öwÞiYŽº7¿ÿþ{Q*˜P@P@P@P@P@P@˜jÐ^ãt%ž¶âÀh»!ãðm—Ö­[W7Ó烷›fX¾KèÇ|±U&T6Hß± T¨?>×xÏ0 —¿KÇ+¿¦µU¢Ëô±Ömtóžæyß}÷íö\òøþóÏ?Ï+Ä{†T. Ý}÷Ý-Ë3þüüóhžNÃë´õ{|×î±åe$ÓQAÀ¤€ ( € ( € ( € ( € ( € ( ÀT0€>€5~àÈH–‡bi9MëkZ1•hYœvqþå—_Ž5ÉÐ|ŸÐiEýì³Ïf<ã™OÆ|ã;ÊG—õsæÌ)>‹ïxÿðíãÓÀü%—\’çóᇶ|¾dÉ’üóh±ÏôayÆï¨Pך¾jùN9唢Œí'P5­Ÿ) € ( € ( € ( € ( € ( € (0 Ð'ÑZ$¨Rº.Ÿ,©@O˽sçÎô혯 6ÓE9]Úúé§Ùßÿ=æ4MÀ<™7qº~¯{yÓóüèjnߣÛö­[·f«V­Êx<À믿ž}ñÅy·ñ1~'C*hDŸ!ó0) € ( € ( € ( € ( € ( € (0Õ  O¢5N+ìh…N+èÉ’ÚÐ'Ë2Œz9_~ùå¢rÏg7) € ( € ( € ( € ( € ( € (0  O²µ~Ï=÷ÎåË—OŠÒ@þÕtÄGÛÕÆ‡¿À–PP@P@P@P@P@P@@ê ³¤z<÷š ç0§íÛ·g³gÏ.ZÍÓýüŒ3ògˆÓ‚~Ö¬YÙ÷ß?Ì‹0%ÊöÉ'ŸÁó;ï¼sJ,³ ©€ ( € ( € ( € ( € ( € ( @•€ô*•!ÿlíÚµÅóªã9ØÃXäÕ«WÙxv{yȳÌM+pÓM7åë‰Ê&P@P@P@P@P@P@¦²€ôIºö¿þúëlñâÅC]ú]»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ÀºÛ€ ( € ( € ( € ( € ( € ( € ( €ÿ°º[‚ ( € ( € ( € ( € ( € ( € ( €“¡ú¦M›²iÓ¦e3fÌp…) € ( € ( € ( € ( € ( € ( € (00¡o¾~ýú<€>}úô!˜± ( € ( € ( € ( € ( € ( € ( €ÐÝ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@ÆCÀúx(;P@P@P@P@P@P@P@¡0€>ô«È* € ( € ( € ( € ( € ( € ( € Œ‡€ôñPv ( € ( € ( € ( € ( € ( € ( €C/`}èW‘T@P@P@P@P@P@P@èã¡ì<P@P@P@P@P@P@P@†^ÀúЯ" ¨€ ( € ( € ( € ( € ( € ( € (0ÐÇCÙy( € ( € ( € ( € ( € ( € ( € ½€ô¡_EPP@P@P@P@P@P@P`<  ‡²óP@P@P@P@P@P@P@zèC¿Š,  ( € ( € ( € ( € ( € ( € ( À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@ÆCÀúx(;P@P@P@P@P@P@P@¡0€>ô«È* € ( € ( € ( € ( € ( € ( € Œ‡€ôñPv ( € ( € ( € ( € ( € ( € ( €C/`}èW‘T@P@P@P@P@P@P@¡ oÞ¼9Ûk¯½²™3gއ‡óP@P@P@P@P@P@P@˜¢C@Ÿ¢ëÅÅV@P@P@P@P@P@P@gèã îìP@P@P@P@P@P@P@†SÀúp®K¥€ ( € ( € ( € ( € ( € ( € (0ÎÐÇÜÙ) € ( € ( € ( € ( € ( € ( € §€ôá\/–JP@P@P@P@P@P@P`œ†>€¾iÓ¦lÚ´iÙŒ3Æ™ÆÙ) € ( € ( € ( € ( € ( € ( € L%¡ ¯_¿> OŸ>}*­—UP@P@P@P@P@P@P`œ  3¸³S@P@Ñøá‡²íÛ·Þ‚¹D (0²¿ýö[¶aÆ‘]>LP@&NÀ룉³wÎ ( € (ÐŒ€ôfÍEP@¦¨À¬Y³òÞröÚk¯lçÎSTÁÅV@:ï¾û.{÷Ýw[þÞ{ï½ì?þ¨›dàŸ/]º4?nñ¨¬E‹ |~Î@P@¦Ž€×GSg]»¤ ( € Œ²€ôQ^».[W»víêj|GV@PPñ÷ÓO?‰¢€ ëÖ­+ŽqœˆáÝwß]Œ7Þ/®¿þú¢\çœsÎxÏÞù) € ( @À(Ü›Šs†“õúhÖCÍ&æÇ ( € (С€ô¡mtþú믌‡{ì±GÆkÓÔxþùç³¹sçVþ]~ùåÙm·Ý–=õÔSÙûï¿?uP†hIß~ûílß}÷ÍþýïQ©,Ê(üüóÏÙÌ™3³ýë_ÙßÿÝ×"qc%½ADÞS!­Y³&ã8Yw 7o^¶páÂlÅŠÙï¿ÿ>H†j=~ÏêØ¸qc~Ž™'âõ-·ÜÒUAO9å”ì€Ⱦþú뮦«™yG9Î?ÿüªQ†ú³{î¹'›1cFöâ‹/u9-ÜÔà\àŽ;î¨ýM¼òÊ+3¶Y®=𨧆j³Ké1£YOskV`Ó¦MÙ…^˜ÿÆs_ŠßçéÓ§g'Ÿ|rv饗毯¹æš¶3mò¡íŒ:ør®è%ˆuqüñÇgþùgKí( ( € (0ŠÐGq­ºL] \tÑEùÊÞ{ïÝÕtŽ<ùöÜsÏâæqÜD®î¿ÿþÙsÏ=7ùz-Á9Ö‡ûæ$Zi“¤¨¯¼òJ±ïól¾~ÛêÁœ]rÉ%ýf5i¦Ÿ={vaXw܌Ϲù4þ|»·ǵëñs±;œÏ§‚ qs¼›úŽ;Š}nÉ’%ε~´>ú(;ì°ÃòŠDTt™léÀÌ=N?ýôÉVtË;‚Ÿ|òI±Æo_»áÑGQÍ4~3ÆÏÚ9u'ðÈ#çíŽT,¯KMŸ#Ôͧ›ÏGáúˆJ ¬*2˜P@P`j @ŸšëÝ¥þŸÀË/¿\Üì¸ùæ›u™b7Þxcv衇歜ãb•ÖL<¯‹Ö©nãóD¿„–­¸ó\i“M ¤ÏþýòË/›ÌzÊäE+º£Ž:*¯8ÇGöUŽŸüq, a|OËÓøxüç^çûF7tzrˆ}éèuÖ#3-ññ8ñÄGf™\É+@/fX¸®ˆ€ Ûgü&¼å72öa†¾úê«É»Ð“¬ä3&Ù ›"Å}ýõ׋ãÇ„k¯½6[¶lYÆ}*îUÄùÇŒv×Äž# fƒ¡GÂ8n¿ù曃™‰¹* € ( ÀP @êÕóOáè2híÚµ]r¾öÚkÙ§Ÿ~ÚX7Bß}÷]ÆÉ -cÆJëׯÏV¯^ŸÔ¯\¹²ëgÑeÝK/½”}ÿý÷ŬvîÜYäÙɳ‘¶oßž}öÙgÙo¼‘Qº»"‘5ùvš¢&:'Å[·nít²|¼_~ù%ŸåàÇ^Ÿ‘´yóæŒ‹'ž“ÙM7òMZtµð#oÛ¶-[µjU¾~ HõÛ-rÌ¢›í3¦évøÞ{ïF¬‡4±]¤Ï å"÷ºÄ6ðÅ_dï¾ûnöꫯf~øaGûV]~éç”…míÇL?®|ÝïöɾÈÅûÇܲ]³lìÃv?‰ãÒâ—ý•rušš Mæí³S¯É6Çn~{Øç6lØÐUñ9N~ûí·y÷à켎c¿¿þúkÛü†-€N«ZƒrÜØ²eKÛ²·û’cÓ;3ÿ¦Žg·éqcéꫯn)ëéñÇo¹ùÇyL]•ã'çS¬–5íò‘mž¢W;ù­÷øY·¥LþÏã†ød  7qŽÐïþÞd0¬Ÿß£¦¶Æ~¯±ÒrpÞµ|ùòüÚ$ý|¬×œgr|âZóƒ>(~K9¦ÑÂz<Ó Ö Û¿µ\¯ts½Øír/X° ¸®ˆs“ȃ€yÚƒ ÷vÉ{­:ýœÏû1c¼¶ÏVÑfÞ±?±oq^Ï}nÒ°\³Æ¾ÆcAXºïDO0œGs^Àý¾rúüóϳ´×¼ò÷ñ~Ðô‰Ü>{ýM ›~†\SÆ9½™P@P`ê @oxsó#mµJ t.(h©Å³söÙgŸü™î O=õÔüfy»"p6Nªãu:¤†*7Þ«Ò3Ï<Ór²ÍtgŸ}v>*Á†³Î:«¥¬|_Õý,W\qEKmú´ \Œr#"M\Ñú,Æã¤Ÿ–jñž!óçærœÆwu7¿ù曌ç:Åxéüã‚®¢:Ió"9sæt2I>ÎÃ?œí·ß~Å´‘CÊ@° *qÓè ƒZÆåBêÁ¬Ý6h‰QW)¢ßí“ýŠíŽ õnþª¸íè±Àl¯áÁøåD¥“N:i·í9¦a« Rá m­Â4ÑEÃìçqÓ'ò£…|U¦—퓲E¾lé=>gå•2Åx ¯îæŽ9昖ñcZŽ‘Ï>ûl™0Ïòró€·ì#LC™x_þ£Â Û`]šÈí³®Lù9AcLc=2d}ë­·òý4Ž¡˜óÅM_¶õô¸qÄG´,½÷ÞÛ²Íuüâ&üe—]Ö2MlL{ÓM7µ[f–eyåž­–5¦g¿¿TŽ*'ŽT¾¢R Ï)éèU¢¼mQ«nÛ&ß÷ß¿Å%òb›ï4Qž»ï¾»(säÁåã˜SõK_YfŽ?<Ï=̓×TB©ZŽ&Ÿ,oÌ·@‹'Ÿ|²§ª·—É|ü¼óÎ;[¶gö¶å0á5Ç´8žÅçì'«’ÇÏ*•ñýŒcU+Ygì“i¥”ò±‘qÎ<óÌŽ ÛHÝ9ošÛÇ$ºYí‡ç)—[ÛªŽi^§vZËöI~”…sâºÔô9B?û;,{\S~øá»Yð}ÝþËÙïïQäÓϰ—k¬˜ß¿ÿýïb]²ï»ï¾üú¡üûˆSÕykäÃó¹±}¥Ãø]å3®e"Ýu×]Åüc|ÊñÐCÅ(ÅsÈ4Ÿ¿|ÍÖÏ:áZ5ö«Èÿ±ÇËËÀuÂqÇ×rîÀ¸\‹D"0Ë>ÖÍ5×VUçâíèÌiÒãK9ÈÎ8Þˆ5“åçú½^oË1£ßíó‰}uä‘GûûÇ–ªózÎ…ê®{c †åš•ûQåëì8†p?¤îQ M3Ø÷c^Ü+¬KqÝ‚y95qŽÐÔöÙÏõQ“¿iQîÍı–k'z¬a}Þÿý-Û1ÇìvéüóÏ/Ö¿&P@˜ZÐ^ßtâ$8†é§ø,ró êâ››åétñšºªÄM¹'†Üd¢%h»|Ë7œþóŸÿì–Oä—Óé¸`J¿ëö5­\ÓD h®ê–ùtÀHƒ„uÁ¼´ ¼Nƒîí–©*?.üÛMSþ.½©“–£W‹G}´rþ\0–Çò 8ÊÇvKãqËå.¿gZ²”S¿Û'7îÊóêä=û@9qñÓ–[ Ç¸¬ÓgñâÅñq1$ßø¾nÈ6Lજ¨P5 7Ó@Yyœ¸1ùõº}¶›Gyžå÷l×å„Oy¼ª÷<ÿ±|ì+W$¨š.ýŒnÕªÒDoŸUeš¨Ï¨9O cܺ!ë°œÆ  3þyçW”«|Ýë=€Dû9Ÿ¦cF¿Ûç?"û*­T[·OÄç.«®{Y‚a¸fåžFùº‚²—‡|VUù³‰cF@ç¸÷YÊk™ßu*'•{qjê¡©í³Ÿë£¦~Ó°»îºëŠãkl鰼޹/Ø.4é9äv£ú ( € Œ €ô¬Tº½è¢‹Š“¬8Ùâ"‚Ù´ò¦ûŸøœ!-Iʉ ޏHáÏ;äæÕ´$L´H.'ºråâ„Ùª@''ޏYKWÊ­Ó¼¨¡ÉwÌrÒ­35niÅžÞ8æFJšè𪫮*–“›/¼ðB–>Gˆ|ùœ+5_yÏ­Ó”žsƒ>mEÌ mz£‡@ÐX‰àLÌ‹aÚª¨Ý´,ãcGK#n8r“•Úí´ÄŒ<Ó2‘>”•“î!Áº ¦k|*SÄwUyW¯Ìÿ©§ž*òg¾lGuµÃ¹™Ã÷Qk—aúœ¾´G¶*fDKG¶ Z¶Ç²à¢w†4õ»}v{Áe© Þt@§eOäÁórŠ}ˆ ·ÞzkÞ:žíŠVPéþ^5-yq“—®ÑÓ y¶˜'5þ-Z”]pÁyÀÓò ·^·Onª¤‹y1ä¸ðÄO´gøw¾‹ OzÈHSjÉølÓ?üðC> ½eM]ì÷iªk–-}]ÕÒ‰ü&zûL—i¢_Óê*5ÆõÂ>šnc17ƒãF;7v¸YörcÇuzY‰õZÕ “naã÷Œùp£‚V„ä­¡Ó^/x]¾é–Þ¼`ˆ2âËo ŸÅ2°}–7õãûN†UÁÍÈ“\´c¹ù‹€¿Ec¥r%(* ð;ÍMbŽT¨)ß”£r^š8®ð<ñt9ØwX¸¦-+‡cršš<~’o”£.€ÎyQŒS>W`úÉ|ü¤¢ çE±ý³œ_|qÞR3ݦùœãë:=gIo–züü;ÝL'ü5¿©×\sM±í²G¢å0ç=ìÿñ{þÜiŠßбèu7£c*9¦·  sÎÇìô8Ð.€Þä9>½îï#í¼ÜUï·|ìdþMüuºžÇ¯×k,òåq\?•+‘±Ír,á·;¶M|ªŽ½ä“ž£²m°m“ø}NÏ÷ȃ7"ñûŸþÝ~ûíyy¸N­Jü¶‘¬+*öE M¬¶Î˜Gl'© ÇhÎèù%ZÛSþHu¢¼uCÖa9u@O+Ø—+ zàÑtîözs˜ŽýnŸÿˆLì+ηi9žîìg+hÙËþÇoa|_wÝ; ׬\¯G9Yöå¸ÇÄ1‰ßç8–0^Úk¡©cFZ±ÇÆÕõ.Yµæ›:Ghjûìçú¨©ß´òöɱ–ßEîÁ¦Û&ë”õN,u¿[aÎyUº-Ð{ IP@¦Ž€ô­ërËNй‘&NxÓ±ªVëNèÒf.VÚ%‚†qÀàE9_N Ó¹i~Ü$©JŒ/ZÓ•ؘ/âHi·ôi—çÌ)·`Œ`'¹U7qŠ›! ÇJ©yw“¸ ª*ypbËËMõª”–«‚ i€½êY¸ýZ„ñ±ÇÛRÓmu"_§¿E\¤û8å" ë­®§DÆ›ÈkVŽkQF–¡. šÞËb¼òq¤‰cFzMeb{çÑ…4œà¾WêRÓçMnŸÝ\Åòõó›Æïiü¦±¾¨pœ&Ž4h gæÕiJïW±NL ( € (0u  h]§7Giå]—h='pñ|òò¸œ˜/Y²$£U '¡—iÙMp!¦å¹HíRzÂ×I€¹*/‚*Ì“ZXÑÚ Z.p²ZNéE7$"¥-zÒ–É<æfXš˜W,+-À•o6ðž`|ù".Í'^§˜r اÝ-œ¹¨Á“‹Ão¼±¥õ0Ï¢¬Ji½ê榱¬U•*úµà†ù³¾bþRbž‚ì\ IDATé ¶ôOúü¯´EkUëžXn.RÒ‹˜ø¼ õÐë©1ë¸ÀàFyš¸QF :NÔÚ ¹Po—"@É…c¹æl»éøŽ› UÛÍ?¾+ç•)ÒzUði£›¶r+ˆôfsÌ‹!vœøtÐAyK‡ºV‰årðŽ|ÒåñÊï¹È«zFTä•Ë-=#¯ Wµ(dZêG>UA‘~-ÒqäE¢âAÌ“!ËIŠ 'RiJ/ðµûKóݸqcšMñº—í“2Òý#•-ºù£En9¥ô´¼å×lŸuÁ.¤â`yºô}zƒ±\Þ§ôªª¦‰ÏúÙ>ÓóéÍñt{KoD·wìƒ‘Ò A,s»í"Ž{ŒÇÍĪÔk}¶Ïªå™ˆÏb›d]´KÜ4í4Ž éøý´@§»ÇÈ{¬í‚rƸåÖœì±<1N 9FñûÆ1z¹‚Uº,¼N›Æ3€ž> œJ c%бŒåVäi½î†ZìgåœM?Y†(c»!ë6~[ÊË=ÙŸ,OܘOè©MZQ(ý§Ë}’ÇÏœaèþ@ÿï*iâ!VnSû{ìsÝÚú=ŠeéwØÏ5VÌ;‚ TܬJéMÿªïÓ^Òc8¿üS¹™s>ºÿ¯Jéô`O¯_ùÝŽkÏD¥HÄ:‰P]7UËAà…Š{Ý\SpýÇð4Ï4€žš–_W=Â|¼ð_ͦÏç‡å˜ÑËö™n_ý:  ³ßT¥x¬R]Åž‰¾fåz!öG¶³v׬1Co©ÉcyRñ€ /¿iu÷Þ8Çñ4ÊÃôwº‰z7ÇÏ(CyØO½—ß4zLŒõ¿Gå2ñ>‰ØMÊX‘wݽ‹ªyù™ ( € L~èZ‡i½.೎šÿi Šèh1'j\€ø7o^vúé§·Ó‹îºúW_}UÌšà,kzÜ¢ûæ^ÖI]/½Їeû, &ðEÚ³ÄXÛ]Ú­jÓt‹ÐËvQµ¯Òª:Z ´Ë“LU­Ócu¤¿Mã@O«ººòÅ0mÅGÏiJètõZ•â÷¹@oòøÉ|ÓuÁñ“?nFÒç œKÔÝð…ã'qc¾.€N¯5‘ÒÞ]"øâñ3t†khý¿ë£‰srjr}®›z“¿Gýn©ý^cÅü#€NGU)íu«ê{ŽÍ*â÷"=ž—_—ŸL~ô¢ãÅ÷éõßQi(íNžž" bÄüyJ§)í‰'–§“aU<  Ço"e:ðÀ3*‰S‘¼®B‚÷þûhµAœÏË1£—í³Óíx<ÆKèǪRÓ‹î^èiÙ¹€k» 0±råʬê9”,gÐÙκIð}vSÞA›Þ|ä¦a»ÄºŠí¸×z¶Ëáô9ƒ¶ÇÚ.8ÞP†v=0ÐbnO Vò;@7ŸiE–…Ç}Ô¥ô·©®UtÝ´UŸwzƒ(ÝϪn*–ó¾çž{ŠõB å4õ@oòøI™bÛ©{zZîôõ¨?Y¦¸1ßkÝãgºe Ïë4€^w¬¸è¢‹ò} =k â¼+*,Ž5>ߧǺŸí75Ñ…;ÇêØÿǪd×ôþûÜXŒIñ{”æßÍë~¯±b^ýÐ#ÎÅøý%XÃ3Éi%N%Ò8§b=s>Vu-eàúˆž×—JTG}tþšGw½öÚkŶ’>žhë$ÊÜMˆ ÕÝ\O0.çU=Í¥ô±zÄ ÿzà¿t†a÷çØÏ9§¯Jì|_u®; ׬qï2ÒßX×&¿¢Âc,oÇ ~79NŽu.@%LÊÊ_ݹvSçMnŸ^…)Ãø=é%€NwüáTwÎÆ<¨xÇxÝйgysÍkR@P@©#`}@ë:  G+ߪY¥7æ¸tÓM7'huÏH¥Õ\œÄ "€Î”ÈŸE]âKŒW' Rô@çÆ2Á‘ò3`ÓùqÓ:ÊÁøí7†b\ÖU')‚3í*s¤7ƒ  7a‘ËèŠ .>ÿüóÂ… aÄMÏ4q1ßñ\Ä~S\ÄV]d÷›w'Ó÷@O/¦Ê®˜77Ùn0T½ßí3½èî5€ž:rs¤ßD þØÎºÉkX·OZ Óúˆ‹õø»÷Þ{»Y´®ÇMoз{Ĉ°®  §­c«Zv§Áúr›Ç‘7åé5±}qœjWé‰m7æÅq¥.-[¶¬ŠOý¦nnd£ŒÐê*ÇEy¢â ã§Pàû~è‘SÃ0¯»©W7ŸQ9~²|qc¾×ºÇϺ­äŸÏ©üÇN†íÎMÿ™ª¿Wi‹£º vñ›3è:ÇÞØ×¨¼ÚoïzÓû{œ÷pœì45õ{ÔéüêÆkâ+òf_`»è%Ø@ìWìKu=™`–VT&à]NÌ›2P–èB÷ /,KÄo7yÆ)¯¯A¬“&@åeíä}?tïü@c+ÛM×›ÃrÌhjû¤7*ª¤¿‹u¹;Ùn;§ßz¬‡^ï©4qÍJå¶+þèíi¢RÚCȧŸ~Z[ zñ‹òÖuÞÔ9BSÛ' ÓÍõQ,|?¿iôÞNô‚ÅïK9¥ ^˜W§‰ÇFÞT1) € ( ÀÔ0€> uÐ9Ѫêj‰`q‚Ê8,#Ñ yœ UMK0šîmcœAÐÓ–"uAº‡Š20,§¦èqÍ êºnÀÓ€Ïêk—ÒcÕø|âbå¬ zpó›òáÁûª­¿Y–ª4VîMX°n£õS iíKŠ‹–øœZÑå”^tŽÕò‡.—颛.¿ëÒd ó¼åXçUÏêâb– ÝgÐô^·Ï&nFGl7”£\S?]ÿôpÀñƒÖJu½t¤]½V ÚR ‰–ÈikåaÝ>Óî¸c{xë­·R–Æ_§on`ÿðûÍcáÂ…ÅöI¹ªèé±¾ªµIÚI9€Î ã¸Eþ´¨KÜèÀ‰ïåß>º!gz¶±ºViP‚–suiÕªUÅ2³üU‰cÇ­ºy¥Ótsƒ(myÃñ•í¸*ÑÂ4¶“ª<£@•ã'ë¯ßºÇϪ½àŸÏh±ûC o¸á†FЫ´²jU€2m=è:‹Ë~ì±ÇV.1]ãrìhWÑ(&ïzÓû;çSxpî]U±‹ó Ž“ü~¥­¦›ø= Ã^‡M\cż㼽jûdœ4@Ó¤ÃØ¦8W­ 60nZ¡±*@—VŽˆó@®Ù#ÿøœ®sË©éu××ÕÂ7½Î¬3-Ä{ïü˜jú|~XŽMmŸQ)%ÝÇÒÞb›jzØTr÷rO¥‰kÖ4ÐÊ1´Ý#Y^*¶Ð(¦ißôø<{öìÚcpÚƒG»û\±-ôsŽÐÔöÉv×ÍõQl§ýþ¦8î—Ò«ç;4IÍŒSu}å(Óu÷ÍÊãø^P@FS`÷ˆç-gœ ·«¡:dE΋S s‚Æržaʃ¢ÆÉ'ziJk8²ìL·fÍš<°Á [Ü„ˆéiQMÀvÛ¶mE6t™Í #Ñe9ðø,† êRÚâ€Ö Ü ¡K–,Ù­»\ÊÂÄh%·aÆ,½ð}þùçó“WæÅ³ßŸå Ü=þ?{oömGqç{ú¨×ûä'ÿ^‹—î—z¨v=\¯»ê®å¾Õ×·Ý×vµ«Û¾åj—«\v¹|mWy*cŒm °ÌŒÌ`̌Š“˜„„H$4#! „Nöú$ü6¿'sŸ=ä>ÚGúÄZçdî̌鑑™ñø.Ö&çe—xÂåax °Ä‡fŒ$Í3-Ÿxâ‰ðÚ¸ÅäV°CÀÆÅþ0q΀D=ÒÂúç^le`zt  ÄD'/ì¤3òA‘͸b‚˜{ »®X`ñ ÒÊ–8xäãyÝÖHå•ëi¢ È ùÄj3k™m›ÃÊyí¢~FzÆÝ¾ùæ›õýDDÛ¸7ÙŸ;]›âÉÐä3øt$R¯vã#08PO{챊û#â/÷ñ"Ƶ‘ŽØÂ8î•ðÛIê'ÄñÒQ¢v°V°ÿþ:ÊX?ä5¬„EŽsó‘O} ^Pþ„G'mnÃÚ>B³…î9î'Úû»¿ë€”;fµ~fá>äiÚŽravJ”-ÜcVís<â<[ʨt”q\CÇ åŒ°Äñlуkè ¤ýÎÆY„âêí!eÅËn`â•ö=â)sÎ9½s\Cû˜-›Ðóœ ÿ\ßæ²G~¨k˜…G´§œrûV ñ âŠû5îÍOúÓu¼„Çb›Ÿe¤‡Y~1 ´’OÂãþày—/ù˽|p }Ù!JÄ’œG˜Ê³³+w²9¢ÇBíYŽc¡}â`Hn; ù¦½Šv¡-¬S¡ý¤þ2X(Úú¿üË¿¬ï‹xöG}¤£;–2Ék óŒ…Îö³­¦Tõ»V°dK[–ë|»ÏÉÎPŽùyE»É=J{ƒz"]¹ÜwåL^Ú›¸Wâþ?<ËâXlå+¿W]uÕUu»EûÁ¬_–ȈpIsvÜû¤9â`›ß…l•Ïf Xíê¡‹û=ç 1#òûõ¯½bf4ï <"âÛ,úvñ<Êéw??gFýÆ"N–’"¿1x—<“ÏòÙ›-tQ®{÷îíKræÄ{ aDûEÙSOâ`[Öo£~E:"<ž¸œOÎeSÿ‘IË„øi‡£Gzy?‰c±dÍ,Ò3î–¶xò{–+"nò¹³àÃ>€.Þç3ï“ÕfL«~æ¾î­Q­e6ÃîóÍJŸNÜç¼kGßaÐvðÞ=¼Ëû`^únV¾YówÞËo¿ýö^ûÈóŠßôßE^ÙÒÖ¥»îºëú§O)÷}M¹}m³Nɵã¼#tU?'ý>êê™Æ»v<rÙ5í" Ó_a  Þeý0, H@€fƒ€ú”Ê! èMâj¼|±EÀ(_”ù`,?вöyÑ/_‰ Ç,ÃòúA¿Aš\þˆoó³®òù<!G\ÀeótœÙ±^Vø !ÂUvYpkÚ¶å’$Ü/M×ÒI®í9A'\W.VhJÇÚÇä4,õö³-ýQn™ õq‹ö*çÞ gû$ú·XxÉÌ-Ôïsò_å;kNGÓ>kkfBBÓµMÇBÛB\“ŸòÂ|vY,/¯mûïÓmmÞ8ïm÷K¤{¤|'ÊïK9O¼KÆÀ•ðß´%ýå`žIŸG9ãîûïLmß—”5Ž÷ &¼‹d×t ÇšÞñÛ,¶^£ø¶ —EKôµ¹Iʤü¦iËÇ©c1@ --ãg`À xãÜ 2ÄkÀûfµ£`’÷ù²OV›1úI=Š:ÛŒWæ»ËßM}/ÄqÓ§éÉÛ¼´×¬|³2ñd”ç3„›M·íÙLûK[Ùcafç¡«úÙö®ëA¹Ÿ¿ºz¦Á‡Im}¼‡„>Š€žû<Û–ôT6ž“€$  H`éP@ŸRÙåN:mè$aÆotôð2L7¬ŒúlrÌ0+G½òÒɇËOúÓú;›yã3Gq˜¶FŒð¢3¦)¬ëÜô±Ä,fQ!þçó䣦ø˜qÙ i€.› ¦ƒ#Ò3ö‰'â/ÿqA„އ…'ùƒd˜„ÉÇ|ÓÇ}·ÜrKmžÑI qÓ ç³ Š’óÄ>ñeË]±`fñᨯQž¹,Îç-éÊ3¦›ÒNþ¨ËÙuY?s¸£î·‰L‘8 /ä`bá—-|™‘É „ø|.³-­äëÊ}Ú–67nýÌë_G|äŽ&fD=á\t€<ûì³}DZJ‘õ½é^‰ðáÂ`6óÕ÷*³§¢N†¶tdІ2CªÉÍRý,;ºø_LGhf(KÊçK_úR=û!$ض èÌZkêÜ .Ó’@Qg(ßÒÑÁÌ Ýˆ«Ü²6*‚unó"Œl²|PÝ¢¾Ðq2Œ£#ªé™Bù0»4f ç°ÇÊtú ï¦p´ÄÀ¢|åpàÙ五Ÿ_á‡v$\“H…ŸaÚ³c¡mi Ò[òÅÀ±…ÜRo?±¶yŽ-y ù î{îÇl‚Ù³ÙÙ~fïï—ïGÙúÄü«»=Bya.>Ê7¶´¼ã•‚ïèÙ!f‡Ÿa¶ <áY_ò \Þ»hÀ¥ã}½­­iKS<ï»|G˜ô~/óÅûPʼñ›çåÓöž1Éó¨LǸ¿ÇùÆâÝ WZ@ yÞÐ~à°ªRráÞ¿²‹g Ûx?(ëÏ\fcrÔËð‡å´p¼/Æñ³Î:+7nÇ-,?4å5âÍ[îÇ>÷&¯³›•oVúá˜ìÐvïrüÛßþvmí#§¿«ý<Ð)¿– i·†;ê;BWõsÒži¹lhK郡¿€þÇèÁ{7ŒéÃÆñ<2á{Š÷B$  H@§ô)•u) çhÚ:rò5yŸ?ÌkóÒ&ùübì‡I%„nLûѶ˜ÁQ':è „ #@1Û8ê‹lîôÆÔö°ŽN„}>\養ÆaýOz]—,`HÞKS`˜„¤ó«I¼iJ?Ÿ|X ˜QO™]×&j6ù?UŽÑy†™:F“SG¦ÑA·«“]?ËôqÏÒfÐv`–º6ja2X‡N:XG¹ßg¡~fs‡|€Ç ±ÈÛboË2F@4Ò‘I™ò×$tÇuƒ¶ tbf<èÐ þ…ž+”#p<¹ž™Ü<0aŽˆψAq7£½Â i7Œ¦p‡=ÆRäç ¦0Ëò6œ¥|íçüÒ³ý|Ÿ u#:.Ù2@äd8Ú-,Ñ ;Hgšé$=ˆ“ˆãÑ)<Íøº »ëûçÏ„ž£¼ó<ê’ÅÉþÆ‚u:˜ñ›w­x ^rˆïPøãx×CŒ*giòlcÀÚ°Ïד]&Ãäu1®± Ÿr—ïóK¹Í€J9€½´fØOn6ÍÚ7+óèoâÛ„>’ŨÇòq,ËD»‡õ,úRè§b¹4ÚTDöaÛà\ÒKù!çcû±l$ƒÃ†q,ï¡ H×I@€$pzP@ŸRyЧ¥ÁŽHñ%fñ ûò%°{饗ö^ ™ý§“€$0 ¹£«4w?øF S}Tb^/ ,<©\Û{±Ò`<€$ Y À,åÙk1nÒn$°˜°êÅÒ’XpÄ¢Öó2`åÒMiÃAÜo'ËRSº<& H@ÀâP@ï˜5&ë>÷¹Ïõf6ó²ÅlC^ÔøcÝpgÎt }‚à˜…kÿ±¦—NÀ4ĺv´7³`žóëögÖ{6E;È3‹utYÇN' H`0».:/_yå•YH’i€$  œ,‡ÏÄ¿ù›¿9)i0R Ì:þð‡½û$î—¼ep;ÂúBî¯þê¯êpø>vöùB´åcKmMÝYàj$ î °äë’®ZµªûÀ Q#X±bEuÛm·MôÇzã: H@“Ø»wouà 7T¾«OBQ¿§2³Ï>»õ[÷Ì3Ϭל_(ÿ zocúyu€$  œžÐ;.w:ù0Ï{þùç7þ]{íµ³Ôu³EÓNW_}õl%ÊÔH@˜:Ü.¾øâÆçÏ/D$  H@øÿá?ô:Ó£S}Ô­¯>äéž$  H`Ž?^­\¹²îã»ð «åË—W˜tÕÜå—_^aõA' H@ÀéK`æt:ù?úÑVŸøÄ'NßR2ç€$  H@€$pÒüà?¨¾úÕ¯NôwÙe—´ô±$  H@€$  H@Øy}ø¬x¥$  H@€$  H@€$  H@€$  H`| èã³Ó§$  H@€$  H@€$  H@€$  œBÐO¡Â4+€$  H@€$  H@€$  H@€$0>ôñÙéS€$  H@€$  H@€$  H@€N! è§Paš H@€$  H@€$  H@€$  H@ŸÀÌ è{öì©>ò‘TÿøÇÇÏ¥>%  H@€$  H@€$  H@€$  H@ ˜y}çε€þ±}l¬xZ€$  H@€$  H@€$  H@€$0>ôñÙéS8E \ýÀÕÕywžWýæ®ßT{ÞÚsRrùÂë/Ôi ÷¬»ç¤¤ÁH%  H@€$  H@€$  H@ÀéF@ýt+qó+%@àÄ܉jß¡}Õ–7¶Tk_Z[mÚ±©:xäࢤüػǪ3¾vFïo冕‹oÉ…w]ØKÃ_žõ—åiK@€$  H@€$  H@€$  L€ú äì@Œ=qâÄì'ô4K!¢9³¾ÿô[Ú³˜}Áò …HŽóÉŸ\”8ËH–­ZÖcðÅó¿Xžö·$  H@€$  H@€$  H@º†â IDATÀ( Oê‘wŽTw­½«ºðî +f‘–—ÜsIuÃÃ7TO¿ütuøèá)¤À Û¼süŠÙ¼¤Ÿüþ'«·½Ýv©Ç™3¿?ó‹ÏôDã,bçýÅHu#âdü ·áÕ Õe÷]6ï>ûþâ{.®þðèªg^y¦:þÞñAAõ»óÉ;{iøæUßì;ç H@€$  H@€$  H@€$ éP@Ÿ×kV]Ó¾B„´ecDwÝô 0›8—ÅÛž~¤Æ0,G1ý³ç|¶úóùóºÜ~yÇ/‡ kÒ‹ˆ3Ò0h ôoè]×ÚþÅ¿ýEµú¹ÕC%ï¡MõÂþéM?ÊI@€$  H@€$  H@€$  LF@}2~¾™y>HDk:÷+¾Q±î³nº/3ÿ{ÖÝ3Ý }h?¸þ}esýC×Wï¾÷îÐþ»¼ð«—|µ—–A³ÆwØÙ».׫…ö™µ¾{vÛ³½°™Å®“€$  H@€$  H@€$  H@˜>ô)0~mïkÕ·¯ùvOü 1Y­Ÿ:óSóŽÇùåk—·¦†Y°/¼þBõÔKOUl~¤6Í:Þ]¸ý‡÷Woy¼Ú{hïÀà·îÚZ!þ­y~MÅlîßx±š››è/ŸÄ„úÆm«—w¾Ü0ÀZä„»n뺊´ ë`BZvìÝ1ôzæ] è䇼lÚ¾©:tôаI_ðº{vÔe}ô£¯Åìù¶ÝÛêå0Îþ{'Þ«…gf0Ãu±\°Àäù[GÞ9Ú¯\ô•¾{cp=rà#zøÑ ?ªÓ ø…Ü¥+.íÍ{ù ¿úBõ÷—þ}õùs?ß—§8ÿÍ+6Éþú¾×{~oZsÓBÉð¼$  H@€$  H@€$  H@@Ð;€Øâ[fÿxù?ö.Cø~àÙª|žëκé¬Þ5ì`Ö™§ƒD÷ï_÷ýjÓŽM}þâÇK;_ªò" l1…HÍÌÞß®ømo=ð¸Á¯m 10®)·¬)ΚáöÚWýõyÝóC¼ËV/ëý& Ö»¾ý‰Û牘«Fnr¤¡µdGx_<ÿ‹µ Þäð“¯}ðÚ¾4\qÿ‚oüÁ¬ÌB1k^cŽ»dA>±@ÐƽëïWž_þÍ—ë(ðã<ÇOþð“2 ÕÎý;«yË4ð;¯áÝ––yŽq`!ÿzí¿Öim š2¹ú«+–13푎ÅåvøØá¶`:?N}¸rå•ÕªgW 6÷i¤›m¸À½†`žÏS>Ã8ÖNgIˆ…· –×H@€$  H@€$  H@€$  ,L@}aFc_‘EÞ, G€,¿ OTCìÎîg·ü¬ï|àò>ñì>¸;{­÷KQ/ü0sñ:~—Ûë¼®3šËóm¿;Ã!·]7ÌqdÇlä3oçkX}Vókº$  H@€$  H@€$  H@À©J@}Š%›Ñ&½4󎸚])rÞ÷®ý^õeßéãæ~yÇ/³×z冕ó®Ë"^Ûþ¹·ŸÛ «4yŽÄÛ_Üú‹y³×9wàíµ_ß¶ð‡9N³#M¥?x”Œâš,Ø>öÂcóüÆum[íÒaR;—iøe~9{šsˆ¤¥™ýŸßòó‘ÓBX”C¸¹±/ ,0“žÙüÌ`tÅvúûÞhdq6mïæþÈBo »¦k›Ž1³V]) c>ŸÁ'Ï¿ö|u÷ÓwÏ«XŽÐI@€$  H@€$  H@€$  Ìô)–I[1g¾åõ-õLcfx#¨—"áoîúM_jX—ÓéˆÇ¬ÎÌãpÌnÎþ¿zÉWãTo{bîD½6ö…w]Øwmø êÌì SïlYÃ=&º}¿vÙת[»už)iŽGxlŸÝölx­ó›°˜ËLõìçW|£Â|yž ¯pO¼øDßõ„0óÎzÛˆÇ9LöYG‡)ü&»¼>~#ê6Í@/gÃ…tã˜ñNù~é‚/õ¥3ëÙ.âjiÒ;âæ8B3u!LÄ3Ð"òJX¥9}Ì|g‡p›×Ÿ† ÷’é§1ÛŸøÖ<¿fž©zÊí̓oæ¤Voz¸ÎfìƒAl9°Þu`WŸßYúQ 葇¦-u‘2ÒI@€$  H@€$  H@€$  Ìô)–I›„´òëo7¹CGU/¼þB-¼._»¼bf9röÏ,ä6‡©í|-ûÌdgmæìšDã8@Œ MX÷¬»§þ{dó#õÌ÷6¦Û³ËÂõ·®þV}ªœNx8Ö>°r~JSöw­½+GÑÛ/׿óÉ;{ç0{óš›{q¢ïÎ;{mkMs<ÒÆ‘½äGd¥Éû6SÝ—®¸´/<ÂdùÒ1€¡tß]öÝy~„@^(“õ©OÚ];ÂÌ,Øoš]N:Ê{ —I™®,¢ão)¹Qt©„¥†¥”GÓ* H@€$  H@€$  H@€N èS,åR<,EÇüûŽ'’{w4šäÎþb¿M¨%ÐR@ÿÁõ?è[#|^ÄÅfwkj?», _t÷Eõ)Ì£GºÙÆúÞçßy~ï8³¯Ã!¦çë‡ÝgÖréJ“ô!ޗו¿W¬_1VHëþÃûËàªR@$,—žo}üÖÓB}`Íøi˜o¿ï™ûúâσÊ´^°ü‚¾k1ýßæN%{ŸºßÖpóî: H@€$  H@€$  H@€$ Ù" €>ÅòhÏBFDûÉ~Ò35ž“‚ÙñQ„ãQôß?üûÕÀ}̶Gz‡ÙЙ¥‹U@_ˆc[ºäK7®€Îìð¶x:ÞdB½з¼±¥Ljëo¬4­uÞ–Ì©wé¨?9.Ìø·¹²þ`Z¾ÍJúÖ][{ÙÄüþ†W7TŸ=ç³}ÜξùìÞ5îH@€$  H@€$  H@€$  Ìô)–C~YÓ3ìüa ½É4wNJ)"¦#\nܶ±^Gú¹íÏõ­> ½4GŽhŠ™õUÏ®ª^ß÷zµ}ÏöêÚ¯í§! þÜÏ÷ÅÁ,æ+W^ÙúwÕÊ«*fi³6zéJ½Loy}üÆ4zü0( œc­{Lí7¹IôµêyÇ/{k¥çôåý<›?üN²EÏác Í±–{¾óómîTÐ#¿k_ZÛÇbÐ=~ÜJ@€$  H@€$  H@€$  ,.ô)òÎú?^þ#Å”ý"@"¼—îo/üÛž 7HŒ+M¸;ýþ¡>i`íÒ•³ÉKA¡9Ôqg çµÑ ômŽÙ¾ˆä÷o¸¿:qâļËJ!ü²û.›w ëF6½ÎzÞ‘Ø2ÎÖ[gÛä&Пxñ‰Ú¤>3˜=TÏ€ŒMÛ7UËV/«>ó‹ÏÌKëác‡›’1Ö±&mzh^Xð+ë13ÒÛÜ©. soD݉mSmããq H@€$  H@€$  H@€$ éP@Ÿã-¯oéË5Ÿzé© ‘w!W ×?t}Ï˱wÕ³›ó5쿼óåÞ5ìp3 K±öG7ü¨N éá/››ÎÜøÈ}y@ ÏB,k8c’;Ä@¶Ìˆ“åÌ`ÏiüβïÔþKÑÙâ'æNTy tü1SWŠÞÄóÛ{[¯!?Ükñ˜ÙÖ‘ž&Óåå`òD>9¾lղ꫗|µçŸ4dKåLxLr#Ô…š°qÛÆÖÚ†<ûÀP±Þ÷Ì}U“ÀˆhÉzâo|³úëóþº'à1;‘6ÜBB~ˆ|_<ÿ‹Õ[GÞ o}[DÜï_÷ý^á‡-â:B?éŒã¤íÇ7þ¸:pø@õ©3?Õ;çIS):2{1–Êq[fÐÇß‘(ÌŸ3£:_—÷Y>ÏÂy‹°ŽÙö6ñ÷»Ë¾[±Îx›Ãd÷/nýE«òsÎmçT¥iu50#=§·m†mnØrev7ƒ¦é`Á 6–”Ç–7¶´&᪕W Ń:¦ò[;I'ÓVŽù8÷2×®Þ¸ú$¥Ôh%  H@€$  H@€$  H@€" €¾¡“|ž5¢Y£œ™Ø¬ŸŽhÞfÚ{šIÝx-ί~nuÅúî±Îù4ã63Ò1Oz»Ù/ÅöAþãùÚ´cS½f÷Îý;+xâX{S÷¤ƒÁ ¯í}­uæû(ẖÁX  ^P0§NÝ`` àÀ\ü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ß¾ê£ýhõ‰O|bj X€$  H@€$  H@€$  H@€$0óºE$ H@€$  H@€$  H@€$  H@X  è‹AÙ8$  H@€$  H@€$  H@€$  H`æ ( Ï|™@ H@€$  H@€$  H@€$  H@X  è‹AÙ8$  H@€$  H@€$  H@€$  H`æ ( Ï|™@ H@€$  H@€$  H@€$  H@X 3/ ÿzù¯«3¾vFý·býŠÅ`b€$  H@€$  H@€$  H@€$p˜yýÜÛÏí èË×.? ‹È,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@} E´býŠêÇ7þ¸úÁõ?úïÌϬÖm]W§æø{Ç«kV]Sýè† ퟸνýÜêõ}¯×aì>¸»úõò_äŸ0.]qiuô£u›¶oª~vËÏFãÎ'ïìQ”M†²ù‘±üïY7U‡qÁò ÆãÞõ÷Öa|þÜÏÆ›߬º`ÑEò<£Šº%Ï÷ïÍS­~Îo‰<" H@€$  H@€$  H@€$0ôq¨ ðÃìíqôÏžóÙ:df¢ƯîøUÆe÷]6vmz¨ãË¿ùòØan>‚…<ß|åùá ëç‡,â~Ð$yJ€$  H@€$  H@€$  H` è#ÀöÒM;6UW?puuÕÊ«†þ»öÁk«{wÔQœ˜;Q­~nõÐ~#ž›×Ü\:z¨ãÈ;GªÛŸ¸}ä0î{æ¾ê½ïÕaì:°«ºþ¡ëG ƒ|?óÊ3=T“²  IÃç‡uKž§fýìÝpîH@€$  H@€$  H@€$  LD@}"|íž½{¬žÍ êaþÞ9þN_`sssCùËa³Öwv'Nœ9 üdG˜9Žaö³ö'eÑEòì/•IËDž³Ç³?Eþ’€$  H@€$  H@€$  H@‡€ú8Ôðó•‹¾2–éó0Ǽûàîê/þí/Æ ãþ ÷ש[óüšêO¿õ§#‡ñçÿòçÕö=Ûë0Æ5ÿ…_}¡bÖ7nR]„!Ï3ª¨[ò<£:ëg}³ùO€$  H@€$  H@€$  H`b è#ì±vÜu¿¯q÷®¿wì0¾»ì»ugÝtÖØaÜúØ­uŸ:óSc‡ñêîW«.Xt†<Ϩ¢nÉóýõÃOµúYß°þ“€$  H@€$  H@€$  H@˜˜€úÄç𥠾4–ðüó[~^öæÁ7kÁs!þžu÷Ôa<¼éá±ÒÀ¬uÄEÜEw_4VŸùÅgª0?) Ò1iò<£Šº%Ï3ªS±~Ö7¬ÿ$  H@€$  H@€$  H@€&& €>1Âæ9XòwøØá¾€Þ;ñÞHþ‰‹u­³c]õQÒÀµÄ›ÝÛÇÞ9ŒìŸýQÓP²è" yö—ʤe"ÏÙãÙŸ"I@€$  H@€$  H@€$  ŒC@}jú‘€$  H@€$  H@€$  H@€$ SŽ€ú)W¤fH€$  H@€$  H@€$  H@€Æ! €>5ýH@€$  H@€$  H@€$  H@À)G@ý”+R3$ H@€$  H@€$  H@€$  H@ãP@‡š~$  H@€$  H@€$  H@€$  Hà”# €~Ê©’€$  H@€$  H@€$  H@€$ q( CM?€$  H@€$  H@€$  H@€$pÊP@?åŠÔ I@€$  H@€$  H@€$  H@À8ÐÇ¡¦ H@€$  H@€$  H@€$  H@8å( ŸrEj†$  H@€$  H@€$  H@€$  H`3/ _~ßåÕ'¿ÿÉúoõs«ÇÉ£~$  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@€$Lìhà 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óúÎ;kýcû˜…& H@€$  H@€$  H@€$  H@˜ô©¡5` H@€x÷í·«wÞzkÐ%ž“€$  H` ¼÷Î;ÕÑ}û1F£’€$  H@€$  ÌôÙ+S$Ó‚À»‡Wûžß\íÝüþß‘={N‹|/v&çNœ¨¼ôRs÷îÝ‹”êÄñãÕ‘Ý»ërsݺjÿ‹/Vï;¶èé8­"œ›«ÞÚ¶­Wþû·l©¨³àûéYÕ5ÿëÿRÿ½rÏ=³¤‰Òplÿþç¸Ï¨ã‹Í›øŽíßWß_»ž~º¾ß– …cÌãw`ëÖ‰Êätö,ÏÓ¹ôÍû õ{ÑÖ­½öæà+¯ º|ÑÏ?z´ïý8ž'¼7/†{éŽÛ«ëÿ·?«ŸÍ<§uÝ8ôÚk½:ezhÇŽn7 H@€$  H@˜ ô©`5ÐY'€˜°Ø¢Æ¬3YÌôíx衞pÚ]ÿÏ_/fN‹¸§oüäœÇæüüçþ#?üac:HËŽW/ZZ–rD'Þ}§µ}ùþïyÜßxü±™À÷>Û'~þ³™HÓ¸‰xþ÷¿ŸÇ9òG{·Ñ|ýÅõ„ˆ?¶ë.¼p1’1V/ÜtS+?}Œâ|¾WU—µçŸ?¯#ÉNºîËaß /Ìãvt /†{kûöÖ4DZè¬Õ &ðâí·÷8n¾á†ÁpöÝC‡z~‚5ÛWï¿(ÿÓ¼#§éÑÿÛ4£›zØ«þù›}ùÉy[ŒÙõ –aPLŽ·iê ÆŒàÁo»5í»Ÿ}v¨P}¾ˆ© ž†æžNÌømj¿ñÆLdrÐ`¬Åš ~ï—ÿ¦Çˆo%Ýäšê\{{çÎÉ#0 H@€$  H@˜ ô)`Ýô»ßõ:âãxÐöé__ ;…rh r×Ú§úÊæµGiºÌcS&ðæúu}åÀý¡€Þ=ô¶™Vð^,a´lÿè½ã¿ÿ_õLYÌ„¾ºbE÷™?ÅBÌÚÌ*Ö5 »³  “~êAÔYž= ë——ÿ±——ÈSlC@oŠŸ{‹ºV(¸4«Ž6 x•ÛatŸï–nc`V~ö/†€^TyþÆßW'Þ}wI! ~O_pA¿atŸïýÅ=)ÏþÐü%S‹Âr;=+:”Yôð—Ó¸X:ñ¾xë­³w»«÷Gvï®Ëô™KÛW®2!I@€$  H@蚀z×D«ª:üúëÕCÿú/}Çt€0 á#w†äý­wßÝšš£{÷V¬ºkíÚêõ5kjª«5ÓŽíß_í|òÉêè¾}­ñsâÄñã‚Ù¬¡Ël/Ö6E c _Ö÷;°ukoÀ˜qñæ3ë+Ò2¬ƒÉžÏV‡^{­ÖB~»î`Çd,yÙ»ys§&&)k:Ñ9DhLd³Â$û¦'¯=òp]^ƒüwyŽ´PG)G:ßHF¤­)/ù~Q^~¹ösìÀ¡“ØEýÌ‘‘Žý/¾XíÙ¸q¤¥¨Ï‡v¼_&o¿ùfr¤}¡-á~;²gÏH~Û.~ð»ßéµ?‹% ç6nÅÿ÷·mIù8å³ïùÍu{umP q¯R¦ã´oïÚU×íì—}ê;íõošnýÕûîë•;åú;VXƒ ]5ýÓ¨Ÿ]ñö™ÖU|e8´_¹Þ# OÊ3Ž!îQË“ç8íÍÞM›ª7žx¢zãñÇë}žÙ‹íJ+>§«€ÞÕ3m\ž]—{´Áã¼/ÑÆÓSh³xïŠw Ú`Žò.:nÞÞ}ûíú½—8y'^Þwºpä‡ðÞÚ¶m¨à&m3†ŠdÀEÄO9ðNŸÛ‰úY¹î. –;™<7]{m_;ÝE9NRÇÛPcI$ž'‹% ·¥eœã<ùFỆ:3 nØo¬Hë8ß›á·i»åæþA èM”<& H@€$  H`6( O±r§ftÃ!ºl_½º6_"lŸøùÏâ’zËúÜŒRÏ"c¾žýGÿíGµˆÔçñƒtâ—f 1åJgë†Ë/ï­ábª/:%#LDª•_ÿz¯'®-kг¦;j¸cû÷U÷ü/õüï¦ë®ëýÆ/‚ÐKwÞÙ3/á=ñóŸ÷Ä×/¶¤¡ ³ ˜8nëh§ó‘hó ×÷¥aãUWÕ")¢0+óqÇ–²g¯¼¢6qÇ–|b - „¬²<ïû»¯ÔAÓACçX˜Û0?û숺·¥³e9À<ë·--½À&Ø¡3Œ2,óDÚ)#L‡ð×$Øfì?÷»e}åC8ˆ®tvµ¹qêg„µmåʾ{€4ÓÑóàÛ‡¿÷½B:Æ«¿ý?çù£ÜÎÍÕâf0‹¸ÃÏÃßÿ^µ(O=ZñEÕÊüZ]—àNûÃ,ß¼.nSy|˜Éöv=ýtµá²K«r&)q²dë~óëzvSSLë/ºhÞ}@x8Ê›5ÓsÀþKwÜ>/¨…îÕG~øƒy³«^øÃúâ~òܯ²ùtÊbãÕWWüð‡}eM9#>víhKi›`õ¸¢ÍŠí ÎÿR@ç>kªc´ã;¸;¨Ÿˆy¹}Š< c–¾«gZWeDûÁÌoXRy~Òæ!>G¾Ø¶ èòä™FYr?•Ï¸ÏØòÜC¨jrñ,)ýGúÉ÷#3çJÇs0—%×Ò~†ãÏárñt!7ªàÛåó}¡´µ§ýÉϳàG€=ÏaÚdÄ9¶Ü× ÒÊn’gZ'öGáÉ Ú»œNÞÿÂ1h²|¯dÀh›[¨ ô¾D˜pÛpùe}éÉü2sž;Sqssõ³Š²Ìqçýûþþ«õ{}SüˆËMÏ,¢â°„”ß—£^Pæ¹ ÛŒyá y€o¨lŸ»æšy÷6³•iÇ3êϰ>7#<Ûtêdn·ÈíƒZ›Ü¤uœ0iy× À—ï*Þ1ö½ðB;阦€N}di\~ìÃbÐûõ›w̨øá]˜³ùþä8×Ls鬮¾±(î¿q¾7£ŽÀeó 7T \¥‡#ß{| —uO=¨¹•€$  H@€$0{ЧX&¹3! è%k¾æŽ :%³C`ËçÛö‰§©s»4þ™9Þ$žÄy>øÃ1£9Ž/´¥£>\)Ø,ä·<σ옉õØO:TZêiÖ w¹,ʸÊß\›ät0;( Y¥ßøMg[“PP q\O§ MƒÒ˜;R¸uäFb;ìl¦œÏaörËN߈³m[Z(;×ÚüÁ³©LÆ­Ÿ‘¿QÓO§`“£¾¶¥}Ðñ<¸ž t}œ{ùlJF-¾ªGáŸ-õxSÜ9ž…öé ÎŽÎè&? xᦛÏq=÷Q®#ÃÞ«ø¥33\)4¥¥íÏ]:B´ÅÕt<ç#§c”ösïM®‹úI¸ƒêgS¼ùXÏ´Þ¸û´cMƒešÊ„cMz<›Ä޶4<ÿûßÏË.Ï„š6?ù8Ïœ¾Ássþ^¹÷ž:Ú¶ì?öw<øà¼t”F|»|¾—éå÷ g¨° Ê-ÂY¸IŸiNގ“zR¦/?+š–(jk÷†mƒÛÞ—¨o£Üg/Þ>Uæ0î>eJ&m¿›ÄAÞ¹›®g°V9+_W~7tÑfŒÃ€ï‹œ®Q÷Ÿ:÷ܾhg…g)b"3§- –*ݤuœo›g~Ûoλ-~ŽOS@Ï,Ë44}Ï ,˜•×/ô›Óp]|cMò½yÂ×(Ïgô çV€$  H@ÀìP@Ÿb™d¡ ì#ÚÒÌ;þÙ•"7áÑÙÆLزs‚Yo¥cÆiyÝ0¿×ž^/¨Òä9þéH§C¬©ã“À8ßaâj»†KÇeºi°ÃL¹ò\~VSý óMéëZ@ïêù^²õ÷ !”gϾ’güf­áp“>Ó"œ¼E@o♟Mâg“€ÞÖò¾Ä{X0bKÛÄ "f:³-Ÿ»ÓЛÞ?™•Ï`ÍòÝwÒÒ±ÎnÒú™ÃŠ%LHSnóÉÏB®‹gÚBq,tžú%{Ú ž –±¥MàžçLÓ Êi è¼ó>À³¹¨=H@‡ež¿;"?<xï/ëoÙ¦Dݘt;é7֤ߛ¤¿ Ú¿zàÏEÍV…Ð'-yýK@€$  H@˜ôé±×© m[ÖwmrtbŽŽª­wß] G79Ä 6G§b¾–}:s‡~›Dã^˜ssµ MXtòGç^Ù9NÇKvYLˆÎϲƒ*If½E:s~J1«#"¾5gžÙóO8tV„£c)â`‹àÁÚÕñWÿÏþèä*ùqmi~•™,MîÙ+úE¦³¸tˆÑ¥kC„@^(“õ©O†Ópä?óhëÔ+gËgјt•"f.³Ò8•­nÌúIxXMÈyaŸÎÀpeqŸˆ477ï>ç-])p•u¸äI=*õ­ä^½ÿþyyÈ¢þ•r¢mÎB´Q„“ã+ÛÀç8ûÔOÚ§\ö´+ÑfÅ!§Í‘¦œÆº]JË\”í8&‡³›´~æ°ò~ˆE‘¶|nÐ~'Ï´A 8—Ët×{˦zÕõýÞ”¼|¯P¯Gu¤›p L!½Ì„.Û•zy”"àÒ0ó¹47Þæ(Ç›ÌþfÁ¹ Á™2\wá…å©>Q´¿{øpß5‘¶ÌÆir“ÔOÂ+tÖÀÎŽ™c9ë/úÐRv>Ç~s#:ÛóuYPkêpË×ÚGì•khF'vÄÏ–2¹MÊ÷X¾®ÓýŽôaD’m+ûP÷Úu2óµZK=ËäVˆ¿8:¼sö)mij;ž;Ÿå©É) c";ßä<0 -\õ3Â*·] è£>ÓÊt û›{(sb¿Ib6i¾®ëû½)½YhE@sݺƙƒ9ý±_ZT!³  qŸïá¿‹-ƒƒ[Ö—ë;íoÓ³‚k'}¦E|±]l½«÷%fýf–MûÔu,:íZ»6²Ûù–6ž´ùùÙ”–8Öö>T è¥`Ú–ði¶Ámq6Ïz¶ùf‹ðŠc0g>žÛÉ“Í3òV è9½ì3XµÍuQÇa˜ãl|Ç`—|M)J·¥oÒã]èÙ‚é)gfgK“¦·ô?î7֤ߛåzðõ»ZÃ@ü.Iù* —%èo H@€$  H@³C@}Še±PgâòãgŸÝ35ž“BÓ(ò£è˜lÖ5ÍÊ9åþ@ýÒñô…8–iˆßò¥·ƒ½4%q ³mê$*;w0å8´››z`éËbØÐq ¸°Éìd“ÉÒAôNå:^w4õμ¿“`4 è“ÖOb)ôR$¢3<—sÐK±„ºÚäÊz—µ&ž9¾AûÑAYš‘gæ“cp@„·Ôô6¶e>iß"lCè.¯ãwYèÔÄÍЯçNOD\) œUZûï_—:ÿÙ•r›ÑEýÌqåý,(RVúrPØ(Ï´aãhº®dÑV'K«]ßïMiG@/ó“ý²m$ èóK#—±QŠÁ˜hnr彨TùXùÎÕæb è]¾/½xÛm}íyÎ{¹_ jb1α&«?eÜù÷°z\1(]£Þ§9-ñŽ0(üaÏe=[Êñ±D®Ðó'›gäw!|µYÌ袎—ß6^z)’ÖÛ¾ûöÛ}õ) èå}PZ kú6êe|Âq¿±Ê2Éu{Ð~|o–õ¢íú‚þ¥ÌÐ',p½K@€$  H@˜"ô)ÂÍâõè[¶Ô¦ØY›»É4wNJ9óq‘`ÏsÏÕ3ÞönÚÔ·~ì4ôÒ9˜d H:FXÛ³œ¥Pvæf”<¸Qg —ë2K”~­Ë®©˜¥]š 'îRÈ,Ó›Ë ï—ä«5þÒFGbin:·s'ü³eö3¦—Buìt-”–‚!qÛù3‰€ÞEý„ã$:€%û0÷Ë*[Wàú,¨5ñd=âAõ A4. låtl¸ü²}oŸv"®ëº^ô"É;Î@o+stì#þFÙ2˯ͭ¿¸=ʘq6ëú ö¾)¯å ôQô.êgSš8¶Ôôr0 õ+¬äøIT:Ñ·=ð@O\Ì—–^Ï^yE>]ï³¶!adÓëM&4ubÐÊzëm©ãvî@ÖÜDøx⿨èXÆ1 ƒ5 7]w]c'3³XºtYø¦n4™Ëø˜UÃl‹,"Źû¥Ëõ§œÞEý$¾Itüç<À‚Á2yðFyÿqMÉ¢ cIÌÜsž{/\i‚q¼öÊõwOU½é^mZÇ”û»lgCü›E=[ •kž#Î`î“ÜåÚ¢“èMu|Ôúõ´Ü.5®e™Çbn®oýèiÜï%G~* #ü‘¶ø#_M&Åã<Û&}ß /ôÂà¥dW¶mÓÐÇ}¾çôv±Ï½Xæ;sdpT“ëê™V†=Ž€Î;kN3"n¸Ò|u) 7µÁã¼/!!Ô2°#üSGhÀ É2¤÷µG‰dv²}ð»ßéãÀ³¶tˆÙ™UÛ{߸:ñ•õi^»“ÕôŽN½Û…€>+

..Ÿ7´3ÔIÚzfê–ql¸ìÒy³¯©ƒ˜‚òÏBþ7^}uïÏØ¨»½Ä~°SV"-Ã>ß˰&ý‰ö2ïü¦¾”b\ÄÕÅ3°ºàI›’Ó¸wèµ×êÁœe]£í)E¢² ç})Ïæ¤,󠱚ÙÜÜÆÌ )?ŽóïaÖi,ÅüóQ^ÎŒÈᲟ…¹rÆbymþbuÆQvFÇõ|´—qŽ-BgSç<çð[ è§£= èfâq嬀8϶ìèŠsˆK¥+;ûãÚ¶mÄO8ì·]ׯ# èmþËãµE¹¹2ùõïr½küb–Ùk[n½¥oç¦ÑÉ‚ÈH¸eºùÝVY³ÛW­jô‹ÿí«W×f„›ÂæÂ,n’ú3t¶Úâ¡Ã¯4³˜¯ÍƒIÊùº¦ý|Ÿ’—…x¶1ÍBL)x4ÅÛtìÕ+jž]ýC(œ¦‘¦I¶2m:ez`‘M÷2 ±¼&~Ó¡†É÷ø]né<ÇMR?ƒí  S°ƒ»zFÉI l™Öø]v’–:ÁŒÂ“p1Ë›‚MÓÌ–HCÛ–û®KW®ÙÙo/gSaÞ8Î-´­Eà–Är¯>ýë z¡´Õifu7Õfí–é¤Þtí˜ýYÆÓô›ŽpÄj³¡š®‰c°ý¦mÎäõ“gLSƒŽÑù®‹gZ„5é¶´J1(ù\t3)Oò0Šp¥ŒÒ•æsZí—ï åÌéA~ã\Ì"oàâš¶í û~’ç{ÉgÒߥåòSÎàÏqtñLëŠ'‚í¨¢k½g7J\¾/N“€ÞV'8Î{E׎™µÃ¾óå´!À†kznäkó>–|Ú\mF[؃ŽHY:(§Ÿ}êM“€Î»Æ,ð$ ešùbä wõ<° f“ÖqzŸ‘Ö.ßëGxDü ² ×ôM|y‚Qü.·Ã|G< m•[/¿ã;  ·‹ïÍÒzKSüå±AïÏ-›ÿ^\?[Z—yò·$  H@€$  ŒO@}|v­>÷<÷\k‡Þ̰ÆaÒ­©ƒœÎ¼g.ýmmÆÎœøçcš„p ùá³ztº69:JS~áq|¤3Ž‘6fÞÐáVšµäÒD'`\Ï–Î#:XJ’΄<Öôaþ¼#sXÌ|i'sÞj±ó¦›Z;E™Y³œ³¿Ø‡3ŠÚ:UɳEüeøCVà̳—Âl‡-W:mç™<@:Ú’'Ê­­ãÌØATͯÉeË9Le6§NPÂ[?Ã?k¹6•#u‡årýŠ}L³SJGÝÀt=Ö6ßp}ÅìnÒ[ξm«£ ñ$~êg[¹âS›My¢NqÿDØÒfd³Íe~ÆýÝfº>ÇmE¦€›Ú½ì—}êG9C¹ ‹ò #³‰aÐ^„Ø~Ê˸hË0›ÏÑù[ÎÞ".ÊK!mb ÷–<¬Ä_¦™ô“FÚ\“•Î7Í ›¤~’®Ò„kfYî#@•õ|ضoÐ3-¸LºÅ,z~þFú©»Ì$.—‰Ám9ÞIxNS§z¤#oÛx?&ƒóµ±O§ýc™‡ÜVù\¦-lšYЉc8eÿuÝ+D¦:é(·õ}ë­cßþ¤Ï÷¾À&üÁó)ç¶y!7é3ð»âI›Ç3"—ådz­\?º~voÙ2/{ã¾/Piò<§#ïÓVl½kù¼¸»:€ùõ¦{4Àš:žÛ¦²ŽòŽ ·œæ¦}ò±gãÆÉž´Íø€“M7i HOi±‚6¥XÆ5ÑžÏÏÒjåƒzxÖôAÛÎà©ÒMRÇ ÿM÷,ibI9¨÷SDÞ.]¹lFSýŒcÔïüŽChú¶á:,EÀµ‰'ubǃÃ}“×¶tDºóvÐ7VÄ5é÷&ßµMï²´%´­¹Mà:Xµ9êH^Œ0X¢M' H@€$  H@Ó' €>}ÆÅ€‰?:ç˜ÍÁúéõlÍ“0âüØþýµ8O‡&‚St4M”¹ <ÓyÄÌ ÒƒØÍ~Ù©?Lð䋎>[Ö½†÷(Žò@0%¬_‰PÜ$¬ŽæB×Ò‘Â @ê&˜1#IÝ`` à4Ëm¡ðÇ=Oºˆt”2c–­Åp³V?Ë<—æp1a¿ëñ|ðÁZCX¦cwG„?³è©ßˆ¼§µ››«ÛÎ]O?]·¥ jX¬º9 î”'÷?b ±Ô•Åv“ÔÏÅNë4ãÃò×xF—ÖF‰÷dò$±F3ujÜúÄ{í íÿ¸aŒÂl¡k'}¾/þ0çy/Aˆë(ï3óL››«ßB[3JJ>£¾/ÑFó ¡ˆ÷öyÇ@”f¿œ·HïÄÌPƪ÷ú,?}K@xŸÀÜ\µË–êÕûﯶÜrsµù†ë«Wî¹§ÚõôÓ®yn‘€$  H@€$  H@€$  H@K„ÀÌ èûöí«>úÑVŸøÄ'–R“) H@€$  H@€$  H@€$  H@K‘ÀÌ èKªi–€$  H@€$  H@€$  H@€$ ¥G@}é•™)–€$  H@€$  H@€$  H@€$ )P@ŸTƒ”€$  H@€$  H@€$  H@€$ ¥G@}é•™)–€$  H@€$  H@€$  H@€$ )P@ŸTƒ”€$  H@€$  H@€$  H@€$ ¥G`æô={öTùÈGªüãK®)–€$  H@€$  H@€$  H@€$ %C`æô;wÖúÇ>ö±%Õ„J@€$  H@€$  H@€$  H@ÀÒ# €¾ôÊÌK@€$Ð!}‡WGÞ}·Ã J€$  H@€$  H@€–*ô¥Zr¦{nn®Ú{øpõÂÎÕ[·VϽþzuàÈ‘ CŽ÷wß{¯ÚüÆÕÆ×^«ÿvìÛ7ˆ–x¨ˆ?”cpŠí¡£G—xÎNþц¼Ã‚º£“ÀéNàµýû{í¦×_¯Þ;qbfœ˜›«¶ìÚÕK_´[»ÞzkQÒH»ùß/¾¸ú¿8§þÛº{Ï¢Äk$€$  H@€$  H@€$0»Ðg·l¦’2DóóWÜWý§?·'„pÀöW÷®h÷ãÇ«ÅcQrúbß™‚ýÅ´aÇŽFNð:ç®»û/>Å~!F½(·¿[³æË­Ù‘Àhþõæ[æÝlÞîcDíÅpö¹Ý¸kÆňÖ8$  H@€$  H@€$  H`† ( O¹pö:TýûÝ÷T_ºâÊžPð_ο úúu×WW<ôpõ©_WwÞ¯Û¶mÊ)©*ÄŠ<Ó.‹y¿)!7=ùTOdXöè£M—LåBON[ì3@÷!e®iä¯ÜzÛ‡ž‚{×?öxkÞ/ð¡S0ÇfIÃh¨ïX¿~ø¦xå Á/<'Ã=ñòÖ¾öãÖ§Ÿ^ŒhC€$  H@€$  H@€f˜€ú ç†ÇÛ…½‚c‹˜>mwÛÓëú„âf&úç.¾¤'îŸswólåÿ÷ò+z~¹~±ÜÁ£G{i VlÐûKà¥7ßì•OæÄþ©. oÛ»·Õ¢‚z=ñ×éGà—÷Þ;¯m˜Sòåe›ÅïÅÐ_ß¿¿/þ‡^ØrúUs, H@€$  H@€$  H@}Ðûpt÷cÅÆ}òMA>ö›ûWvyKH¥)ßk׬zè½/)ׂ?™:Ë7<³}{µ}ß¾êÄ9Îq%~Ÿ{ýõ óÒ›ßx£ÚÿöÛµçWöì©V?ÿ|Eݶ£}xùÍÝÕúmÛ«G¶¼X=þòËÕ–]»ªás28…´…äøqyö0×¼qà@µöÕWûÚÞGŽÔÇh—jOO&Oî¥ü<êB@§NsÏR?v¿uh„ ^óÍ~ßKçb è &lÈ ¨Ëð -µí2Š—sáEµùìòê'wÜY‹ƒÒ1.ÏU›7×q{¶Yœ½kƪ4ÁÚÜÙÝ»qcÅ@Š‘›|”yãü·~ã‚BúÆ×^«~úÇ?Vä°û;7ÝT±®ð4t„˜ri¦Ž ÔáHW¹ö2iºñÉ'3’ºî~ÿÖ[ç]›wïŠj×[oõùkû1ª€Î½õ©_7¯L"|Ì_ç|¸·9Î1P%ó¼° âÚ wûºuóêQøÏõë¢ÌDçÈÃß-[ÖcñÇ–zÆl]fý7¹»7<;/ÿãÊ«êKÁБ™.÷@“›”gS˜ »î±ÇúÒG]ÎKXÞKW?XßkÁ„-yŠúŸã˜žm:r½"Ô]žMùõîgŸ­ò€ª`€¿ÿyãª6yí£0`ì+×,«™ÑnüÃï®­î{î¹ê×ßЫwÓÐÉmlÓ}zÎ]͢D~}ßý}míƒA¾tÅ•½´êÉ´Eë#ï¼S•íTÄC¬ÛD>tÛ‡÷ÙÍO­Wöø£mç]ñš2 ¿9LžÙMRÇggΗû€$  H@€$  H@ÀéK@} e¿îÕmó:Ò¯zø‘yéˆiˆ.ü­Û¶m^JWéŽëAÛ«n^Cy¿|nÙ£kúҀȑÏ/´ß–†¾@ÇüÑ$2·¥çW÷®h4M? O„„2¾,F|áÒËæÿùò»úr‹Ð]†1è÷ׯ»¾Ïü`–.ùÍ纞þã;îlûgË朗ãÏûˆ¯á–ò€‚|]¹¨3Ì,îQtD´2~/æ™:‰HMç1÷\:Ê„úÐt}yìÌÛïèÍ Íá¬zþù¡üBå4Ü«{æþ)Ó¿|ÓäJ±™ëë6îÕmóĸ‹-3îÃuÁ3Âuû¹‹/ºrúÙ§½ÍnVx¶ è¥U’È¢p鯛Dâð“··®}ºô^ÿF|/Ùì/ïOS@Ç’BŽ+ïõw¿kL;ÛÚ„Ayb°É´Ï´<(ç£i?·¿åÀ‰|ýý›6Õ–.ò±¼Ï ’p“ÔñYãyr+ H@€$  H@€$  H@} uS¿¹³9öé°¾à¾ûjóÔ˜ZäXvP§|„™·Ìp.ݰ%á0,»&Q8ÇWî#ÆLË" “®RÜ›”'åVæ7 è?½óóΗ:3äË0ú5ƒÒQN ùËç»Й¹˜ÃÏû7=ùT-·‰:Ìú ×$´#š!Ð3c9‡Ë>Vr£è„ÕTÇC@ç|ÓýÓ$ 3kµL/÷oÓL]®+› $•Ì(7X_ñÐÃófŠNK@`óæyù`ðÂÏ–/od¦ås¹ ®•,†ùMÜá&åጳE@&½m×`ö>ܬðlбþД,d÷î{ï5Z Î¶ ±¥µ¬°4ÅÕvlšz›pKZ è0iËo[>`4ÌàŸÌ{˜ýqži¤%ÖJÚÒÌsóümç³%‘Iëø¬ð .n%  H@€$  H@€$  @@} õ€™­ÃÌüF¼c¦âDéšfq^»fMmRÎÖˆ.;žìJS×>ÿBm޹IäXü!z•¦w™5ßÖÞtÏq°Ùçp¥ÀÉŒmê\8ö)ãTsÉ«âT§[ʇ¶‘Á˜Ûή—KSÎ\‹iwÖiÇ‚B™g~sœÁ>X¸ ÿüû{ùí‚gNó¨û´¯å€êÀ®ƒçå‡5Ño{z]ßqÒnx’–²~±:Ën”Ïʆº[ ¾˜ÏeI{ʺïásËYç>‹²Žp¨Ë˜8çÈ€8Ûì?âér»}ß¾º²ä@ÄÉv!6“éÙû ²bðù‰û4®¡ît횬,{ôÑúžÅ;³Ó¿víu}éÌ:u³ÉêEn—`y`K›ÿÌöí½vž¥òÃ1Õ¹ó¹m¡1w°ï=|xž¿¦Ùå»ß:4¯£1§Íeþa3L²ÐB¾8–ÿèDŸ¦+tœ<ðÑ«´ÂqW<Àsf¼Sù|) s <ó5ì#>…+R4.ͧ#h N…#¹œ ?8Ä5]mKá0¯ËhœóY²*Ó;ÄXÖAþãúg*ÄvÌ=ç00}¾U@'¼Ç_îÀ³€Îù2¥€^Š­w®_ߘÌïÝÒ?Û7ß«ÌøÌyeŸ²E¸ek S¶‡Ž­MO{]eB ¤!Â10„4<ô–yƒ Û"™§&sÖÇŠ¶£ žmiö8"hNû‹»vÕ^³ÅÚD\yß719™å׿¨»˜kg€ƒaJWZ5ùÆõ7ô.Eäƒ8ªFï‚v&­ã'›g™K@€$  H@€$  H@P@ŸB@ CÄļ/³²&Ø¿øUófIGç4[Ì¥â0kêÐÇÌr¾vÐ:ä§’€ÎúÙ¥kšÅÛ%Ïiè¥(ŒYå\ž¥€ž&®ÛòÁÌØÌó½9Œi èÄYФ4ˆ,Y ÆùÍß/ãÔL¶¼O9p`9€”'»0'î!úÓ ËÔ§_|±×6àº?§÷Û«¯xx/¨…Yð¬¥;ê1ÚØ² §½%äò3©ˆºSZh³ìâð²ðäP¶ §_C¯<ä³ô†B[~Í}÷ ÔñŸ]páÀ³¤­c²VþÞàa€úÛAÄtB9I…z[wÞs&’ žSßr}!Ú–yÜ“G»À6ß÷ÿ=úèþ¹Ú·ñ¥W^i°ÐÎ× N?÷÷²Rf&Oåóxc¨YpOS¶Ú7íônè߇û±¤FŽGm gÜpÃ@žsþkK¦Î,êøÇ>ËXR³c±E ÏwÉqnÔ-k§—! >ÃÒyhóJ·è‘^¶ª–Î?ÿþ÷qÙÔ[„Ça÷«GèÎ"ç¬xî{æ™cç'„BÄ¥Z^9¶éÙg,—Û·‰†Ð¯ë8÷˜W¨Õ3„›¶€pWË+–‚Yd®Å‰ ¤@^‹Óv ‘®¦ù'€´]›—bL)nç¸YPÌdz€^[g~o¸µ?ò¢‹„IÒ¹ô®»ÚðNtï9±OYKq3αåy…-}9!Ç+÷aוž–g™Þ8¿÷<õ´*<Ôêy¹Œå¤XžåDò—=ԬЉ“ë8“ ÚÚêw[Qþ5/ e¨ýFžewÍnò”Ý€·q íêjK˜¨0ËÐõMk{LÈj îµëøö·…YÔñyð¬M.ä9æoG[™<. H@€$  H@€$  H ès¨ˆ]5! íƒÖXä–˸ڀv-6wÎレ]Ý.ÖÕ®åcm£®uÜv¯|¼tëÛvß|œ<ܵqcN¦·? ž5¡3ß»¶BiM|‹ø¸Ø-ÝäÇ9¶XpF ®t šùº¼ùˆtf¹-י羥«óò~¥«ùœ×®ý쪸M|麾f…^ZRv]ç²E)“5J÷ú¯¶EÈÊBþ¸õ küšËû’ñ8¿±ÞùW'ŽÕvEÙb’Þ.âØ(Û¸®Ìç´<ËôÆùÝ&¾áA öã9¡,+ïò²ð¬¹æ<–yÏ¿ó$$&Xµ±É×Ä>ë ç€¥~mòXÄoÛþ˳]ª ôlÑvß|üü[ßññx‘—û¥'‰ò|æ1‹ýq¾iÔƒûŸx²ó¶µ6¹Ktž¶ŽÏ‹g鉄ç@Ým›°Ó Å“€$  H@€$  H@À6I@}!:· à¬O%Y[xö¥—z®¬ÛÒÁµy—`]H޼å-®Ä¹WW Ÿ5‰t2qw.{»ÒçnÉK‘÷ظIÎù},ü³Õcy¯iy’–¿åó`¢Âõ<Ðà¶=ò–ç"ÔŸn¿}ÅuÄA´DŒ=óÆ›®tH3‹­äáÙ­[«ëäëäk®mN¸bЕ9÷ˆ|”Lfñ›ü}õ¤ßöó¿Ûo;T¬@Æý|”3o{à çç¯7÷ÖËFàø¨Ë)Ü™#Ò²F0Ö€Xo­F@0BTdíiD¥,.,2?䃬ðÈą̂aZž°Çš¶ót>¬<Ô5&PÀàÁÍ›WíîŸåyžÂÜu÷ßß_‡w”ôY·÷ﺫW§V«>‘W&2`Q|óCz“FÉ-“ð€Ûo¼(°Ÿ…ÿÚ5{¼ÛLAìºí‘Gz¿™xAûAÇ•ö"–^hÈù§~ekûEÜ¿¼Ç$<Ë4Vó÷²ñœ &}PGñ4A;ˆ›sêî( 7x5Áóïm‡azôx|àúøsÏ•h¹¶{×ä¼2áe¬ã´£O¿øb™UK@€$  H@€$  H@J@}("#H@€$ õKà…—_°bÇ»‚A€$  H@€$  H@À¶J@}[}ò–[€$ m–nåqñÎ$5·ö,a÷ ƒ$  H@€$  H@€$ m€ú¶öÄ-¯$  H@Û4ÍÏ?ß_#<Ö ¯mùÓŸ¶iN^€$  H@€$  H@Ø6 ( o›ÏÝRK@€$°xíõ×\¶×ÄsŽýêÊ«¶QB[€$  H@€$  H@Ø– ( oËOß²K@€$°M¸{Ó¦æ”k¯m~wÍ5Õ¹÷Þæ•×^Û&ÙXh H@€$  H@€$  H`Û&°ôú–-[ší·ß¾Ùi§¶í'eé%  H@€$  H@€$  H@€$  H`®–^@ŸkéM\€$  H@€$  H@€$  H@€$ðw èV H@€$  H@€$  H@€$  H@@Ó4 èV H@€$  H@€$  H@€$  H@€ºu@€$  H@€$  H@€$  H@€$ð-Э €$  H@€$  H@€$  H@€$ µ`¾yóæf»í¶kvÜqG˜$  H@€$  H@€$  H@€$  H`n–Þ}Ó¦M=}‡v˜–€$  H@€$  H@€$  H@€$  ( ¯ñ:ðè£6ïyÏ{šÝwß}—ÄìK`~>øàæ]ïzWsË-·Ìï&Kšò‹/¾Ø<þøãKš;³µžø=ZOOÓ²¬g¯¿þzó©O}ªyÿûßßð0¬M~ß׿s3×€$  H@€$  ¬  èkã9µæò‹_übÏBÿþáZãxBÛ2{î¹§÷ްÄñÇ¿M¡¸ð ûe?á„æ^öçž{®¹ñÆ{ÿ˜¬ðÆoL|ÏgŸ}¶ŸV¤Éöé§Ÿž8M/œ/¿Góå»-¦NrÇwôÛ‚›nº©yõÕW—Å]wÝÕÏo´]wÞyçRåù׿þuÿÛp÷Ýw/UÞ™™x`àY­¥ïË¢¿ï‹|.Þkñ˜íUlio_yå•‘33‹4F¾™%  H@€$  H@  €¾ÈóºÅƒ>Øýþ÷¿?¯Û˜®Ö4]vÙ¥ÿž¬¥ÁñY@?è ƒúeÿÆ7¾1‹$;ÓØ~ûíû÷cÂÂUW]Õ¿ë$“‚H£ü÷™Ï|¦ë2Ï­¿G«~ßö‡?üáŠ6àôÓO_ÚR3Q©l³â÷²ˆèXŸ¿óïìå ôm5\vÙe+žÕn»í¶fp,úû¾fÀ˜Ñ± 䉦Ñ^Åö˜cŽ)½Y¤1ÒŒ$ H@€$  H@X ôÀ¾á†?ö±5ˆBü{ßûÞ×|ç;ßiN9唿ãÿxsè¡‡Ž“=öØ£?ø÷ÄOô¯ß¼ysóãÿ¸ùîw¿;ðï°Ãk^~ùå~<;~ò“Ÿ Äáš#Ž8¢yíµ×úñ–mg^<—­œÃòsíµ×6ï~÷»›Ÿþô§Ã¢.ýyDnÞ‰ 6Ì4¯=öXÿÙ{ï½Ò>ï¼óVÔýýèG= œñw¿û]ó½ï}o .V°ÌYöÀ»ƒ {íµ×ܳû| ?î{É%—L|OêCä=o?ñ‰OLœæZ»+Ѳßûï¿søá‡7vX¼Žh×;î¸æK_úRoI¸¾÷½ïm¾þõ¯7çž{î(I´ÆiûqÁå—_¾âŠrüà?h9ä†Aú+¯¼²yóÍ7WÜãOúÓ ¼ƒ¼Ã9àõà€XwÞr>æ½?-Ïyço–é×é“O>¹õO>ùd¯_ƒ{r&ô¼ãïh>ô¡õú[‹XÂãüóϯ¶[¼k‹¸+˜tâì³Ïîç±|‡î»ï¾^5Þϼå£ÏxÔQG5øÃš—^z)¥ºövñl¿-ìå+_é„ræò³O_þöÛoïÇa‡g›2î4ß¿´ü˜æû>¯>WKV—òð¥—^Úð-Ÿý@&„µ¾Q¿ÿýï›ýöÛ¯w-ýCÞ©2Ðw,ëic¥=Ë€çúå½Èy%ð7$meù>ð›z6J˜E£Üg™ã\wÝu½62þf€–r`âõ ×;âó·­A€$  H@€–€úŸ bEÍzª6@5Ð8‘<ÒùÚ×¾6p)BDœ+·_|q?î®»îÚoÅÁyòìCYC; –ñ|Ç­;ËVD&uD=µeá·¿ýí~Úå eÛ`a¶Èýkä­Ü~ô£]6”+òó·¿ý­!ŸˆI ì-"d+¤i\‡â5€L@âl+ú3Ï<ÓZ÷ʺÈo&/ ¶…Gy¤·ÞqíÚ8†x4‰(Öõ="?ïyÏ{F.Ë;ìÐ\sÍ5Åx×»ÞU½¾\ºä“Ÿüd5å[OÞ'¦å9w ü@€Ø²eKÿÙ¶ èx¼hó\uü¿øÅÜKŒÀ†¨E#OqïeЙ4Cžx¯Ê +L4‹üÛò =öØcçÎsÞ7àYñݧ¼! #¶•Ÿ¥*rhëK0ÁqžaÒïû<û\ó,ï¬Ó/ µç¼ÓN;µÞîµk˜‰(µ8;ðÀ#ÚL¶§vZ뽸ßW\1ò}^|ñÅ~Ÿ+êõ¨zÜdiDZkm[ûþ0©/‡Zž½ €$  H@€$°|ÐçôL~²5&–PÌ0?óÌ3›_þò—}1(X8?NÀj"®½ÿþû.EÀ ±)â|ðƒìY2A•|ä# ç"&;ï¼óD"J¤;í¼yÎ#ÏóNëžÛ¸ugÞù7}»¨'tÒ¸—·Æg íüò—¿¼"ƒ˜ˆuqoÄÄæSO=µqá«_ýjo`+¶bÍ»lXI [ýO+ çÔ?ýéO÷žÕ¶" Sv¼†0щwœzJ}Fàáß¾ð…^½ŒúËaŒÁë2ðMˆwxˆEx)Á žPrÝFôÙºuk™Dçï®ïb­ºûî»é|g(ï&ï]T&¯ä9ÂYg5G®Ã‚á ‡«¯¾ºwœó”#Ø|ðÁ9ښߟ–çZÀZèñŽõ&õX…bŒ•û Ìî½÷ÞæùçŸïÌÒ¬tú°´†.-qOZá:3ó÷“X Â!«VçkiÌk0·Ðñ~µ­=K>#ΰeò`:kma–<Ë{0)€ºÒei\^3ÍïM›6õê5.£q/ùÔSO MnYô^x¡¹ùæ››‹.º¨çž5Þµ¡Z‚1y€×2`åš-¯k.[³¶ï¾ûöÛ™H‹úœÅÚÛQCþ~t}HÉcñ¾•ü_}õÕže^œG`Ï!{sáBÔÏ%†]³ÖÎOóVV¸2ÐÏÄÞ÷®v®v=Ç&i3ÚÒÊÇ©/Q7Jw;„Kâ0ᢠwß}÷À„)ÚþQ¤ß÷H{ÙôxÏ„yçÚBÎw¶¬ø,«ÏcÔI|xª€;é=þøã‘ÔÈ[ê#ùâÈ?¼ÊŒÒŒrƒ³²€Îu1–I©£„0ñ4nXÔ÷}–}®Y´ãršGüX2&êu‘|XÈí×ÔÖ Ï“VfU_Ûòuá…ößË\–IÞ·ø;s5tÞy&ðÞç%ÈÚÊ¿,Ç?÷¹ÏõŸõ¢6‰¿—¢ ¥=VûoVò·^Þ÷a¬=/ H@€$  H P@Ï4f´ÏÚyñG1®XO×Âá6’ŠQ.H#}¡¶pÆgôãuÍlg0:ÒËÖìüqOú xŽóo’A÷¶2p|–<)\Ùâ2ÊÎ@k×\ýr,[‘p=B!®±ãúØ"l·=sÊÀxã<õLÂ(Ö•åsà÷¨ë+"ª0‘¦ÌOÜ—|tµ;¥€ÎóÜsÏ={–QäƒrÁ°&r•eÉ¿CDÇ熠ÜVÄ’Q&ä|¬Æ~”½& “Êϧ€Cã\—D›ï ñBG £~H«Kð{E;Ë3ËoX”#·)¬ÝÎ`5kŠæ@ÛFüRˆÏq&ÙG¬¥Íç›È«Ú»:Éýó5ÓðÌéÐ3À|ó–vÆ]aÚ6#ÒfíW´uÜûóŸÿ|ÃÒ]z,gB¾ö³ŸEr+¶YÆ[CLâ˜å÷½¼i¾g—:߬p­žùóNÒ†F`bHÙ¾#`ã2zX H{˜(–ó]йc¤÷ðÃWoÏ›Z߀ë(׺„|ÅSFÛ²ðÅžIgLÂJ—ûr}&ÚÑö…ØX¶Ÿ±Ôý„X žv§üŽñ,([žÈ´,ß÷Yö¹¦m32ËÕÞ§Oõ!3&™POèu…üÍŒ:]NmÐçñ7V·Ôsú|Q®I¼°D;3¬­èâ3IL$¡ÏX²¥,|Ž<òÈï)mLô¢¼lék¸ µöˆ÷–:=«Ï!ç§ÉÉ[œ/ÏE>–áoVò²žÞ÷`ëV€$  H@À¨ÐG%5F<þð?ŠKK©2Ü‹—?þG ÂÇæ]Ù¼h^¸>Ë8G}t¿QžQ¶]"MW~ÛÎÍŠ'“Úas¹`vÞyç d§¶v°Íׯþ7¿ùÍëóDúˆÇ–¡Ú@é—¡MÌéå}\ض…Ò;.öáU€Š:ñº¶µA¬,†u]ç`1Ž…kθ¡«+~øáý¸mƒX‘^›˜0 OéH?o±­ F&É”¡&ÂTw²¸ ô»¶mƒY@á·-& `•8Jˆ´FЕðíþ§n]vÙe£daÕâDÙË6:2Ä»e,ãäI>ÃÞÊ#QÜÇù‘×Q_þ#¹ía’N¿öÚk£èïBU„ø®cM×vmÛ„æÈ[Û–÷jÖaž‘—胴å;ŽSÛÚàiÛ ÒeDÜ«¶ cœ+ûUñ-f;ÌsKöÆÀDÂ,¿ïÁ5¶YˆîÐjåæXžÀ9äø£,—Á¤8®…SÎwÛ7/{ª};ñj3JŸqšïF-d0—·ÜÏŒÊtÎ>ûì~_¸¼Žß|3£OS èLȈkbÂb|Ë}r;œÛÚeø¾Ï²Ï5‹6£|F«ù›eLxž¶Lx+žrâ|ÞvµÏeÃ~‡€Îß{Ñ'/ÿ& /Ë߬ëí}öìûôyÂK9\™â¶œu0cÀ‰s¸d/ƒªÁ!0‹Xpq,ΓVÂ2/â Û–Ï4§÷Å/~±w/ÒÄ¢° œ™y¹Î0iW¬ÂòýHd …w˜„e7qÊ4`Ÿ¯¶Ï$ƒ6ñ&—‰} ‰Oš5+þ2~ o«»¬ÓLz1ÀZ¦Áïixrý­·ÞÚs/ž'XD9¸7uƒ‰{ï½wo€Þ¼¿e <ð?ÿüó{k*ÛQtêq”¸og’ú¼Ç{ôŸ[Û:µÁLÊõƒÌ”/¿'ˆÜ£ 솈<ª€žÝš3€I{ž!( ë“XŽjq]ò^Äï({)ŽÇ½Ï<óÌ~YvÛm·8ÜÛÆ{H‡Ú£`Bò7b”ïÑ0Á—úuƒ:“ËlDÞh{´CqŒmèá®6É$§;î>õ>ßsÔ}Þ«Y‡ix’—<)r`ɽqãÆ^6é» JGη-ñ2m›‘—Çà>½—p@uLþ®åöÑbXÈu6D&®™Å÷½vï,Dh°„¥}:Å÷‹ë³gÞÞµˆÃ{·cXx衇ú׌â^<ç»& S?rŸ*êMäƒ !J“Wž ý$Úz„#ú1É…óìç 3‘˯pžvõÇùŽṉ̃ò=þøãûe*Û‹¸a?X±¥oƒG&}„ȕϗzS£OGýË×PÞâxù=Yíïû¬ú\³j3âù¬ö–~%ßFžýÇ¢îëCƵLÒh[6¨M@ÏmQÔ›Q¶mýÔc=¶_ÿb"QžLzñÅ…;¾Åù»:VMÓÿž’‚qÜôõ˜„ƒ…9ï;ù œgriüÍD›xÄ¡¾‡À^æ™´X®*&‘Ö¨šÊ´Ú~GÛBŸ9{٢͊Ð% /Ë߬ëí}ön%  H@€$  ŒC@}Z#Æ?’e`5¬Q¸%0`2¸­+®Ä9Ï ƒe`° ÑqþåÁÞ2½I~Ï‚'ÿÁ5éCH+ó“­ÝÊÝâ3[øâÖ3îSZ±“V¶8a°¼"¯_e`ðùü Ë|øÄ±¼¥2øÍsjsÎóŽr|ë[ߊK¶ GòÝ*ñšk®éŸg"GëÉoNA(—!öËk»~3!%òU{µkóä ê| DFz=maZž‘niíÁ``)”Sïʺ×ç-…‘÷Qt®çºZ Œ1‰£­ *t„ê².òÌcmY×å H IDATòÇsBDE@ÏÓôåýã^Ùõ3ƒ©Ë¢ì0§Ýà‚ ƒÄÙ2,CÌ¡,y€¶Öž•åÍñyÎ]º0î÷( ¾c”ƒo°x¶žrÔÜã|¸kÏâׄØÂï’GWyÆ9ÇÄ«q¾‰míì8÷¬Å†'ÏÑ18µµ•LªîÄí%jyÖfä> ÷aÀ¾ 'žxb?Ÿä! èù;Ã$a!Obc¹•¦ý¾ç´b? ÑÃt®á»¼é—ÕÚb&6Äs£­%ävâ‰'žzIÎ7ߤhwàÇ„‰,ž×Úä<Ѥí¹ðžf/!¼Ïµ€OÜZ í`¢aăe®ã5kb–WÞ¤S èL¤Œôc"Ë Ä1¶1!!‹õ‘‡r»šß÷iû\³n3J6«ñ›É†ñ,ó¤Y&ïÄqÞ…¶u~FL8ÊÂÚtÒœåßXaéœû2¼!JÓ'Äuµoñ¨éŒ“FîÆ7¾¼í@öª“ÿ>aò ÏŒgB]€€ ¿œ ‘›¿ g"mtBL°…Gü‘û[¥ø;ò¬Ö߬ëñ}Ÿõs6= H@€$  H`Û  €>ãçÌÆ1èƒù³ºE¶ eÍǸo^ *äAHÄØ°²h³&4Vk;+ž!ôðl²ÕwY. ò€.E9äv,#Ê€[<ÿšûô"=ÚòQBÎw\[Û’úeˆþíIWéGEº¼÷múŠÎ$6úNLÊcÒMöØrûí·\ž'ÓQÚB~Î7"òÇ·™v)¾ôñ8ÖïQaÒ–áû>iŸkmF§Eɓ儱ì¥tÉžóõ=IJ]zNkšýðÔ@}¤?›ï|Ôár¢kŽWîG=_„€žÛ(&çÔ¾ÿ‘?¶ 43Ðs܆¹µžV@gð·-/‘'¶¬[ Y@gb@- v’냷…IskéåÊšµYíŽñ>…pXZV‡Ṵº3-ÏÈ[`ŸVü›D@g2š\jû‘ç¼Í…Ã&³Ð½uë֜̊ý‘GÐà Œ<3Ùõ/—kËÌ[À({ÎkÞ§Žò•!{O`âÃ(!Ú¯a߯I¾G¹mÍùÏûaåYËkX˜19É*qmqXÄâå#,A)ˬmBß8ßD,­g¦áb_0ìzG¢NÁ² Ó´1)bسʓӲ€ž'¥uy‰<31-Ê\.w0í÷=î‘·YˆU@Ç’5¾GpÁ;žD"ÿäw”°ÿþû÷¯ÁMò(!ç;îWnqÀ\†<…kºêç"ÝÚD*ú¡yÝúˆ[Û–Þ °bx¥¸^æ9êx) /¬Ø±4´AùûÊûkxî‡(ÛVûûN¾&ísͲÍhã³Èãùù1‹¿]ò¿üÖ¾¯ä5úÐ! s ësêýÞåÜ/Í“:gù7–ØQ×Ï:ë¬ràš>Î1édÔïÄ"ô<ñà§?ýéÐ,æ>Qxx¡OyŽoAþ;áå"OÜay‹Y‡R@'ýì¡ O.mú²üͺÞÞ÷Y?cÓ“€$  H@€¶ èsxÖy@°ký8·ÆÍd €0h9N@\ˆk³ø„õÇkƒ–¤Ï@K\7Î1g–aZžXyGþG±¾ÊV ¥E[`oüb§& Ã! ÄžÈWmË \Wý™F@g@,ßáak7¬#B#Î(úE]T}äaݳ(ý£ýh¯\ |Öö«™üûÁ<ØBG¶Àèòú0 ž‘·<ÀÇ&ÝæâQ\¸ós½`x×]wí¹¨ÅU.íGÔoâÕBÐÛ&VÄuy` ª®¦¹ «Åσ’¹,£ì³dÀ2†(;b#6ûü,pƒ^ 1Àϳê‚SçI¿GYðe‚í“·rßæº•¼Ç{F™bXL `Àœ¼Ãˆ5ÒÙ/­ ‡•”óL¤ Fãlc}ÝQî1jœixæönœrà‘"‡iÛŒ¨Ã]V»Ü/÷c²€~ï½÷öŸmÕ°…šRšÅ÷½¼¢ã»RÆ©ý>î¸ãúåB˜%dësÞ™a“츆¥y‚ñg?ûÙÚ­ªÇr¾cR ¢OCÔÞ½Zˆ?NЏeß‘oI´_Ä¡ô[ù!f3‘%DNΗzô?8×&€Fá£OVÐi Iƒ²‡kåx&á‘—ß»ì²K/^m¹¤¸Ïj~ß#“ è³j3"«½ÍK*ñ|»þµMÈŒú™ôü7Gµ è³ú‹ïh䣫 œ£P{gkÏ"Ú²¬Åm;6jyýö®¾vÜ'/ÝÀßT¢} pù}£üѯ‰þíèµbçwî·Käµ+„ˆ ]qCTáÝ@àöÁg˜{þ®ûÍû\”=ô¸ÖrÑŽSfDÆ2„ÀC<Öíî —_~y?=\ö·…I¿G¥àé³TF ´“϶6)[ǡzh/™¼–v¸WïqŸYl`Çù—¶}˜ì$y›†gngöÞ{ï¡ïÈ%—\ÒÜpà +&&MÛf„u/õ·+dQ!—â&“>“Ä"~Ù?›å÷=Ê’…è¶zqó–ïJ´c¼x“ÈÖç£ö³òòÙ­q¾Wm?ç;tâ11-?sê`²›|&k/¾øâÞ’!¥Ç•,’1±¯ö fN<ÏR@6‚ó>ø`™Íß!@ÖtÚBÒ Ü!´ó.B„Eˆ‹¶–w«-ä÷®-ΨÇÇý¾Gº¹Oõ?-Çù‡è²VÞtµ¾ï9“ö¹fÑfä|Ä>m8+Åw•í¸ïJ¤5ê– Mñ¼ ÛÂw¾ó~<Þ‰2D¦Ðqà Dî7Ê{[ÞcØïhCùÛ¢-ä¾ ñG ‘ÿEè,ŸÏq»‹S^ ¶9àE(ÒÉmÏ#ÚÂ<¡¡¶|YNoÒý6ôb¢MüE~Ë6cþf×û>)S¯“€$  H@€$°ZÐç@žÁÃl¡ƒ ÖDµ€ÊÀmÙz<Öz«¥5ì÷áuþhü"F KwÞçgÁ3[21xƒŸeޱƉÁñÊ0í;¢:é󼄮…,J°.c[`P+òŠu^(#bhÙ²+»“¬Y™2¸Šv¤=o|ǽÚ\Ì"ÊòlÚ&0€i  KNùwž¼`¤Û5˜;+ž‘Õ`Ï–ôm¢ÇI'Ôç —ZÈ:q~ÊÁP, óúçX 1H§vZy‹uõ{žÔ‹HàÕµF;Â3ï>ï)V“fÑfd i„á§žz*’ïo©ÓñLÙ–zö>‚ˆTë'QOȤS›Œ8í÷½Ÿá´“…è®ïHº¤¿K}Ï"Kä}”å8H$ E] ú7L;9ߥ€N´èË'&0”!¿ËÐmv€É:ûì³OSz{Ô&>"ðdq²Ðóz÷¤QãÏ÷*O¼© èˆ[Áž-}ƒ¢?qH³-¬Ö÷½ÌOäuœ>×,ÚŒ2üÎ"5ù¢]štd-ýÚ1,èã^¼gm!{SÀ³I¢þ•:ñ²7«à¿WeZ“ü¦½Œ´»&‰ö—¿üå~ÜQ&NÇ÷a:ùËïQ[Yh/Xº!ÊÌß×eˆ‰€‘ÿXÊe·Ývë?óxöm –iŽû;þ~¨MjÌÞr¢¥€¾ ³Îë}—¥ñ%  H@€$  ¬6º²Ú¹J÷_‹.ÜÉ~ž¹ÍÈ 0ÕÂ)k¼!RÄÏlk‚*îÖ#>Ó¢H+¶¸[ aZžX‹ÇŒ~Ê΀ ƒò žÁ>Ö¯ 67ÞxãEóšÍ§Ùº•´¸&®GhbÀ6˜rÈ!ýóÄÃE{¶‚CÏ¥Äo ˆÇ½Ö°˜gМ¢͈“£²% ç(VÔ0FÌȃɜ§î^{íµMpC´Ën#Ég¶Ö‡)ðá&QKš Vʇ{Ì( Vw¸Sçù`=›ßÄj!»ËÄ’eÒÀä…ü êt…Yñ¤¼ ¢!$ĽùÿÁ<ÖõŒú“ãçõ~±àÌç¸_¹æwX8rêá…^Ø{ާŸ~úŠåˆCù¹S/ò=£XÜ0á‡v(¿‹D¼;9P¨9¿Q—xwóqöËÉG0Êea@v܇X¬ñ‘ëy-ó‘ó„ÈåaK]ÌïŽ;«}ÚÊùdÌK‹ÏÌœ÷—8ÔËÉ‘wòM"#,«ã<Û6‘}Òïõìšk®˜˜ƒ(WÞ‡¼Ä 7ùàûôÐCE1zÛ\wr Êå ­Ç0+žgžyfŸÜùNð xGx¨GL¦ÊÏ„¥CrÈïÙ$mïzLîãÙÑþ~øáÍe—]ÖkKÂ~®L”('ewݼ¼7|÷)K¹v7ßÆÒƒÌ,¾ïp‰w6Ú¨l½Œ ÇÙÒ&Qþ®=3À€oiîS´]Kœx6]–©åõ´¼79ßhôòDPò/¾ÕôA"à2=?3êý,ÒçßwÞÙ°nxž BW¤±çž{öÓ C»Ås§-Î"Z܇u”ÉCfŠ×8Ï6„zúOô[rÝæ<“èÇea¾ONƒ¾|ÙËñÊçÃw‘w‰g¾šß÷œçø¦ßqú\³h3r>ØÏõˆüÐW™W ÊRÑÏc’Ïï&e _Á¹¨£Ô¾c‹Ô1þž‹úƒG(ž/“6âùS‡âŒúçÊ{Mò›¶:–r"}&qÿ²ŸM~ù[$/¿Ã»uÿý÷÷o˄ʖۧÈ3Oäãì×ú>Ó¦Aû÷d‹.ÞwXSÚËèržvœw« y"4ñx6„ì2ãxSšG€kLþaÂ'¼È{þæ ¬“‡øGœ–åoÖy¼ï¹œîK@€$  H@X Ðçø”ä‹Ašø#¹mËÀ`-dË K¦ ¥ÐGÞò@ã4i/âÚiy2뿇۞VB9Ô\sm˜ÎW9]ò![Ææ8<‹ˆ‹ãä5D®¸>oyv±¾_\SÛ’.ƒÕ9dK¼¶k²PEò¡­^‡h—-#súm®‚<ÊñÚöÈ+C¶ÀùÚ×¾Vžûw¶ü ¬q:,L˳dÝV~Ž#.ÕBQ»®ÏçJë³r€1Ç}DŽØm >çIœ+ëtÄ-Ó5«@,Â#Î(Û\7ƒ âl™Ÿ®´† Ú—ÖÍÓx:ˆ~ï¿ÿþbtm®ÇyyÎÏJ@Ï^—â¾±-½îä%."NlcÒ`¦ã\×¶ôÂÀ3E´Uù]kË}»¶ r1ñ>®eá…^x¦çœsN®Š3Ùïª7Lž‰@ÏåÌífÄY†¿YÉË,ß÷(›[ H@€$  H@k‰€úœŸÖ°á~7þ˜-åkð ‚Q¾nVßwêiÛû•ó‘÷™€ÐH3,`±ÔU€ã}æ>\›­©»îʼnpÎgÛ÷›>M|wØæI ¤G›‚fN+ï#îrÊ)­ßúµºI»@?ƒÛ=òPº‚§­Œv!ß›¸x¢N”mm™FˆöµÝ´eX¦ï{ÎÛ¤}.Ò˜¶Íˆ|À9?“ZŸ$âÎbÛ6‰•>Jöì‚õy[;‚…3âkÎwìÓ Á6ò›'·Å±i·ôm¢Ÿ÷fK.½„äïiŽK[^%½y>ßµ_ÞƒòÌ" Ò¡]g@íþQ¾Z;ž™f7î¹ÿ“Nío£œÆ$ûÔ¡Z½á~¥+ümÆ(ã^ÄeIªrâ-õ Yµ‰{qí"·xJ¢]ebÔjüÍZ²˜ö}/Óó·$  H@€$ µ@@}‰ŸP3Ý.qvÍšV…PtÐHbûªdЛ®y.!H±g wÍþïð{´^ž¤åX+6Ãú¼\|Xè7ÒE˜––ç%0/Ù³ ^i €$  H@€$  H`™( /ÓÓ0/€$°töÞ{ï¾€Ž›Rƒ$ y`}ô˜¸Ã’* ¬'xB‰úÍ·é H@€$  H@€$°LЗéi˜ H@X:XÆÅ@ÿƒ>¸tù3CÀÚ'ÀÚÊX䲿7ÿX77ÚŽï¾ûîk¿–@'€Kó¨ßµµã% H@€$  H@€V›€új?ï/ H@KM7ÈW_}uoΥΨ™“€Ö,øÃ}A1„ÅrË´ ¬O=õTsê©§ºÏzy –C€$  H@€$°Î( ¯³jq$  H@€Ö[o½µ9òÈ#›#Ž8¢úï²Ë.[[2·€$  H@€$  H@XÖ^@ß²eK³ýöÛ7;í´ÓÆ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@ëƒÀÒ è›7on¶Ûn»fÇw\Ä-…$  H@€$  H@€$  H@€$  ,%¥Ð7mÚÔÐwØa‡¥h¦$  H@€$  H@€$  H@€$  H`}P@_ÏÑRH@€$  H@€$  H@€$  H@À”Чèå€$  H@€$  H@€$  H@€$°>( ¯çh)$  H@€$  H@€$  H@€$  H`J èSôr H@€$  H@€$  H@€$  H@XÐ×Çs´€$  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@냀úúxŽ–B€$  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 èSôr H@€$  H@€$  H@€$  H@XÐ×Çs´€$  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¶ß~ûf§vZÄ-…$  H@€$  H@€$  H@€$  ,%¥З’š™’€$  H@€$  H@€$  H@€$ uG@}Ý=R $ H@€$  H@€$  H@€$  H@“P@Ÿ„š×H@€$  H@€$  H@€$  H@Àº# €¾î©’€$  H@€$  H@€$  H@€$ I( OBÍk$  H@€$  H@€$  H@€$  H`ÝXz}óæÍÍvÛm×ì¸ãŽë¾’€$  H@€$  H@€$  H@€$ å!°ôú¦M›zú;ì°<Ổ$  H@€$  H@€$  H@€$  ¬; èëî‘Z H@€$  H@€$  H@€$  H@˜„€ú$Ô¼F€$°@/½üzsÓ}Ï57Þûlÿ¿_yíæÂ[I@À¬Ü·ñ¥~{m;Ç €$  H@€$  H@«O@}õŸ9€$  ´øëýÏ5ïÜùŠê¿#ÏÝÐz'$  H`9 |úÇ·TÛtÚú-Ï¿ºœ™6W€$  H@€$  H`" €>ã‡ýúo6ç\óDóÃSîk¾ÿ»{›œ|osô¿=ܼñæÛ7ºsà Íag=Ø;OœøwüŸi¶¾¢5áۤܓ€$°XϾøZó›‹ë·ËÑ>ïwÊ}=KÁÈÍ3/¼ÖœtÉ`<ÚûãþüHóò«³mÇyþíBËÁ¿ ²äVÛç_z½9꼇›þõÝÍ·Ný}´¿mxa›ãµ­xÓ[{õ"ÚïÃÏy¨ÙðÄÖjñ_Øúzóë‹í·ùÔ+ïxºw–Û&Eq¼-¯³¼¿iI@€$  H@€$  tP@ïæ3öÙs¯{¢*t ˜Þ|³iþãn©ÆaÐì_/ß8ö=½@€fCà ÓhmŸÿÃ?^Õ0IŠpà©÷·Æ;ýÊM³ÉÌßS¹÷±›ÿë+WVï§€>SÔ&¶ÆœqÕ¦ê{Ñ%Nƹ¿c•ÖìŽJà¿ÿà¦õâÿùúÕÕÉM»u犸Ô–͘gøÇŸÿ­z_î­€>Oò¦- H@€$  H@€F# €>§‘crÙÆÖ±ÇŸ~¥g‰Þ&„0h¦;Þ‘QQÀÌ ìû›{ZÛpÚèÿ.ªìyü¿·Æ;úü‡gž/ÄD•3ÿòøÀ}ÐgŽÚ׬Ïy/'ù÷?~ø×5TR³:ÿºÏõÕ:Aû™Ãæg_­Æ£>=õÜüݨ?öÔË̓omðp’ë°z~JîK@€$  H@€$ Õ! €>cî¸ÿýê/êV%{sWïn¿ºðÑæƒÿ|ý ‹Âÿó³Û›M[^©æˆã·<ð\sÙm[š?ß´¹¹æ®gÆÜÃ¥åU{ºyúù×ú÷`€c¸•õµäg¾c宋oºïÙæö‡ž×åüJ<‘€Ö0{}±ùìOo36þËž×5'\ðH¿dw=üBó¿»mE<¬ZŸ|fe;NûÊ5×þû3ÍÝÜ\zÛ–^ŠW’qÂõ÷<;pÏÕбĿÓK½òlÚòò8E0®fFï>!<29ñCß¾¡÷/ŽÅA•sX!DZÿ¶ï 3ËGWBLºá½§7É2=ö¹ºèÖÏ]rëSMMDÿÌA· \pÂöëCÔ õrϳÔ@¢ü`ÒUÜŸí(z䃺e<t/‘€$  H@€$  H@C( 4éijó`Xìÿû#/ö“ü§ãîêÇaP· ¬ËˆUJðtb‹Xà ^Xd1q¸Åñ›íOÎx A°ÏǸæ†{žÍIõ÷$üÊ‘w ¤×’¿ß]ºq&ƒˆýº# H` üÏýþ:Ð>â]¤ ¬už=аs˜lô‰ýÓŠ¶“íÚãšæÜ0²»àqt¼Íîà[òH;ÏD/bÞ'¸yàüÿýÿ]Ý*ò!öã2›É¹ ìà "ôäû»›á¸®]ß&Òsïq]ǽÜJ@Xx)Û¾-Ï®‰›ÛbD¸2œ{Ý+Ò@hcýtÄ»2ýšp]¦YŠB].ܱ4/ïÁïÐqû[;_æã•×Þ¨ „ÿq·¿´Š¸6H`Þ˜xuøëGßÙ¿]Ù ý›ßîÓ„€^zã‰ôØâ5ˆ¥ ò±¼ÜŸßZÖ{çã£îŸ}ÍàšÜö¹úpêËoßR}&x ÿçGªçpò½ý{Óö±äҨϓxL~Íáñ§_Yá½`Xz¥€nœ‰º/ H@€$  H@€C@}Nœóàm)„3‡Èðm>ßü®\¢ãßn|²_˜Ï²rÒ+í%ß“ïþöí ¤ÿJÝ6¸Ó H@€$  H@€$°0 èsBo±RÌVR áF4 5ëÅÈë5âþ7¢ c±Ä@l ´±-¯Ç…n>ÇCÏ÷’Ë–4qMi…xú•›zq¹ON#›#_±-xÖe7H@XëJkÐNÜ+÷ÁxÇB@ç<“J˜¬’æ$ö.ûÊ‘w ¼X0G`ba~WYK½ µIŠyyâcÑžÓaŸwš`Ÿ«$:Ýïì®δ¯Áþ‡iR}Û<™ '\ðhï¸}®*®‰ÒžæçÙ¶¤“™²uúÛâ÷Ê5Ò7mô"™+­ÝYïœPN¼ ¯^ âZ¶¹oNž³€nœI¹/ H@€$  H@€G@}N¬óà-:µ2ó`^Þ/tÖß-­ùrür˜€Ž{QB¤û_¶è·¬úÜÁ·¶æ·¼þÝ%&õ2á€Ö–ÐÈíb"z>v乪¥Ám{Ž7lôRTV†|QÉ Õ û`ÔÉQtòYŠÛžÜÚÜõð ïñ/þXßK¶£¾ô/·¤÷/g?Ô‹fŸ«Fkòcys&°òDêÇqáO?=Ú.ž¡ÖöõNTþ«y u%Òe›'eädþù׃ÞH²€^ËGN³kß68Sv_€$  H@€$  ŒG@}<^#Ç΃·! ³†9V)µÁ®R@/-Ï9Ôy÷\}â~ô†{žHkúwN\›‘5"þýÿÈc^7td`F”€$°„n¼÷Ù6- 3´ç¥›fŠq÷£+]5ã*ý×>Ñ<øøÖ^;yø9 ¤½(±'‡ü½¢<Ù…{é¡„ó?9½û;pØY6g\µi…‹ì|O÷%0OeU@/-‰± Ç¥wôÛ\cÍò2ÿ¥€N[Q e?0DNû\5Z“cbS~n¤Tzgb2«óˆû‰ýÿÚ;Vkûºn~á̓ë­ÿ‡¼ª—FiO?ºòýÉGÐkù° ®Qô˜$  H@€$  H@˜-ôÙòì§–oC@çdm­CËJ=»{çÿá~â~úÇ·¬(÷Õw==g^z’È bO&dEYb›tâ•åÿÍÅÅå+¶¸I>é’ÇškîzfÅ9H`QrŒz=ª€ÎzÖe}÷‚íþõþÖ"”:ñqáÃiW ö«ˆb©}®Ljú}Dæxv!hÓ‡Íð.@È“V³È]Ö…ox÷ŠŒá’kâ^lÃ}?}÷|œýrMÊ>Ô‰¸Y™Ûà ãV€$  H@€$  Ì€úزvyìúà?_ßüÛO6O<ó–åJéB’5âçP¦eÁ/½üzsÄp#þß6¼5Èá>¿t‰Å:!»pÇÕ:ë9–Ö-ßýí= k¦³îb™ÞúÊ[B:®æO¹lc“«¿ÿ»{sQÜ—€$°¦ üø´ûWˆ !Š`ÑX ¿,tœ<¹è¯÷?7ГâLiÝÊZË×ÝýLsÞõOöþræƒy¡MŽsX²ÖÖè=ó/\Ã7éÚ¦A<ÏkÿF™.øëæëñÒ2–ïî鱤€›aòŸ¿ˆé ,‚~xO¨ëüË“\¨×¼§?÷º'zK0tå©ô0ï[ÄζPй懧Ü×ë'ñ~ä´ØÏkìsµ‘ÿ8“€hƒ7íÒï##xÓO ¡ôÍ?úý·×¥§?ŽBÙö‘}äg^x«m{äÉ­=·ìqŸØ2Q‚@_™¶?޳ýO{\ÓÐþ“{}±¡=ÎçÙ§®R§#”ù° 2n%  H@€$  H@Àü( Ï˜-¢D9¿±gÀ¬¶Vb) çÁ¼¸žA¸l9Çóö_/ߨ´­£ùÇëŸ\!Øp-ƒ‡9 öq'I(-¢r¼rP0Î) Ï¸R™œ$°ª[¢}+·m–­WÞ1h]×ý—=¯[!¨Ä9¶|'"d(ÇkÛ§]NºK/–Õh‹ßvœIXšÈw-.BNÍsœ™"·˜ÖÏu¯k³+0‰¥V§ÛÖ4´Úô¶¼p&±ä`Ÿ+Ó˜lŸ‰Jm̦sx²ÒÿkñÔ1¬í‹¸y[®sÎ=óùQ÷Y: °|Ôê+÷± ‚n%  H@€$  H@ÀøÐÇgÖyVy]ƒc[žËz©ì-t¬¼»Òi;÷OÇÝÕ*²ÿËÙUô›î\ã—´±NpÀ©íÖ—e>˜fÙ  H`=øoûÞ°¢Mf²R[Àz°të[¶—m¿Ã[ i· ×m×r¼f…ލÓuMy«È°”6+§Á:è ,ŠëVçú7lï9]«ñ2Ò­vyý8:}>ú^µ`Ÿ«Feôc5·éñ,±ÏᪿÕ':ÿÐ3ßjÃÆiûðòTó¼Aß<ò0ê6÷ÃÉó8ùà¶ÁùI»/ H@€$  H@€Æ' €>>³Î+°&b`´6@–­IÒ±Hxå@7Áj%lj¸XšìwÊ}=Ë¥ì:q x„òˆ[ò„ûÊݺsàƒÎ¸kÏÖä¤Ïú½9`5_s=ésïƒÿÀ ÷Ã9 ÷%  ¬Uˆ2¹m§m¾ã¡ç;‹ƒŽ7h'óqýâ[žêµóqœ¶—µxsèr'×åí?þ|ðúH Kùšˆþ~v{oYŽœeciŽ20)à¨óø^äëØÇbÞõÏKrþž7³¯y¼j5^ÖOÞ±a–ääµayO†…R@§OÄRÿëÀ›{míǽ­·Kñtû\]tºÏá£6y‰~*Ï5‡W_{³7a´¬'ôÉÃ;ñYJãÜÐÚö1y”åºÂù7{¦¦( H@€$°F°æ9n¸q—´ —ÚÿuŸë{Þ‚jÅÁ‹ÐÿÜï¯ÕëH×í']òXíRI@€$  H@€$  H@KJ@}IŒÙ’€$  H@˜/M[^p×¢y¹ýÖ‰wW3Âñ2nùÝ  H@€$  H@€$  ¬ èkçY™S H@€$ xõµ7\¶—âwü>ôÌ«wýù7 ÐÿÛ¾7T¯õ $  H@€$  H@€$°œЗó¹˜+ H@€$ ¸íÁ監Î{¸9òÜ ÕÞ¼¹yùÕ7ª9ÙúÊÍ™y¼9öOTÿqî©ç^­^ëA H@€$  H@€$  H`9 ,½€¾eË–fûí·ovÚi§å$h®$  H@€$  H@€$  H@€$  H`]Xz}]P¶€$  H@€$  H@€$  H@€$°ôЗþ™A H@€$  H@€$  H@€$  H@XôEPö€$  H@€$  H@€$  H@€$°ôЗþ™A H@€$  H@€$  H@€$  H@XôEPö€$  H@€$  H@€$  H@€$°ô–^@ß¼ys³ÝvÛ5;î¸ãÒÃ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¶ß~ûf§vZz˜fP€$  H@€$  H@€$  H@€Ö.¥Ð×.Zs. H@€$  H@€$  H@€$  H@k‰€úZzZæU€$  H@€$  H@€$  H@€æF@}nhMX€$  H@€$  H@€$  H@€Öôµô´Ì«$  H@€$  H@€$  H@€$  Ì€úÜК°$  H@€$  H@€$  H@€$  ¬%K/ oÞ¼¹Ùn»íšwÜq-q5¯€$  H@€$  H@€$  H@€$°Æ,½€¾iÓ¦ž€¾Ã;¬1´fW€$  H@€$  H@€$  H@€Öôµô´Ìë 7nl¶nݺâ¸$ Ùxýõכ矾yå•Wf“ ©Ì„À–-[šgžyf&i­f"/¾øbóøã¯ffrïW_}µ÷ž¼öÚk3IÏD$  H@€$  H@€$  H`õ( ¯{ï<%~ðƒ=ïÛo¿}ƒÈg€fOà£ýhï=c) „ôy„ú›o¾¹¹ñÆ››nº©yùå—ç}Ë5—þþûïß&ûÛßÖ\þ#Ã^xa¿'œpB^s[Äów¼ã½²¼ÿýï_sù7À$  H@€$  H@€$0H@}‡¿Ö½ø÷ÔSO­¡œ›U ¬;í´Sÿ={úé§çžñO~ò“ýûñ~yä‘s¿çZ»Á‡?üá>£sÎ9g­e¿Ÿßƒ:¨_Žo|ãýã‹ÚAø¾÷Þ{› .¸ 9ú裛ÓN;­¹å–[š7Þxc¬,`uߢw¿ûÝc]kd H@€$  H@€$  H@X> èË÷LÖmŽ®½öÚqá§?ýéÔe|óÍ7û‚ÂÅ"„½©3mXƒ>ýéO÷ß5Þ»¶ð§?ý©ùîw¿;ðïûßÿ~sÞyç \‚@yÀ Ä㺰@FH 1’íÁõ©O-vs&\åzëû¼?ùÉOš£Ž:ªùÃþP}Ïîºë®†|G¾Øò›kr`ŒÃ;l qüã7,“a€$  H@€$  H@Ÿ€úøÌ¼bB "N¼óïœ0…ÁËHïCúP³ï¾ûžð—$03»ï¾{ï½ý‡ø‡Î4ßõ®wõÅÐ,H–וæ9nL„A° Arµt\ÈGO?ýôN‹:yÆgôÚ¾/ùËÍO<±¨ÛÎü>¸Ÿg‰„p„ÚQÃ%—\Ò&7nõ²~¼Ã?¼=Ï–º¶Ã;¬8öè£ö¯¶Ãw´¾ð…/ ‹ºç?÷¹Ï ”7êxm ŸC9Ä¥Røä¦­ã ÌêRÞê3ŸùLký΂2KÕê<Ǻ&ŒZè½÷Þ»5ýò¾¼gLZÉáë_ÿzëõˆóðÔR¦¿×ò$«(Ÿ[ H@€$  H@€$°ÐWƒú6zO¬aÐcÀÒ  ¬ XçñÞsMÅ^ˆˆÄÿÊW¾Ò`¹[쮾úêÞqΓf ò—B9Â;çÊã9­Eì#æGO:é¤EÜÒ{ !×NðÁ‡Ä^y:\ÇSÇn¸á†~„;ï¼³ùæ7¿Ùވ̣†Xê`KúQÓžG¼óÏ?¿a2 “Т~ómþà?Øû·ãŽ;ö'±Äù}öÙgY1Í iëx%Émê¢ñ{ßûÞ~Ýæ]§®c½Ã/ùËÞqê}Ôs&ÓüèG?ÊÑ&Þ¿í¶Û¼ªd¯!x©àûÇ?ÞÁròÙïÿûþýn½õÖ&/ÙA|®Ã²üõ×_ïÇc²b;瘔eùêW¿ÚlÙ²¥Ï H@€$  H@€$ Ñ ( Îj]Äܼysƒ€uå•W6¸|'°Îë#<Ò³¼êª«zû±V,ëÈ>ÿüóÉ-›€Že)7ÞxcóÌ3Ïtæ½ëäÖ­[›ë¯¿¾çòWW·]¤<· 00ÿ‘|dhöqU÷£Xïí¿ÿþ½øïÿûW¤]Ðxà^ÛõØc­ˆßu€µ®q…Ë2¸ì¥½âØ(a½ è´WX.Ò†#óM ÆàÖ;‹2£ðY8ÓŠ‹¸K¦žâ]¡BTg²W¸„ŸÅ%µ<ÍóX¼³¥hÈ71/¼Aw¨+lÚ´©÷-¼üòË{õ wù³¯¼òJ¯ï‘­nÛÒæ}'M,”ù¦SÖÜNð®\|ñÅ -`3J˜´Ï5mÏy \pA¯ÏËY„gŸ}¶×Ž<ù䓳Hnæið£î"*wúÅñ\zé¥]Q':GÛéÓÿ.K Äù²Ýá›çnºé¦òÒ¿¯¸âŠ~|ú¹ H@€$  H@€$ É( OÆmä«ýÒ—¾Ô·´Äâ ‘k®¹¦·kˆD Ža-ƒ Íh1`†åJÇ|P8 žzê©9ÊÀ>ƒˆXÝÄ@b¤×z衃Ä7Mƒà‚jÎkN#¬Nk"âÂV8pˆ{ò»üwÏ=÷tºÌÄ's‰<àªsÔ€8tÌ1Ç XÊF:” žp#Óäy—2#âáz8ŽÅ–‰³ Îyp_«A¡•ºM½h‡â=Èt°Äš·èvÞyç^|¶eˆö ô_ÿú×+ÚÚRD‰®€–­ñ"o±=ðÀ›6‹>ÒǵxÄg‚@Ùn±Ön­ÍèÊ׸çhƒßóž÷ôóùÁR’s£„‡~¸gù׿-¬ƒ7KcÌ3`Y~‹ø-Ák÷§íF<…7ëÿFþÏ;ï¼Ï„ç6¬ f2FÛd¾Ñ‘~PkùŠcaÕŽ¶ÖB”µ|?£x—ˆ8?ûÙÏâp £üà+ÜàÇ5¬)óÍ7÷ãçú<Ùú—k¨áYàw¿ûÝŠw˜~ÀC=”“éíOò¾3A(×Gú[ù7ûôa¢ÿe¢ýisñ?iŸkÖuœ °<ç-ÌÛÚOØ—KP^}:ÚÍ2]<0Œ:©`Ń›ãX‚„²3´-Ä;OÝŠ‰¡mq'9ëà_ÐIKõˆC{zdzu:õ’%#È;u.ï ñ)‹A€$  H@€$  H`r 蓳ë¼k싯rkù8×!6åc9 @äóì3ðV XW—¡åµüÆ…d›Ug¹#âqˆ-9-šË0ʽs?ÿùÏË$ú¿±žÊqca{”pÿý÷WE¨H'¶”Q&Ä–8Û<ÈÇb‹`ƒÖ Q¬É)+“âÈ–ªyòIcÄá ñ± µ6&ÒŽ-ï`›…k¶>ŒøµmÍåvžP»¦æ)á!£¼güÅ=/br)À×\t“æ8’Êg6ÊïìV9ÊÀ«ð®°ë®»¶2ÈéÄ>®Ž' L†ˆ¶ï×8aÔ÷dœ47˜µ è0‰8Ée8ì°Ãúç#^m[œyo‚w¾‹àZ)â”Þ&}ß÷Øc‘ò÷ÍÛÚÄŸiú\³ªãÔòßF¾kœk"˜ìËû6l˜HÇc{Æg”UcÕçoÒPÍÏ /¼Ð/oÛ;P½pŒƒ£è1 žL\‹Àó ÆÙÃE®»ˆæ(ñqýn€$  H@€$  H@˜œ€úäì:¯üüç?ßðb ‹Á|,ûô ‹ícËt .cý‚øƒÇ¥€Ž`‚•k˜†@ýµ¯}mE~p¯™-¶±Åe9écéÄ iXäýÒ"/»µüÄ'>ÑÏ#7ÃB†cQgËPZ)EܶíÉ'Ÿ\&ÑÿÍ "f”›ûØÇz÷E@/$& È#¾aQ „£œ/,âsÀò”5`sÊdz€+V²y€ºËÚ)§ë¾Ö ,ôžrÊ)½bÑNäwæˆ#Žè7Ú¯šðR èxʸå–[zKPàN>ÒüÖ·¾ÕO/ïЖEû‡§Ö/Ç‚¡ Ï qoÒÉÖ~¤Ñ6Y'îYniÓç) “'Ú£hû¾óïôË?Š€Îz¹‘gÄ•çž{®Š6>Opà»3Ï€»çø~å‰ Ãt¼¨DFÙÖÄÍaåÂíx®ó¶Æ–ŸEžmâ!KD&ð•oça‡—8"Jò Ì"_íZÒbR nª=öØþ}òûÏwöðÃo£®ž}öÙÙ˜ô}gÒbùÜ¿ýío÷\µçþåÃÊž¶!òÀ±×¬êø‘G9À‘çïßÚÜûb,°”,B_œgÜrÂÁO~ò“þùÒŠ½LËßXBø 1Ž6%Þ¶±ÔmO¯¹wŽtˆƒUgˆ@q-íW[À½yvm›ãe‘¼æv›‰5´O¬é÷ÂÝsn³b?§»ˆýüE@ÉA0­ ýpŠI X®.*÷`;L@'Oˆ0Ï.Åúâ9Ķ홷• áWÈ‘¶Xo¶}'ÚÒY«Ç£ÜñΖåÈlk¡­"0G?°+à%&ò‘· mnõ§yß#ذùÉý›ÓN;­<{À˜EŸkÚ:ž½åð¾gQ.¶yuâå‰9Þo~ó›~Ya] åôƒÊ¾PNcµ÷s?¶VŸ¢LŸn^! è0¥?Í?úþLpƹF~bR.“:L¨Šº[Ú?BLüÈ“Õz'üO€$  H@€$  H`, ècá-r^s÷Š+®¨^Ä`e ~ÍC@ ¿kpq*à¾øÅ/äa)Î!¬0àW`('BFW` eÒ"?³ãèÙb,,ýkyà¹Ä`*yÅeiY@/E=âa¥¼ºÜÑç4Ý—Àz"ƒ÷áêï¼yÝj–‹È–éX®—!Ú/ûìþ½ÚÄ0âaùyš4?ùº(ÄRnX`À”{c ×&¦Eymúr]Pô äVu÷Þ{oÿ=÷‹Í!bÄ9ÄÜZA` tûHŸ5®kµ³#N—€ž]â¶å¥–þ<+ #‚1ÙiÆ ­ÙbbTð þ"¤úå—_ÞÏ+ëuOÖn¿ýö&»už&½µxm<÷qtÄ¿¸–ÉmµÕ#^[ŽO* Ïâ}ŸV@ŸUŸ+øLZÇö³ŸõYë?î·ëE@§L‡rHŸÍµ×^Û·þ¥™™L²ß& “$âý`ÛÖNÇ71âþüç?ïg¯qœmx}éGpG€$  H@€$  H`l èc#~A¾Ÿ7nܸâ"Ö:̃]5ýÿgïÎ~í¨Î„ûÈm®¸ò¹°D$¤H r+JGé(‰"H“€@ ¡?¦fŒ™!àfM03˜Áp`°1‰C›Ñ4fˆÁxÀc›€©OO%o±ö:U{>öž%×Þµ«V­úUíÚåz×0gΜjZ å)mÕ•ÐY–ÖÖ±Ûo¿=_½zσ_´ž~úé­GÓÄ8ÞäA๩Túý ƒJWoy];“_]Å*0ž#­ÜÛµ„L{  Ó•lXrÈ!e‹È'ÒÂ4–#З'蹈ïhHÎ|—x蟶J=óÌ3«ïŸßwß}­üó] š‚Ö—_~y™Ëå‰nÈã{œ_ÓX–`)ÃUÄ2íè,ËvØaù¦Ê÷k×®-¸v´«hT»bŸ3{  ÇoAÂO?ý´v«iÀnOµÆî7€¾bÅŠê˜ð[Z—z„SÓïëp¼¢ gŽ1•§øšl)Îï^è_|ñEuš*ê¥÷1l§]ê7€>Œïû tö+¾gìg¿÷\áÓï9þÒK/UÇ„{˜Ï?ÿ<²1¥÷*ÐÝ>-œëÒD  GVîUã¼.ÝëöXóÚÐÙFÜkS&®Cu‰ x”™izöüÂg´R7) € ( € ( € ( € &Ðþiæ`yeíxà5^Æ@g§y¨œŽ¯Kð™à4"i”?ãaW]=ç‡ü>ø`A«IæGŽñ0ñÖy@—ŽŸ˜¶°d¹“N:© &ðænz/ºè¢Ûȇ‡ÈiJ[ì° ]´§-õx€G˳XŸå›ÁñXŽn• pЛn_“8 dä‚>ÑUküÅǬóbŠEš'2Z¡Sö“üx¸LÀ Ë1ÇS•ex&‚d3f̨–yöÙg‹mÛ¶U‹WúðšAt—œ«…}¡ÀH¿kGuTËžÒò/®Ló®srïMŒ›Ìõ…kEú=¢•{\wXîÅ_l ¥f®o×_}y}ä;ÏP‘w”ƒë)娾}{KYãÍP•yöìÙÃYA|†Èˆ|Èw؉k5e‹kÓ´’ûš~†EÞc ]ýFñ ÂTÖX–VÜiÏ)ä1ìÄñã÷"-ëc=V•‹}J?÷)ØŸvgÌõŸßŽ A$*K¥]Ò·«ÔAÓ°aJ*“!qü—-[VšÇþÓƒLÎ îe:¥Ôšï$]óû;oÞ¼C¼°ŽkÚ³ Ûàw“íÞ}÷ÝÕùÀ½F”%¦Mëù¾óý¢ óèƒ1Ûù¾Åw(lÎ>ûìbÍš5%G::c£S„4Œ{®ðäç^/Ê͵xÁ‚ÅæÍ›Ë¬)+ï©HË0å¸E™ã„;÷®±\‡˜rŽà7žR8ýêÔ+Ñ û×o*TÄöØ/_¾¼åûÅù–^ƒùmáw/MT”<¸î¥i×®]Õg,Ãÿ5L ( € ( € ( € ( €ƒ @̯qí·ß~»å!~<ôJ§´fŒ÷<¬¬Kià#–m7¥µWšhÝÙnùô3‚? .LW/Â¥ËÄkZ}æA(@oݺµeýô #èùÔMÉ7ƒ7}H_·Nݼ´uåH’×-ŸÎË»0®ë–šåéa RÚú,ÍkݨÆ6œ*0Ò EÞœJ>ѺœïIŸb¿`¥ßŸx½téÒr®•1/ÈŽDÐ" F¤ËÅkKù5ìˆ#Žˆ,Z¦Œb½vS* ;¸k·ÍºÏòVÙMצô8D>M­ìݯ4XÛê4mWz è´>Ÿ”mJT<Ëó :RT@É÷?}Oe“N) ^§ë¦¯ë**ð›JJƒ½é:u¯9^uiïû©§ž:â`Ûq–ƒë×/îOÒù15eôž+Ý¿~Ïq*¥=l¤e­{+• #¥ ê–Oçuê½#ò+SîÇÒòzè¡£Z´sÏ=·e{é¶iùŸ¦ü¼bY~{"Ñ£F¬_×Â<ý?ßK“ ( € ( € ( € ( À`Ðók»6cZ؉@`Æ%”vWÚ@§ÕAxhSZ± æá_Ìãáî¢E‹F”iõêÕESPŠué.’ C]ëË´ËòvAlÆÏýøãGl;Ÿ ˆG”)qyh]ו(­XÓe;½&ÿh-––V:´tÌg‘žuëÑÒ¹±8ý~ßgÍš5bûì{T&L{ â¼"qޤçM^®Aî¹òì÷§ÇZ=ó½ÎyÏü3Î8cD+g¶Ï}MÝ:uóò RyùÇâû´bê믿>ªE¤BNç½,ä‰{î¸N2M+‚ÒcDäUw¯ŸVàÿ&P@P@P@P@Á  æ×ÓÚùتÝÐc· ñWèŽåÚM ŒÐ2ž–€`Òá^ IDAT‹/.øºhå!,àxØÍò´äf¼\º0çá^tÜnÛuŸñP ›÷FЂáغý¥KÙüøÔ•Ùy (0¾¬q½"¸×±Aö€ë(Aƒu•É{´Ö¥[|*iŵš Aüq¡eÿûï¿¿W®ÁÃÚ_*] ç÷-ö±—¼ù-èg½^¶1Ñ—¥r]NÓò•û”N÷£å1ìïû åìçž«i{ƒœã »Ã÷Ÿ{?¾÷é0 Å>óè%€Þç&\MP@P@P@P@P@P@P Kè]BÆbÐGCÕ<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@="`}0ÿc#Çoºé¦âšk®©ý{òÉ'÷`iÜ” ( € ( € ( € ( € ( € ( € ( @*0æè÷ÙgŸbúôéi¹}­€ ( € ( € ( € ( € ( € ( € ( ÀPÆ|}¨{kf ( € ( € ( € ( € ( € ( € ( € 4@o€q¶ ( € ( € ( € ( € ( € ( € ( Àä0€>¹Ž·{«€ ( € ( € ( € ( € ( € ( € (Ð `½ÆÙ ( € ( € ( € ( € ( € ( € ( €“KÀúä:Þî­ ( € ( € ( € ( € ( € ( € ( @ƒÀ˜ oÚ´©˜2eJ1mÚ´†]p¶ ( € ( € ( € ( € ( € ( € ( €ƒ ŒùúúõëËúÔ©Sß[sP@P@P@P@P@P@P@P AÀzŒ³P@P@P@P@P@P@P@&—€ôÉu¼Ý[P@ ðõ×_O€½pP@P@P@P@P@{ÐÇÞ1±D ( € ´|õÕWÅm·ÝVzè¡CšL™2¥Øwß}‹_ÿú×Å»ï¾Û²¬oP@P@P@P@P@P èýÛ¹¦ ( €£.ðꫯûï¿4'p^÷wï½÷Žz9ÜÀÄxã7ŠóÎ;¯8çœsjÿÎ?ÿüâ–[n)^xá…bÇŽgÇÇÉžlݺµ˜>}zqðÁ»wï'¥ÛÅ\³fMqå•W¿úÕ¯ÊÊGßúÖ·Šï~÷»ÅoûÛbþüùåüÃ?|¨;Aå¦3fÔ~Çøþ]vÙeÅ 7ÜP,X° ö{öÖ[oçž{nËú¼g4mذ¡˜9sfËr|·/¹ä’âóÏ?Oõµ ( € ( € ( € ( @—л„r1P@ö†À·¿ýí*hNÀ禛n*žxâ‰âì³Ï®æT_µjÕÞ(žÛ‡ë*bÔÍ#ÐxÅWô‚`Ú3O?ýtu|Ö­[·g6:·òÐCœÇuçw>ï£>šÄi§ÖÕ6)å£ÒJšN:餯õÓžG®½öÚÆåyä‘4K_+ € ( € ( € ( € t)`½K(S@P`oD€çC)vîÜÙRºuÏ?þø–Ï|£@“ÀÂ… ‹ƒ:¨øÑ~T?ûì³OñÃþ°ü›6mÚˆ€ãé§ŸÞ”ó‡,ðä“OVÇåoûÛsŸ\Ùy晕%×J‚ÒwÝuW1wîÜâÈ#qž¿÷Þ{Czíµ×Š“O>¹øÅ/~Q•áÇ?þqÙê–ï|¿óïTŸQ¾x Ú>½°|\ãYžõhYžVh!èÏ~ñÙOúÓjùãŽ;®Ø²eK•Ÿ/P@P@P@P@º0€Þ½UßK~ýõ×Åûï¿_¼ôÒKÅ3Ïúè*¸Àbúë_ÿºñê/ùËj=^óÐŒqÓÙäC«–NeiiCk²Øn:Mçoß¾½ÜW§ócyºN[¼L]¹x˜åy˜ýýï¿¶´zãc]"@ÄçQ¦8D«,ö/mqÃçlõêÕuÙ9O˜PÑÅ;"“½ Äok@|èþ:–¹êª«bv5%@ûûßÿ¾àü‹åÒ)¿û+V¬¨–O_ ó÷ w~/–ãâ‹/.ò€â7ÞØr_Å=UzŸÅk*üÑR9Í‹û¡ü'ö‹{¨ºû"Ö'?ÆáNó±÷WT¤’%c\Çö{ì±2ðKð7þ¸_êTù°ß{.î­òcÉþ’vìØQà˜ßË1^{·• bG{JyÒ{YŽcSb¼ðð¦‚İS§:Û£¥z”áÃ?¬ŠÀyóÓÖéìÿ ÕyZ¹Š{b–ÏñU†¾P@P@P@P@èJÀzWLý-ô³Ÿý¬zè¿òéþûï_”æÁW¾lÓû¦À6¥~ðÁ[ò!вï¾û¶Ì‹|i N"ðóòiÙS‘Ã;¬vyZ»“hý^7Žcú:¶S÷pž‡ uËÒjˆ.cÝ|z '¤Åôµ (0á¨\×>Ƶ6)Ы@œ?Mô­[·Vçä<Íœ9³ú<òª›Öœ‡õûN±î>!/Ç©§žÚRüßýîw]•=χ÷TÌ= åÁçºué¶›{­4{ì±=•…î¼ëÒ ÷\Tv¨+ó|ÐΗ¹ÿþûëŠ³×æ1öw”‘ s»DÅ…X–^™†º  _zé¥UhÙ‰ãe»üòËcv‘ž»Í#ñ=fyº~7) € ( € ( € ( € ô/`½»ŽkqÄåC,Zê\{íµe ,ZïЂ(wô¾ûî‘­Oî¾ûîê¡Ã~ßqÇeKmº{LÇM¬Ëã³Ï>«(ó@7älŒ‡ÍguVKþi pº^Vèéúóí·ßQNf0¾"]ÓÓ&ò±~$ö=æ“çìÙ³‹O?ý´ü˜.@¯¹æšªœ,G©y¢EÝXÞrË--yE¾_}õÕ£ÅÑüùóól|¯€ LZ¬¦-A¹FšèU ~G›ètË0¦ržøMçsî3f̘QÐ};•íÖ¬YÓä«[—¼†ñûÎw!*|ðÁÅœ9sÊÖÜÜëÐzÔN[÷rŸDyÓÏ3›û¼çZÙs/÷ì3ÁÍHÜÏDøŒ -ÜwÑÊžò˜Nóä5ó#Ñj=œ»™ÖðÉk÷\Œ½ýÔSOµT~L+_ÒKÒm·ÝVœvÚiå½"÷v U4–-åÑ{ÔN‰J™ÝG#u  Ó3j£¼ëÖ­k)F|vÌ1ÇTóÓs–ž"QY€|¬D"NP@P@P@P@þ  ÷çÖÕZiSº_ç =P"@NYÓs0ü?üðò8Pé‚Ä8íéqáuTLJTN5) € ( € ( € ( € ô/`½»®ÖdŒJ‚Ô×_}A«&º2§•ÌóЋÖRu)  ÓEjžx`]–ÖuåÉôxÀÆ[ß´€Ïâê(§œÓñKãA3yGP?- cŠFâáp”áöÛoÙµÓ´Ë{ºÓ¬Kévö½Ý˜–uë;O˜\OãÚJ«×47öÏ}ØsqÑ¢™ 1÷ Üð[žö8Ïš·”–` ¿ù´Ô>ñÄË{î}¢Ç~³Û¥aü¾ó=˜7o^Á=Ó)§œRŽ+}Á”åŠýd|ñ<Å} ÝbGâ¾&ÖI[ó²Ÿ1?í™'îépjP¦›÷XŸžŠêRZ©°©âaÝzþçJèà/‰`8ïíJ•i=ÊT7¥ÌÜûç)ºeçÜ"E/§Ÿ~zÕ{ç=)Z¦?öØcå{ÿQ@P@P@P@èOÀzn]­uÝu×UAòºe1/íz1Í8  ×¸Y––*äCð¹.ýô§?­ Æö˜ò —uïyèVX'øúK.¹¤Ì>-ùDë©ôpÚÒ'º’dYêÑõiÓ_Z>Æn¯Kévü&P`² ÐûGz½L+8M6 ÷wpô\jzÍï6•íê¿Åi°²)æ·Kƒü¾sCоݶã3†{ÉS]eb*=Fzå•Wªù14ÝÂDzL›îsb~,›¶lü™ö@ö=W@ÿä“OÒ"Žù×Qqë>úh¯–·›:C5ú¹Os†žßœ÷O?ýtÙ>¯=ôЂb¹•+WîÕ}vã ( € ( € ( € ( Àxhÿ4s ì]l ¾Ž§D׉ñ‹)gi}~þùçt·žváÙM½®›rì{®ôž/-ßxxMoq ööu2  s®è½ î±)'ÿÝ»w×Ò¦•6¸÷ý"˜ž÷ˆŸÕUŒ­ÍÜ™ ( € ( € ( € ( €µíŸfÖ®²ggŽÇ:-·ãÝ-òà«.1V8Ëf=¶KË¡{ï½·ìÒ”‡ÛGydùp<}°M0>O÷ßµ/Œq©y(ûÈC:óž‡æib;±Ü

ˆç{Óßâŋ˱ÛÓ<Ò×ý>`Oóðµ (0fÍšU]O¹®6U4ûf™÷ž@üF7Þ®dßûÞ÷ªs’{Œº@ûþð‡j™vyõûûNp4öòÆk7ËŒF={œÖÁM÷81Ñ¢Ee೩Uw@ojù_·“þçÏtZtÇ1?ãŒ3ê¸öؼº:§ÅyúJ‡@J G×ù±/Çw\ùš .$*Pé…϶€itõžæákP@P@P@P@Þ  ÷æÕÕÒË—/¯tÑZ°.ñ9Æ)­:c•ؾòÊ+ëŠPΣÕy<”«{à–Ž£N ¬XvëÖ­å:ÞóÐ<æ3vjš®ºêªê³at¹Þïö´L¾V@Æ›ÀÍ7ß\]K©ø´·[TŽ7?ËÛ,¿ß½Ð þźtÞ”[=–kZ†ùýþ¾sù×õ¤CÞŒ#ËŒF–Ñ?ÑAÓ’%Kªüòûªvyûžk<ÐiáÇ„iÓ¹‘zîÚµ+};´×Mt6@‰´œ|ðAív#HË2LT$*ÆÆ|¦´f7) € ( € ( € ( € &`}0¿ÚµŸ}öÙêAÖ™gž9bÆ(¤;÷xØ5Zôt\rZ7¥èNž L]ŠqÔ£µ:ÝÑ“è•}ˆùLi “¦´ ûî»oAP¿)ÑÛÂiÅNk›ºÔïöº¼œ§€ Œ4ˆÅu–ëjšdÒò°©õbº¬¯Èâ^¤×zÚÛNSÐxΜ9Õ½Ûi—úý}¿à‚ ªm<þøã#6±iÓ¦ª—Ê0t6J·ñayûí·(GÌ ØN…ÂÓO?½¨+/Ë­X±¢Ê‹–Ôuéµ×^+¢¦-Ô‡}Ï•^{êÊ0ÖçaÇ„¡ˆÖ­[W[dν8~ ,¨]f™íèä;sæÌªœôNU—Ò1ÝÙ§·Þz«ZŒó(ö“)­ÔM ( € ( € ( € ( €ƒ ´š9XÞCY{üðÃ2àÎØò©ûA”¬Y´TÐìçž‹û(ì8Òtœ1åa¿ÆrÂ1m¹Ík*pÏÍñ¢+ý´Û{Ž ÷¼ÃJøp>Q¡"Î%î»ù?Aúýâ|K»rçO‰4xâ‰UyQÜûGþLé‰À¤€ ( € ( € ( € (0˜€ôÁü×>õÔS[f¥¶xM+Bl¦óy°)}à—.jii˜Î׿Ûâ=÷[|?ÒÏøn5Ý«Ñòš”.Ïú´'žÎO+Þwß}-Ÿ¥Ëå¯ÉŠ’M‰–òù:uï©X˜&î͹çÊïMë¶óêZó§e ¯©Ä@%Ó(s»)¢òÄ0Ê~î¹ç6n—Þ–Ò”ŸW”“Ê ‘üGÙëZ˜vØaÕç|/M ( € ( € ( € ( €ƒ @̯qmZ–ðð9vÅ”¦´vÙ¸qcÙR+æ3MÇ,$œ~ÆkZH1&9顇ñ9yÓ*;Rø¦µŸçyòžmÑ¢ª]J»qO.FËqònêv|iI œºÀ=e`þgœ1¢Å ëÞtÓMµå®Û—“O>¹Ýnø™ (0®î½÷Þ®¯´N7)нÚÔý†Æ<~ÓgÏžÝMVÅܹskÛøÃ–÷ô®“þöÓz6ºÒÖïûÚµkGôd÷—\rIÙR:íêÏèMgÖ¬Y#Ø÷l§ÝfGkqî¦M›V­—ßwÐÓ-ÙÃ2ŸÒE7AoÝ­¡£u|še¡ÛïhŸç3È=×á‡ÞXö´ ¼¦ÂÀxHT² ¦û`ö™ž†è¡ 7ã=åHï×c»÷ßUF–I»i§ÇˆÈ‹–óybX€øœ.éM ( € ( € ( € ( €ƒ @̯ãÚt…I7—=öXÙâŠÖK{2Ñò†V-$¶ÍC5Z?óÌ3]G‹öNeâá4ÝÏ3.gšx0Nkø|LÞt™ü5ã‘RLÈ÷&P@Ÿt!M—Ó´|åÞ"ížzOî÷*K–,)Ç ýžÜ~º-‚¶o¿ývy¯³xñâòþ«_*(ÒKùÑý~/É{®V­>ú¨¬ Á0˜:ôO«ïP@P@P@P@þ!`Ý3AP@P@P@P@P@P@P@†wë Ñ ùÔ©SÇzQ-Ÿ ( € ( € ( € ( € ( € ( € ( À80€>ŽžEW@P@P@P@P@P@P@ž€ôáYš“ ( € ( € ( € ( € ( € ( € ( À80€>ŽžEW@P@P@P@P@P@P@žÀ˜ oÙ²¥ØgŸ}ŠéÓ§o¯ÍIP@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æèëׯ/èS§Nõˆ) € ( € ( € ( € ( € ( € ( € (0jÐGvbg¼eË–â“O>™Ø;éÞ) € ( € ( € ( € ( € ( € ( À¤0€>Æ÷G}T¼ôÒK-/¿ürñÅ_ìµ’_pÁe¯t­ÿæ›oîµr¸aP`² |ýõדu×ÝoP@P@P@P@P@FUÀú¨ò–ùªU«ª@5Áêôïæ›o,óÖþñ\•åᇠ'WU@èVàÕW_-=ôÐbß}÷-¯Áßþö· ®Ç=ôP·Y´,7{öì2Ÿï|ç;Åõ×_ßò™oP@P@P@P@P@˜¬ÐÇð‘ÿøã‹o}ë[U°:  _sÍ5=•ü˜cŽ)¾ÿýï|ðAOëÕ-ü‹_ü¢*Ó#P‘ê ƒ*.½ôÒbýúõC-¿™) € ( € ( € ( €“YÀú88úŸþyÁÃTþ" ÞK}×®]UàeÞ¼yïñý÷ß_üèG?*Ž:ꨂ ÿxKp@éqüñÇ·¢[^˜„Ï<óLu 'Xr×]w+W®,-ZTxàÕgguV×:\ÿ"øÎÔz×tbA‚ˆéño÷šûŽ+®¸¢øê«¯&ľ‡xúé§«ã³nݺñPä1]Fzéˆûçvç:Ÿ1tÒ°Òi§VÇNÛ¥|TZi—è}ª]>ü><ûì³í²ð3P@P@P@P@.  w 5V‹€½Ði=ÜæÌ™3Vve¯•ƒ–øxPÀ¤€ Œu4ØùöÛo·÷ïÿ{±ÿþûW×øn‚œ¯¼òJµ<•¡¸@oaðo.\X¶ZãÏ9°Ï>û?üáË?zi‰û¸8ýôÓ'¼ËXÙÁ'Ÿ|²ú?möË IDATŽþío+Å—å8óÌ3+KÎå“N:©¬„4wîÜâÈ#qž¿÷Þ{CÛÏ×^{­8ùä“‹´ç&ZŽÓê?ZŽ3„F|ǘ>ðÀµÛÿÝï~W-Çwó„N(†ƒìéoyÜ{ï½µy8SP@P@P@P {èÝ[‰%ãöx  Ó…üSO=U¬]»¶ò$àC@gÉ’%Ý wJ_ýuñþûï/½ôRAËÌ×_½ •~·i˜ôM›6ýë_‹¥K—6lè¶C[nP‹¼ Z/õÚkÛ¶me‹ØÅ‹<0þì³Ïʬ9.yÀ/ßæ0ßãÁ>ÐzoùòåŧŸ~:”ì¿øâ‹²+cºc5)°§l!¸Y— Dæ­·Þª[¤šÇw$zá8û쳋c=¶\×zE4é^Ĺsá…¶ì;CÌ‹û–{þùç[–ÉßÐ…tüžs îæ7=Ï£î}/×`*•ð= ¬ü&½óÎ;óºMün½øâ‹åúézüÞÓë½?t;\B¿÷à  ‡ÃÖ¼ùæ›–ÃHüî?÷ÜsÅÆ‡‘ݨäÁýYœßS§N-ïOò ¥Ë°ìªU«òE~Ï QîóôøãWŸs½ÏÓË/¿\}Î~pæ‰!•b|g?ùä“|ß+ € ( € ( € ( € ô `½¬nåÁ+c*ÆC,ºSLÇå!XCb™O<±›¬ËeâAv7t‚ <´c ÓØc1àLÿ@Ó=|SÚ¹sgK ÇÈ‹‡x|V—\Ò‚-–1ã=SZñ€;ö)>kÚ7‚îG}ôˆåc½_ÿú×Aì-[¶”ŽÙïhíó³Ÿý¬Å!L:9Æçž{nm9ØÆØLº×ù :o‹Ë/¿¼*;å½ýöÛËÖJ£°dŠçk»´lÙ²â'?ùIËz‘Ǿûî[Í'èéˆ#ލæÇ²l».ÐÎCå¼\”ù±Ç‹ìÊ)ÇìÐC‘/ùS,×%Æ–%(å`JþÑ⮲ ,¦Ÿóý]½zu]vÎS`T¢…ySýºë®«ÎÑNÃjÜ}÷Ý岜çÀ  Ê!W™Æõ- ÇNÐýu,sÕUWÅìjÊïÝïÿû‚{‚X.RimÅŠÕòé‹a^ƒ zç×ë´_|qÁ6iºñÆ«ßC–¥¥0ßX×ãi©ó˜rŸ×t¯Ðï=• ©¤Å=YZ)†ß»¸?‰)÷váÜ_E…Á´ì¼æZBu‰ß¿üX²¿$îgqÌóe¼ön+Ôms4æQžô>´]å"Æ£ºàô åë@'ZªG>üðÖM¦Cu´»/»é¦›ª<ì1¢…Ð7 ( € ( € ( € ( @ÏÐ{&k¿-qâXLÓV«÷Ýw߈ÏyÀ×mŠ»MAæÈ‡îcûÝLÉ·éá'­Xšò 0]—hÝ´N7óiaž'ÞÖ%ДwaL‹äNë埿úê«ùæË÷´zÏ,çëòžàsÚʾ6³fökÁ&©„PWæ¦yMAº9ó1ÖMVÇ<¦<Ô”ÖÓeè™ OŒš.¯ÓóÿŽ;î¨]&–é!‡2âçœÏ÷åi‘Æò±n>¥ëT“{Jà¼óΫÎÅ<¸C-*mE«©\ô RK—d½IkòÌë[S v± ä<Íœ9³ú<–«›Öý– ëLкîZž—ãÔSOm)~Ú-v¾l§÷ü–æi{„ø.vÚn|N÷ßu‰^&¨ ËŴΧ®B•btʽTÓo<ËÝÿýuÅÙkóÒÙt—Þ.qŸûÚ©R»|š>ë&€~饗Ve e$*TDÙèþ½]¢‚C\ãY'¿n·®Ÿ) € ( € ( € ( € ´ @oõÊ;]tQõÀ‹îC#ÑR‹î¥cüQplí6ÅÐ4€X·.]uÆ·n¦5›èäO—–”™?ºý<›謃Ã\P-Ë6hQœ¶–$æÓÍ*-Ø"_Zqç)Z.ÓòéÚk¯-[´ñ°Yé8®TRH]Ȇ[äßnʲu]xÒZ3í=€1'é•t<¤ä¡sÚš™×£õð²_ \¨wúÎYZ6ÑjóatÜqÇ¥œÕëÔœã­îñ ¨šÓÊ/wGþ¬K/ <ôÏyR&Î÷Xž z´¼ã³˜Ï”@ãºuëÊlhiÈô´ÂçXžhm‡G¬Om8ß®¾úê‚–û<˜?~žï5®3ñ}âÜœ3gNy}¥ÒItÇÎù_wÝL có›×ûÚÙ…{*5¹^Ç5´)€Î=L,S÷{À8Ì|ε–^nŽeûöíÅš5kŠ4@]·.Òøó[¿Ï|pù!(J`îÐÓß´u/•J(oú9ßzMIÏÙ?~?¸·Šß楿[ƒÞ#гM8w3­ àãÉýQ¬Ïõ‚ãÙPF~OãzÂri1qæS‰ëKˆO+¿ýò—¿,n»í¶â´ÓN+ï%ØN]ÅÇÈooLi) ÷Ó)Qq®©·šNëvú¼Sû•èi„2Ç} ù¦=@Ìš5«Ó¦ªJQä½ét\ÉP@P@P@P@F@A2œÉãÁ]@Os.#G#€Îv.óД1I£,2™—ÿ¥åêô:mÕÓ.€N>é8žŒ‹)íf•1V#ÅZº²Ìª›ºšgœÑØÇ3Î8#_µ îÆ>ÇCJà1/F`)Ï„‡ÿ±³Î:+ÿ¸|ϺteËñ€y4Ò Qž°¦¬ÒÄÃÜØ‡¦V­ñž®ÓëÓ#(Áà4Í›7¯Ê?íV•–_´£’F˜¸õÖ[«å#xÎçé>äݺÇöhõ˜Ä뺊gYû±ÏLÙ¿ºVñQQ òwªÀžà{’žÇé¹Êk‚dí•‚b*BE2€“wçESáQb*oÔ¥¦{®ÓÑÝ7¿½íÒ ×`†”!`^—ÒJ…TÚÊS”‘Àp$º„ýNïÓÒž„Ò1Þ‡q@@Ÿû‘4hJ0;½GáuÓ~ò»eæzÑ@MïÍX.ý½ýgš÷ðÂý@(§òÂhULËÒëkîñÂbo»’Ð1åzÎA{®Ýq_JyÓs}¾ì²Ëªýà<î”.¹ä’jy޳IP@P@P@P ?èý¹u\k,УÒã!bÓÃïX¶›i¿t‚Ü‘Òúø#EËd|Ö%‚§®¿þú‚Vb´Ž¢Õ #¸Dë³v)”uÔQíñYäO+·v‹éÂ5¼i)>ZiP‹>Ó|]‘ý \—¾÷½ï•ŸãÂ1©  ÐÚ`C>î,óÃ(íM!íÝ m -âéV=R$ÀÓ.¥] 7Óà ûœöÛåíg ì z†ˆkP|wÒi§–‰?ùÉOÊï=G¤Ézª19_ÇyD‹f‚ÆôB™ wi`ß¾¦À-r©4FKíO<±üæ·:z+iú- õa\ƒ hSA‹kþ)§œRŽ+MO8”+ö“ñÅó÷´˜Äï|¬“ÛÁ~Æü´BV|?‡q¸›‚àQÎtšîçí·ßž~4âu:L:ÔPº`@LjÿxI1´ÇjoW|KèqîÔM)szOŒ5• cYz\ê”ÒcÖéè”—Ÿ+ € ( € ( € ( €“YÀú(}èÿ€M§ôë‘ÎiŠ@i]«fº~Ôñ0±nú‹_ü"ÍrÄëxPÞK._ÓmÑ…k»¿X6mµ6¢ ̆EЩˆP—ÒnÓë>¿âŠ+ZLbŸ ’°ßT† +Ù7Ö­^D/i@õ"ŸŠÓªŸ<™×]wUyÍ;·Z–ÏÚXŸå¨tQ—ÒàM7­¼êòpž£!þžp ¼é¦›ŠeË–•­TãzƹM¯&u)m}›ö²iï®L“K€s§Ó×׺¡MâüJƒ•íòj';È5˜ó– }»mÇg É‘§ø¥t–‰uÒŠW|‡b>Ã+†}Þ;õ@Oƒ­ôÓîw1öéƒ>˜“”ïÓ`,ÿŒ§7Ø?z¢Ù›©›:ÃÈÔúéU)Ž•7:¥t˜$*“˜P@P@P@P@ú0€ÞŸ[ǵҀÇÞêÂ= 9[ G–Zij  §Á\$ò°›@èùçŸ_œzê©-ݦF}éÒ¥ÕÌxÙÍôÀLwo(¯‡etZð×%‚t±uŸÓå+Á÷48ËçÓº1Vi1Ë1^nùÐK[û¥ÁxºŽõ{™ÖƒÎþ¥Ûglu“cEàÈ#,Ïu¾y0ïHÿø¼®wŒ´ nlé_ ³@`>‚mce¿-ÇžH¯Ÿœq.Pɉ  eÑÔÅ7Ÿ¥ës>±ÎyçW0Ô ÝQ§¿íö¨ßk0ç|ô˜e¡u5÷Ü#üñ幟õ@O+oÑ¥zäôaß#ô@ÏÇmrvš6žÐÛ»±ø•!b¿Ÿ{î¹½ZÄ4€Î¹B¢÷‚´›yþ»wïQNZ‘Ç~Ü{ï½#>Ïg0.},ŸW˜Ê—õ½ ( € ( € ( € ( @³€ôf›>IèM­¶¢UI/­”ãAtÚíu§‚¦ôaŒÉ=Œ.Ü{  ÓUy< $àÞô@0º(î6€-œ;ùñy:®(-« ´û#`LàwØ-¶†i1h=Üh1ÅþÒý:cÀÒ 0!8VqÜÈä˜uëÖUŸ3~9Ýê²<#â<ç¡÷Œ3Êùp@l²œ¦öy`ÜîxðßÉåË—×¶ð"Ã~ƒ7-…òC 8ßßuiþüùÕw‰î·ó½=Ä÷±Ý”–ĦÉ%çCÓèí4b(ò ¸—_çY÷øCu~¶Ë«ßk0¿±ÓŠVéöb™Ñ û!  7ÝC¦û¯£² ûúÀtü]\¼xq•"tšþΦóÇÃkZtÇ1ç~{o¦º:åáþ)ý1lBžÒJ„T 锦OŸ^í÷xêr¿Ó~ù¹ ( € ( € ( € (°§  ’xÚ)Z›ä›Šñ'G;€N ÅxˆÈƒìAÓž ôŒòÓmw]â¡}´¤ì@‡‹Ü»M´ Š2ð°so¥aZ @gLXz;wî¬åÀ,íÖ—Cž¢å,-£ã©G€‡Þ±L~îÒJ<Ž -MýoÝ®ë+ÐN ­E•ºDëÑø.ÔµP¤#½aÔýEpž)Ÿ7]cë¶ë¼‰!çN¯t‚±nS儨T˵ë÷|å•WVù7uÕMÏ Q†Ñ ûaÉ’%Uyë*Å49^uÕUÕzÊd<ÐiáÇœiÓ¹‘ZîÚµ+};´×Mt6@‰´œ|ðAËv ‚Çušå¶nÝÚòyú&½GäÞɤ€ ( € ( € ( € (п€ôþíÚ®™>¬­ë";mQ2Út ç;ì°Úr¯]»¶ uzS7žéJ{:€þì³ÏV寯› ´ZŽ}ì@çs–¥U4ëæéóÏ?/î¿ÿþbΜ9-­é¢ÂëjJÒ‡Ã<,fÜNÖKçó:MÓÏÛµ¢ã˜r1âÜé5€žöˆÒT±ŒënäÏ´]ê÷|ÁTÛ¨ûÍÝ´iSY9$Ê1tök˜÷+V¬¨ö‰–Ôuéµ×^+ï:m¡þÒK/UëqÝà·¯)­_¿¾à7˜îö›Z*ç:ûÍ}Xwz±¡ç›ºÄ¹ÇoÁ‚u‹ 4¯]ŒgΜY•³®rg:¤×ìº{.Ž!• bù†IP@P@P@P öO3ûÏwhkò€‡A´.O‰ÀlÜ£U9ßzë­–eì\zæ-yy¶lÙ²‚îIã/ŒÑb7æÅtÛ¶mD<8ŒugÏž]|øá‡eã—_~yA—äñeNÁZúÆ6˜¦cOLO?{ñÅ« ô† Š´5-‹·lÙRf]¸³=ò PŒÎCÀ±N”)]zÓʆõèö;‚K± òŸþù‚í×%Dz'Ÿ|rÙ=;¢o¾ùæ–ñ(Y& ú¦•"øŒÀ1Ç”²ó·råÊ⢋.ªZ³̰[ ÂVì/çed Nö3} ˾¦ã‡ò vóæÍ-œ¬t›KQ!s—nÙã;À4?¿É,oyEï¤|?›¾ÿ>ø`U¶qíµ×–­ÌØ”;ížòžsÎ9Õ~â»Ç9|÷ÝwWyq.¤ç5¯‡ÝU_(ÐA€Šñ];âˆ#ЏÖÓó×óøŒaÒïq»lùîq^G÷îûï¿y]mpk—ŸŸ/ŽsÜ_ÄùCÏqÝ£Õ3×ÇN)íe„Vèt=Îo7• ¢·—ÈŸ)×äwß}·Êv×à´R¿×_}Y¶Åõ>~‡¢ôäÀýÃöíÛËßn*_Å}c¶óYü–Å: Q²fÍš²ÜéèŒ÷5üGà74¶Í÷š!H¸o£‚fRwî›Ò”V(¤Ò áøý¦¬¼?äCªüÙÇ-¿uØq.¤è87bÊ9÷n±îX›â÷:ì'¯©tÀ="Ç‹ág¢Ç›ðÆPGá€çS:Ž9C0q›~¿8ßҮܹ7ç>-Ÿs,£Œœ§Üsr½'ögÚ´iÕçM•e#?§ ( € ( € ( € ( €  w6ê{‰h]¼:M:ꨖm¥-I:­ËçB›<»ÉƒÀ|šxhÜÍzé2´–¢ÅW:/^ó ˜”Ûĸ‡zhËzäIi)òJ§< O.òJë-âayšGþšÔéNòºï¾ûZÊ—¯“¾§LTvÔ‚\ZÎxMÀ›”¶”ŠÏ˜æ- ÓÏÒ×éØç1¿©ÛKÛ%—\Rq¥•>¨èДþßÿûµûÛN§lëwÞ©²J éru¯;õlPeê †,@°3¿f(̃D»IiKßü\o×w7y»Ìøà8çÇ>ŸV6jÚ«4x¯ïcŽxÏ4®Ãø¤Lƒévâ5×þüûÂoaÓï)-¯I±>SÖ'`Ià2O%”HüG ¥|º¦×TþJz¹ä·-­à–þ&7m3æ×µæOË2^SYˆ BQævS* Då‰a”¨š¶GeÎ4åçëqÝ;ç¬C刨dë:U@P@P@P@è]Àzïf]¯ÁC¸Ë.»lÄÃ3!´""hœ>XKƒ‡l„`vúy§×vŒ®{ˆH †®Ñé24O´Äé´ÝüsZ7x&ßü³ha¼xñâ–ÏxOJ»{åau(ÊÈ”^«ÒÄý_S¥A–§“ ( € ( € ( € ( Àp  DZm.<¸£ ]6ÙÛ‰ò¼þúëåƒíèzto—©ÛíÓµ("ZK\ï¶»â¦üyÈLkLºñæ~/ùáÈxÚ”… äÃÃÍ=•†mÑk¹ñâœ3ÞSIã™gž)»% o§DË7ôG øXKZ>üðÃ1«ã”–ºt%O… Zã~ôÑG#‚æ3qư³'žx¢ ¾<ôÐCÝOçCÆpñ-Úˆ¡A¸ör Þ“¿…))¿;K–,)¿#¤O?ß“¯‡y@WätÎ=G¯Ã,Ð#]²óKEIÞOæÄ}$fÓñ8< ÷]×ƆJŠ´N7) € ( € ( € ( € WÀúp=ÍMP@P@P@P@P@P@P`œ @§Îb+ € ( € ( € ( € ( € ( € ( € WÀúp=ÍMP@P@P@P@P@P@P`œ @§Îb;v”ãx3–÷ ï¿ÿþøÙiKª€ ( € ( € ( € ( € ( € ( À80€>šE_«V­*¦L™2ðßÕW_=¾vÜÒ* € ( € ( € ( € ( € ( € (0ÎÆ|}Ë–-Å>ûìSLŸ>}œÑZ\þ!°yóæâ„NøïÙgŸ•TP@P@P@P@P@P@FQ`ÌÐGqßÍZP@P@P@P@P@P@P@JÀzEá P@P@P@P@P@P@P@É,`}2}÷]P@P@P@P@P@P@P 0€^QøBP@P@P@P@P@P@P`2 @ŸÌGß}W@P@P@P@P@P@P@¨Æ|}Ó¦MÅ”)SŠiÓ¦U…ö… ( € ( € ( € ( € ( € ( € ( €Ãóôõë×—ô©S§{ßÍOP@P@P@P@P@P@P@JÀzEá P@P@P@P@P@P@P@É,`}2}÷]P@P@P@P@P@P@P 0€^QøBP@P@P@P@P@P@P`2 @ŸÌGß}W@P@P@P@P@P@P@¨  W¾P@P@P@P@P@P@P@˜ÌÐ'óÑwßP@P@P@P@P@P@P@*è…/P@P@P@P@P@P@P@&³€ôÉ|ôÝwP@P@P@P@P@P@P@JÀzEá P@P@P@P@P@P@P@É,`}2}÷]P@P@P@P@P@P@P 0€^QøBP@P@P@P@P@P@P`2 @ŸÌGß}W@P@P@P@P@P@P@¨  W¾P@P@P@P@P@P@P@˜ÌÐ'óÑwßP@P@P@P@P@P@P@*è…/P@P@P@P@P@P@P@&³€ôÉ|ôÝwP@P@P@P@P@P@P@JÀzEá P@P@P@P@P@P@P@É,`}2}÷]P@P@P@P@P@P@P 0€^QøBP@P@P@P@P@P@P`2 @ŸÌGß}W@P@P@P@P@P@P@¨  W¾P@P@P@P@P@P@P@˜ÌÐ'óÑwßP@P@P@P@P@P@P@*è…/P@P@P@P@P@P@P@&³€ôÉ|ôÝwP@P@P@P@P@P@P@JÀzEá P@P@P@P@P@P@P@É,`}2}÷]P@P@P@P@P@P@P óô-[¶ûì4¬žF IDAT³O1}úôªÐ¾P@P@P@P@P@P@P@P`Øc>€>ì6?P@P@P@P@P@P@P@ê  ×©8OP@P@P@P@P@P@P`Ò @Ÿt‡ÜV@P@P@P@P@P@P@¨0€^§â<P@P@P@P@P@P@P@I'`}ÒrwXP@P@P@P@P@P@P N`ÌÐ7mÚTL™2¥˜6mZ]ù§€ ( € ( € ( € ( € ( € ( € ( ÀPÆ|}ýúõe}êÔ©CÙa3Q@P@P@P@P@P@P@P NÀzŠóP@1 ðÅ—_›?Û<JbP@P@P@P@P@P@É!`}rg÷R®¶nßZ¬üpeËߪu«ŠÝ»wwµ¾ O`þ ó‹œöƒb¿“ö+.˜wÁð26'P@P@P@P@P@P@Fè4~0‘¶íØfP8;À÷,½§ Ö°Íÿ–¼¹$[zb¾¥Å÷öÛÇÄÎ4ë¤ê8üüŸ‰2YP@P@P@P@P@P`¢ @…#¼ã‹ÅŸ^þSqÃã77üiäß;¹¸÷¹{‹ÿ}ïÇL°nÆd–Hyñ/ËÀ俞û¯Åç»>“åÜ…úÝm¿«¶yóy¢§ùÏϯöÿîgïÞë»{Ô5GUåá\5) € ( € ( € ( € ( € ( € Œ¾€ôQ0ž³xNøÊ‘uï¯~ôê‚ »iô–¿³¼åØ<·ò¹Ñßè8Ù£Ëm±IÏÕÉ@OÖ‡]qØ^?j3îQÿ˜ù{½<@P@P@P@P@P@˜ ÐGá(Óò< >vóú¿þø_Åî¯gzGK–tEž?¯øsËç“ý ]ۯݲ¶ bAê4èÿþ‡¯öy,t™¾n˺âÁeTlX¿uýd?5ÝP@P@P@P@P@P`@æ6Tœ1çŒ*HZµ˜‹÷ùtáË K³éÓMÅÛkß.^z÷¥â/oý¥xõo¯;‡‘¶nßZ¼°ê…bóg›Ûf÷åW_ïox¿xmõkŲÿ[VКûuï_ýuÛõÒéBýÞ(Þ[ÿ^Ua`÷îÝe¾+Þ_QP–n&”eÍæ5]g>ì:ûþ¬üpeñÙÎϺ-zÇåÖlZSë_ìì¸ìh,ðîúw[ÎÓnèa±jݪâÓŸöU,ÎiŽéë¼^p|ãÜúäóOÊyí*™ìúû®âƒ”C#ðýàõW»¿*þþÕß‹¥+—–çX»Bv²¬\³²<ÿy½'߆ŒÀ´—´qÛÆr½ÕW—–½¬Ë²ƒ“^·çò ( € ( € ( € ( € ( € ( À Ðl³þNûA„d|éH Ÿyý™"ýœ`úÅ\‹”Sºu¿éÏ7µ ºŸ{÷¹eP®ež!ú›«~S•müËYÿR© *Þòä-Õxà̧«èªÿÌ3 ˆÖÐónÞ ®¦)r“ß9sÏ)þûÎÿ,£Õpžž~íéË寺÷3̬²Ê»ÛùYùÊã,Ãy^/(Ç Ç$7¹õÉ[G”‹m³RI¾Mß+ € ( € ( € ( € ( € ( Àä0€>ŠÇž Y¢º™6µ¨$€õöÚ·ËÀë—–8Èiž+›ÈtY^Ó’q…ÓT4ŽÏ &¯?¯øsù÷—·þRŽ—æMà/Mi@ï´;N+?Ê[„“‰±Ï#¯tò®ìó`nl/ûÑåÆGÉ Ÿl(€Æ6˜ô¥õlü¼­KÌO×#Èžû±^Þå}ð¼ë‚‚0óT×Ív¾L¯ïóÀ6c §]Þ ÎÏÝÔœ yjÁë¼u9eªË'ŽI]é6bŸ¨8@Wè±=*}¤)ý,–!èÎqåüŒ |·Øf]¢×ŽêÂq‹s"¦Ý Ƴr0Å‘ïjšØ§n*Dä-¯;ÐÙFZ …í§¦ô†–-¯t“WêÈ+~ÞÒÊ?OÃ8&iž|oÓróšãÓaš¯P@P@P@P@P@P@n  w£Ôç2y2¥ïYþȈ­¬Ù¼¦¶êt½xݨ%Ó<€žLGl8›Aëîn»¨&ÀŸ¦4€Î8Í$ZÄF¹™ÆøÞ×u¯ÉûÂy–½äåKߧôۧtüu@§BE¿itzgHSzMb(†4á”úå–¥2AºL]}˜Ç$ÊG/œ\®xøŠbÕºUñ‘SP@P@P@P@P@P`¨ЇÊÙšY¬JƒNñšà2Ý(GWãéÚt;ÞKà¸]2 ß÷Ü}é¦Ú¾¦[ï(o7Óvtºƒ&õ@ïäØT.òyê7€^×½uÓvóùy·á”) ï©€ ]y§åö.ÑÍ|º\@çüI?£ÿ¦”Ÿ?1†xîI๟DËùº±ÎÓò¥¯é½) ;€ÞdÛ´ý|þ ôºkB$Ïèù÷Œ^/òD@>õ¬  ó˜äÛ÷½ ( € ( € ( € ( € ( € (0ÚÐGQ8 H1¦7)þ˜wê: L'pùÆo”ãH¿ùá›-Ý5×Ëb×ú  çÝ‘8£›õů/.ÖnY[|¸éÃ"òF@_´£3-¤›þf?=» lÞú<€ž—7Ìò)ݧe òCÓöc>ãy×µâ%ï½@_·e]Ë~°Ou]ÑÓ">Ýß4€N:ýŒÞ šc¹§ËÒ½7)÷L{hÊ«Ý|ÆáþŸGþ§7>ÝfúºÝvÒz»ïS»r¤-ÐÇ[ý7Wý¦åXåãÉ×·ºzø ã˜D^L©TÄõÓ®ÛS_+ € ( € ( € ( € ( € ( À°  [4É/  ÿî¶ß%Ÿt~™®K°®5è±7[¼Úüú  @KƒŒ-§¼5yN»pï·z:6:å¡ ú¦Dp ù¢×»wï±X¸õÔ¬Ë0Þ9y¤]¯3–vjÁëÕWX7fPÁ€ñÖ™Ö¥½@§up~nåÝdzÌQ×Õ²¿i½Î‚ëyÂ/ß-ÒIuypÌšÒs+Ÿ+¨8ñÚê×Z¡koø—>xiñÙÎÏÊϨœÂxáw.¹³Hƒâqü¶ïÚÞ’G¼9ìŠÃZö9¯ä²ûëÝåöù>aT—ÆsÃ0bʱ{oý{Õn2yÞ+F]}˜Ç„ãÎw>ÊF¹þ¸èU¹|¡€ ( € ( € ( € ( € ( € SÀú05“¼ò®² äd|éÝ—ºjA™ïYzO•;ãXÓº9]&v±0ËÑ:ÖθwFYÊÃßûÞ¯òN_Ìû˼*hEðŠ`x|ü¿þ¯ Kîl1¥ptYN ö´Œÿ}ç—ëçAwZ‹$KÇ@g½èÚ>z³[ž¸¥`û¬Gbo¦´0ŽòÔu×W&`ŸØOæß¹øÎâ„›O¨Ö§ i5o OÀ• ï¶ÛÊ2°aÕ ûe Uoš>ùü“‚r1çŒj–åxÆñ`·|¶%]m¨¯©åcÊ~²T>`_/<ýœ×WZGÊ-ȃ ;^“¼‡„È/­PP—•âüaʘçér˜§‰s9òÆš1ºÓDeˆø<¦ÛwÖÐÓà7ËrŸûù‚1ß/{è²"íþœs-M´¶æøÌí0cÊ”s#=ŸÒõyýù®Ï‹åï,¯Ö9÷îs[òºã™;ªÏÈ‹ïwšv§V8&+׬¬¡ó¢||WÒùšÍkªÏb¦§Ì>¥´H×ÏëèÃ<&þ™×Ÿ©-•0L ( € ( € ( € ( € ( € ( À°  [´(мûë6Åô†?ÝÐq«y0u Ž|éÁµÁ¤È;m)œ·¨eꦬN –›c=*¤º˜Ssï¬{§¶œ¬›ÐYï¶§nk  G^tOÊ[¡ÇçLë{Ì'ø™§º€jšWþ:¶O>¼Î?÷Mi½®Õu¬ŸO Ö6µrÎ÷©×÷±óíuóžÖÇ‘ÚY4åÅij—GÓ1Í{r8åöSFìËÅ\\Ð{ÂËh©A¹8ÿšÒ_1"¯¦}9gî9U6MÞºu Š×%ºÑo:‡êòa^t‡O~ùÐé:TB gˆt^úš`|$ÌÒÏ:½®  ó˜P®™ fÖ–)ïí"öÁ© ( € ( € ( € ( € ( € (0ˆ€ôAôÖ¥µn»ÀÓÞúŸ k~3› V»<š>»ü¡ËËLhMÜ´LÝüºî³iQ}ôµG÷”OäýÈ‹4®W@' ™¶@|hM¢µmÞr:–©›hÖéߨþãÖºuòy´:Î-‰›‚»ùú,G‹âHt?ž/Óîýh¶Büï©,Qδ‚^,®~ôê`h™ö’ü¼·„º`m”µnZW©" ÔÔ;χ@7=LDbÌû|™¦÷T€©K½Vì ÿt扮æGžÞ4æ,žSN_ÿàõ²W„ôX×§Ã>&”^;¢üT¤`Ø“ ( € ( € ( € ( € ( € (0ÐGCuˆy~ùÕ—e«[Zb3~:AóÑêÚ»]±·nßZçé*šÖ·1Nu»uFó3Z¤ÓM<å!ØÍë<ØÞÍöÙ/‚‹ŒÙ½~ëúï^c¯l¦T^`,즖ï½ä»'—eìmÊÎ9Æþô“8'Y÷åw_.ó!(K…^SxR¡‚.ç{»DÅzO €Lèžï •Tø£õr?ceÓ¥:y4fJßTT0Hƒðu•*Fë˜p>mødƒ‡AP@P@P@P@P@Uè£Êkæ ( Àø 2 •K¢²S*Ð[E@ô¥GÇßÎYbP@P@P@P@P@P €ô68~¤€ L6»Ÿ½»%HžvÙžÏyS0Ù¬Ü_P@P@P@P@P@˜xÐ'Þ1uP@¾ë=”×½¿ã™;úÞ†+* € ( € ( € ( € ( € ( €cUÀúX=2–KØ ÷,½§mý—ÿ²øóŠ?WÝ»ï…"ºIP@P@P@P@P@P`Ô  ­+ €ãS`ýÖõÅâ×ó_˜_ܹäÎbÁ‹ Šeÿ·¬xwý»Åî¯wÏ²Ô ( € ( € ( € ( € ( € ( @c>€¾eË–bŸ}ö)¦OŸÞÅ ( € ( € ( € ( € ( € ( € ( €ý Œùz»åZ ( € ( € ( € ( € ( € ( € ( € ô&`½7/—V@P@P@P@P@P@P@˜ Ð'èu·P@P@P@P@P@P@P@z0€Þ›—K+ € ( € ( € ( € ( € ( € ( € LPèôÀº[ ( € ( € ( € ( € ( € ( € ( €½ Œùúu ¯+ö;i¿òïÉWžìmï\ZP@P@P@P@P@P@P@.Æ|}æ‚™U}áË »Ü-S@P@P@P@P@P@P@P 7è½y¹´ ( € ( € ( € ( € ( € ( € ( À0€>A¬»¥€ ( € ( € ( € ( € ( € ( € (Л€ôÞ¼\ZP@P@P@P@P@P@P`‚ @Ÿ ÖÝR@P@P@P@P@P@P@èMÀzo^.­€ ( € ( € ( € ( € ( € ( € (0A  OÐën) € ( € ( € ( € ( € ( € ( € ô&`½7/—V@P@P@P@P@P@P@˜ ÐGáÀ>ùÊ“Åó.(λ缮ÿ.œwa±âýei¾üêËbÎâ9ÅŒ{gt½>Ûš¹`f±vËÚ2Û6×-¼®§õÉãÖ'o-v~±³Ìcå‡+‹Ëº¬ç<]þh¥:¨ š‡žßœ[zžWx~~s]ö÷}¬\3ª /P@P@P@P@P@P@  ÷ÖiñOw|ZìwÒ~}ýýüŸ—Ùÿå­¿ôµ>Û½ø‹Ë<®]xmßy<ñÊeÿ1ó?úÎããmðFzîWĹ¥ç?¾›žŸß\£†õ}ç¢1®åÅËP@P@P@P@P@P@>  ÷ ×´­·û  vÅae¶´Dï7«¹ªÌcÖS³úÎcéÊ¥eÇ\LßylÛ±­lÉÞï~„…žÿtêùMÀ×óó‹±ô}ç¢1®åÅËP@P@P@P@P@P@>  ÷ ×nµ•kVw,óè·øß\õ›‚VߤA-†‘‡žûqné¹_áùùMðÃþ¾•kFyññP@P@P@P@P@P è} µ[…`m¿ã~Ì"=ñÊ}çqæg–y\üÀÅ}ç1ÿùùe?¿ðç}ç±zãêbÃÈCÏýŠ8·ôüGðØóó› ú°¾ï\4ÆÂ5£¼xù ( € ( € ( € ( € ( € (Ч€ô>áÚ­vôµG÷x¾ü¡ËËl?Þöqðì'ÿç.óxnås}•VëI7>~c_yüûþ½ˆ®àµ ƒæ¡ç~Eœ[zîWx~~<ö÷}¬\3Ê ˜ÿ( € ( € ( € ( € ( € ( €}@ï­›U¶íØVôò·}×ö–l¿ÚýUOë³-ƵNãª÷R–e»iú|×ç=ç‘®Ïë^Ë[ #=[Ê Ç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@èFÀz7J.£€ ( € ( € ( € ( € ( € ( € (0á  OøCì* € ( € ( € ( € ( € ( € ( vÆÅ IDAT€ t#`½%—Q@P@P@P@P@P@P@˜ðÐ'ü!vP@P@P@P@P@P@P@º0€Þ’Ë( € ( € ( € ( € ( € ( € ( € Lxèþ»ƒ ( € ( € ( € ( € ( € ( € ( €ÝŒùúmOÝVüë¹ÿZþ-ysI7ûä2 ( € ( € ( € ( € ( € ( € ( € ô,0æè=ï‘+( € ( € ( € ( € ( € ( € ( € (Ї€ô>Ð\EP@P@P@P@P@P@P`â @ŸxÇÔ=R@P@P@P@P@P@P@èCÀzh®¢€ ( € ( € ( € ( € ( € ( € (0ñ  O¼cê) € ( € ( € ( € ( € ( € ( € ô!0æè›6m*¦L™RL›6­ÝsP@P@P@P@P@P@P@ºóôõë×—ô©S§v·G.¥€ ( € ( € ( € ( € ( € ( € ( @Ðû@sP@P@P@P@P@P@P@‰'`}âS÷HP@P@P@P@P@P@P è} ¹Š ( € ( € ( € ( € ( € ( € ( ÀÄ0€>ñŽ©{¤€ ( € ( € ( € ( € ( € ( € (Ї€ô>Ð\EP@P@P@P@P@P@P`â @ŸxÇÔ=R@P@P@P@P@P@P@èCÀzh®¢€ ( € ( € ( € ( € ( € ( € (0ñ  O¼cê) € ( € ( € ( € ( € ( € ( € ô!`½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@>  ÷æ* ( € ( € ( € ( € ( € ( € ( €OÀúÿoïÎó¸î{ûñOþ¡@¨Å Eë¢H€6Eo{ƒ¹ÍmrÓ¸¾7nâ­q½$¾^bÇ[lÇ»¼[Ž÷}·lK¶dK²Vk§ŠEI”H‘¢(J¤¶sñùûè;ç9óðYæ!’ïÐó,3gμæ™àÏœsfß9åˆ@@@@@@@š @oM@@@@@@@fŸúì;§ € € € € € € €@èM ±  € € € € € € €Àì @Ÿ}ç”#B@@@@@@hB€½ 46A@@@@@@˜}è³ïœrD € € € € € € €M 7Æ& € € € € € € €³O€}öSŽ@@@@@@@  ô&ÐØ@@@@@@@`ö  Ï¾sÊ!€ € € € € € €4!ÐñúÐÐP8ÿüóÃE]ÔÄá±  € € € € € € €õ t|€^ßa° € € € € € € €­  ·æÇÖ € € € € € € €³D€}–œH@@@@@@@ 5ôÖüØ@@@@@@@`– Ï’Éa € € € € € € €´&Ðñúàà`8ï¼óÂ…^ØÚ‘²5 € € € € € € €5:>@ïïïÏô .¸ Æað € € € € € € €­  ·æÇÖÓ(púLýCãáä©3ÓØ vÌMã§Ãቹyð5 € € € € €À¬ @Ÿµ§vvØšî‘ðÇÿ±4üÁÅK·._N)M§ €@Kû‡Õ;F*£ÇNµTÏ^§?Ùþð’/²{ð¥ož½Ê‘!€ € € € €Ì9ô9wʧ÷€È©×b«å¾·vgÁtý)ø£ €@ó×=»-wM麺á÷Û›¯-;R ¬{ð÷nûºò{ÑCL@@@@@f‹úl9“3à8.ypS%pYÖ5ÜR‹o}©»R—‚¾îýÇZª˜ÉúýßýzO¸ö™m ýÝñjO=™úŸÿbEîšÒuuù¼-3™…¶GeÞƒ¿óËU•ß‹F¡ € € € € € 0[ÐgË™ìðã86~ª¶(˜»~~k=[Ÿÿl®¾‘±³!`‡3Ð<Ú"  [×U3Ï~º7kÓ½oîªÚž½-§kZ*-ûüÓ‡6W~/uÍÊi9&vŠ € € € € €@;ÐÛ¡JUC£'*a‹B¾ËZìÙzôø©ðÜ¢}aÞ‡{ºž#Uûãæ’À¿Üµ>w}5¤ßùZO…jsïÑ\=èšÿ¢ì{pïÁãá‰}aþÂ}A¯) € € € € €Ìô)8“§NŸ ;ö…/6 ‡ÃS°Çâ]hþq…d«wŒ„ÃGëµ­ŒÛ÷ž=Žå]‡³×:¶‰“§Ã«Bמ£…;.;¼)ÜQ_˜Å†]£•a¬Ø<·ª†ÐÖùíb.ö o¦Dྲྀ|ÿÉÏ—…o_¿*üõu++ŸY ®Ïõ÷G?ý²òÝ-/vWÚ¸ÿÐxåsmcúØø©°bÛá°¦ûHÐÃ+ÝkÖt„»Gƒ®»©.ã'N]ç_m=t,ÓYZ±˜m÷`Yè¼èw¡ck¥pnEm@@@@@¼º×(ùõ¾Cã!Õ3T—ÂVÿÝ^òE¸òñ®¬;ûÍ/«Ï,øR@fEaCŽýëo7Ø×UKV·¿²3h¿VŸ-¤ÝôÂŽš¡’zúázm[[jþ[{ §v8ž¯­£å÷ïX—' O,@™ìáµãonXsQ]rzcéªãN}0™ÅïÛ˜ìM¹¥÷h<úcxàÝYxFê{Í%½µo,µûªÏΜ aÕö‘lþêwW ú(4* ëÂ~›O}Ü—m®ß’}f¿K«÷Ò‡Ï ¿í§SˆtÍ™ýÃ{6äêQ]AâÈ$Ó&¨.m_Úþ[—/ š†A¿ÿvæ^Ü;èµ¼tú{—Ú©6©¼·r üÙU_å¶ÓóV^YÒŸƒ¯÷ÑöØ×UËV-:åüÉ׃U.2]½½ÐõQÑïBÿFéߣÔCíºW,>@@@@@˜óèmú ¼¿r *èõaKÑkоۛ n´®‚+ âíö¤Š‚úTpo¯ I½Òã¢æ©*ÞÞÞÇÁqôÛzEKõr-* ¨Š¶»ï­ÝE›U>¯×BûÐ|оÜübwá¾SmR TOO[ßsXõèa€É‚Iß.^# Z\+@¿ê‰®ÊïY¯­Äzê·mŸÕzhGS+Øzµ–ê èÈ Û}©Kõ¬¯µïÔwº_©üÃ-çzôÛz—>²¥Ò¾Ô}íê§¶V¾÷/Zµè¤{°èË\l™ú÷Ã;èu½º*¬÷¥]÷`¿^#€ € € € € ô6üöŽ7ž[H®àÀB [Úwj®z[Ûç¶Lèêùí{±ÛºzÅòd¯P…èq˜‡Öê9ª9“Ÿýtoð½X­n ëaû¼Þ¥Ÿ9>5K· Ö7Y€®Þ‘)‹Zíò½ÚSæµ¶ÕwO}¼7>„Ü{ iªc^ž¬¹ xƒÀ7qõŠÊoéµ/û³Okè7>î!ùB5 ë·»®çHÕ9øxmõýKëú6úßý÷nûºôžè®¨xø}MöÚBrïcÛØw:`™Ùç¶LèeXtÒ=øŽW{ªŽÛ޲]#lغõ,u¿Ö°ìVÚq¶ºY"€ € € € € àнFI¯Õ33æ/ÜF ½ÇÿýncÕ÷ê:µGÃð[QÛÔ3Ò¯£ó²¬k8³Œ{gþÌõXµ:üR=â}]öúÚg¶ùÕxÀ¤úÍØïGS@¨Ô Ð?Xu.`~iñÙa˵M*@×=@÷M»G³©%l?Zê¾æ‹À‰¯»÷i½‰“§Ãç†r÷.Õc½æ}];Ví¸ ºNu½iTˆµ;$ÃüçíËÚ§ýªŽ…ëULÕF Ÿ8y&hwoèeYtÒ=XÇ­{¥îÁñÐþµtYø¯ä¦¶4R‹þ]9žXÐWµŽFVðÃü—}nö7Æv € € € € €³[€½ ç7  ø¢@ øÅÏÏ…X¶®}ã]ëøÀ7Ð OäÂõ:Wx—m{Çrë)ÔðåG÷V? ðÏ·¯Ë†™_°f°È+\S«B9õTõa“Â=L`굺ùãÞÛµtµËï[¯}ïrk³BÂøÜ)TóEξ.Þ†FOä¾Ó0еŠlü9´z»ö­µß!P% ß’® ]kVjèZgãîÑÿÖRºòÚF§ýNµÔ0é¾¼µ|ñA¬­ÛJ€®aœ}ý¼pAøÓŸì´…î —üðív ¶ŒÃåËæ›OØÖidÙH€‡7 ­‹Ê¯žÛž;Þ_<™Ÿ×8oü¨ªÓ‡rê=9YQ€©ÞÃz@Bè¡ eL §öè6'¸­?sÃï·ÛWÙò¦Î ßÈ=Gø”U^^¼?w ë>š* í}ËÐ˲èÔ{p#ú凼×û¢¢û¦?/o/?˜[µì{p®rÞ € € € € € @€^þo@¨ÿŸÿ TS%ž£·ìýá÷zsíðmšìµ†]=[SsÕ¡9Se:ôGÞß“³Ð°òEå©ûrëjxz_|x÷Ö×zúÌlê Ð}ݼF L2ôË£]ôŒý¾µŒôܽ>÷½_·ÖkÍ^VyrAþV›Råúùù‡eÊÐ˲èÔ{p#z<½E­Câ÷w¾Ö“;}܃s¼A@@@@hƒ=ÐKF‡ýUhÔ?T=´ù=oìÊM“èšÓ×Íë)… ¾¨'³ÿ^ÛßýzOÍ¿ÞÙ¾Ü<ì«©¼Ö0ºÖSÚ׿N 5¯Jâ=žk½²£:_4Ò]¡¾og­Þ®7>ŸïA«0Ç—v„7šoY={GÆNú]ñ–¦#@Gpø›V×¼ç螤\vìkéXýƺ‡Õs½kŠ¿Þdz|/X´>?Gz<„{Ùvn$@׃Þúª'Š{ ÇÓ›è_Úqöõó@@@@@ô6üüÿàWhϬ!Òhû@! ÿÓo¾Î­£ÐÜJ»wp¼êT~¾¡ú¼Äÿn•}®j € € € € €Ìyô6üRsÖ* öÓ½YoK?Ì·0qÀ£f]ùx~ÞXÍÙÛ½ÿXÖ[Y¡¶m«¥êŒƒ†x.Y “¬ùÀ9‘µ‚Ú…ëeso[]qH¤žâþ»Þã9±Ã•ïm½TOjí˾·åÍ/vg=ÞRÿø¾¹ðzÕö‘Ü~Nž:¶)øÖß½oæ{ðÿÛï6V¾Sï{µË—ØBŽÌî)¯Öþ|0cmôlî=š »UÇšî#•ݨ¹àÕ_Û•}‡Æ«L´ßw¾ÊÏù[´=Ÿ# =~*»¦uÇsë¡Ïõ÷ÕÖÃA×”/ºFß]q0÷›Ôv-i}]3vmhùý;ÖÖVô°Š¿´Žzn+Öƒ7*j£êùöõ«*uiä‡2‹î¾z}ÍÓÛ²ýÞòbwÕwú> Ç÷Y=4¤mtÆÇ©©!ô•²,:嬇 >Û0T¹ÏÆsÈkT»?ë|?÷À—Fa‰½ôpÅò®ÃÙýwâäé =µŽyjYö=Ø×Ík@@@@@L€Ý$J\jÎÚïüò\89©÷©]{jÝZŸ)³¢a‘‹Ö{ÀÛzq€®€Ì¾³åeó¶„yì ÔâaÕ#½¨¤j«3^êØ}Iµ#ÞÆ¿×ñùžÜµ,üvþµ†–¶òÞÊ*[÷å%ûÃóŸåƒEûNK…I©‡•¶M«ÃÛ§öÅgsC 5"„ý®â¥ÐñEÃjÇëØ{…è—<¸©ð{߃<îmluhYtß);@O&áÛ‘z Ðã8µ]üÙA¢—a‘º÷Mõ=Xwѹ‹ßÞÇCÞײ°mâ¥F±ÒŽ{°ÕÍ@@@@@/@€î5J|­^™©à#ì}*@WïèFC‹›^È÷äTÀ¸WŸí3^j=õ0ô¥‘cP}‚½¨¼²¤¿0€ómQOyß{QõÅ=AýúE¯ã!‚±¸~þöÜaÜùZOaÛµ®þŠÚ¡Þò©¢cLEHòPô›ÔçñÃ2µúÑC º.‹ê[»óÜH j÷m/ï,\7®C×v|­6zì©õ7ìMŽ*ïßÞ§tÕaß×»Œï­ZtÂ=85ÒÈd#.õZè¾øêý¹ÍÛqÎí€7 € € € € €|#@€Þ柂†K¾ô‘-Yø¤!”ÿî¦5á†ßoÏz%û" «iš<î¹­pAóÏÆÃ˜«÷©†ËÐè‰ð‹'·&ÃZµA!½æï‡gW=×>³­®IC=/ëŽw]õ^A\Q.õ<÷=Ç­õx÷^“½VoÙT‘…zÚ¦‚kÕ©6(4‹Kמ£Ù0ùñ~un4Œ»zJ¦vPè¸}ïX\]å½*0µIç)Ð+<¼hPàWÏ?ÈỺiÔ_æ/Ü—¼ÆtMhøõ'ô%¿×Ðå©ß¬¦”Ðw~¿þµ®»_ï cn¨oßž2^«Ý¾Û› #¯!Ãõ§6=ô^ovõíIèjƒFŠˆïòÓ<êññÉ*~øGu´bÑ)÷àÔ°øÞϿֽP÷ìTѽô½¶ðw¡/ýPøVG»îÁV?K@@@@@L€Ý$¦x¹iw¾gcQ€®fiHx…;_lÎõTXUoó÷ÏêQ/g̓¼³ÿX2ü²ú†GOfs&+¼P;4úç†ÂÛËfš—\óÛ6Z4Ÿ®Â~Í=®:Û¢¥Ú¦c‘…B0Í™®;ž:µ]»>SÜéܻދzÐ1Ýû…×fך8ÑüàÓ]â‡rŠtµSצîº÷ oºéÍXÌÖ{°·Ð¿)ºê¾LA@@@@@`ºЧé 4 OSÙ- 0k Ðg-† € € € € €UèU$íû@Cƒk¨s ÿ ¬÷^ø¥Åùa•Û×jFæ–€¦Ôø«kV&ïÁv\S.¤¦Á˜[J- € € € € €s[€} Ï¿æ!÷óĦ^k>l  €@ùšw=ußõŸ]õDWù;¦F@@@@@˜1èSxª~¯wÒðæ»7®™Â±+@`î|ÿŽu“ÞƒoeçÜáH@@@@@@ J€½Š¤}Ÿ84„ðãõ%ÿôÝ¡#'Ú×jFæ°Àá‰0á¾äýW÷å…ë…ñ§ç°‡Ž € € € € €  …óÏ??\tÑEœ-@@@@@@@Ú&ÐñzÛŽœŠ@@@@@@@p胗 € € € € € € €sW€}îž{Ž@@@@@@@À  ; ^"€ € € € € € €Ì]ô¹{î9r@@@@@@@'Ðñúàà`8ï¼óÂ…^èšÍK@@@@@@@Êèø½¿¿? Ð/¸à‚rœÚ@@@@@@@p胗 € € € € € € €sW€}îž{ŽÈ ‡ºº*'Ë}Ï@@@@@@`¶  Ïö3Ìñ!€u¬}ðÁðÜ·þ[îoÝ£Ö±%« € € € € € €³G€}öœKŽæ¨ÀèÞ½aósÏ…5<ÐÐߦgŸ GŽdjïÿ¯äÂs…é+ïºkŽŠrØ € € € € € 0WÐçê™ç¸@`Ö(èŽ{×û~çûïg[^x¡ªôYóá@@@@@@@ Nô:¡X èT%×^[~× oš?¿rX‡{zrõ Whx € € € € €s¨{ï$ IDATD€} Nô‰±±p¸»;\¿.ì[¾<\÷u8ºÓ{¿þ:Œöõ…3§O×UÏ™S§ÂØa`ãÆlÿ ÊN?žm«v©MáÌ™ºê*c¥ã‡…ÁMƒ†ž®÷&Û¯ŽqhkWܼ9è5¹"°èÊ++Á÷›ÿðß¿8|ô“ŸT>³0]ŸëïÕï|»òÝú'¯0¨|®m,@×½bpÓ¦ìú:yìXeýz_œͶÕ}ðÔÄD½›•¶žî7ÃÛ·‡kÖd÷à *C×7³Ý·tÏ<68X÷æ§ÆÇÑ={²{·ö¯×ºO>q"ì]úeÙµ«îºÊX±÷`ýNär¤··Œ&R € € € € €Ó"@€Þ.ö3gÂÎÞþøç) ²´üôç? {/®jÁéáó«^úË¿¨l»øÚk‚B—·¿÷?*Ÿ©­³wÙÒª:ìžz2W—oƒ¯ïÀÚµÙf Ô?øÑ«ö£àMaO\Ö?þxxý»[µþ–çŸÏ­ªp{áe—%Ûòñÿ¹$ ÷s¸7ë}4úé6<ùD¶†êýì?¯ÊÕ+—îwßq5T¿ÔÜÏ:G[_}%Œöí©^O˜!ŸüôÿV®¿ïœýÝë>â¯uÍqnå«ÛSùníCÚÇ!Зßzkøò†*ëZ}+ïº3èÁ ZEui{ÖÛöºïô|ôQ[ÚQЯ‡ü=ÎöoËe·ÜœûñqŒìêÉ4°õ´ìzùå°ýÍ7ªŽG®µã±þþ°ü¶[« ­nïô5uÒ=X¿¥Ô=vÿН2ºÝŸ|Rå¥9to¦ € € € € € 0ÓÐÛtÆz-* L,8±e€÷¯ZU÷¶VÇðŽUG¢žÝ ÞmÉ–¼ùp-ÞÆBvÛÙé“' ëWp¦¢u¾ºýöÂõü>ÌÅ=áÕcÕ¯c¯¿ü7„m¯¿žüNëhÿêõ™*zÁê±åæçžK­Êgt¼€P' ¦uÝÕÐWÝ{Oå÷¯×VâÝ®Ôò‹_ýÒ6«Z*hNm¦‡rô K;ʪ{~[Wô°ŽÛ=·µÖûì^“èY¯{°?7µêÐwÄwÚ=øÀšÕIm¯½–ÞtSò;£ € € € € € 0ÓÐÛtÆÖ>4Ô°‡|âÛrȼA@@@@@ CÐÛtb4G®‚6}KWæ×îlÅáu<¸æH×pçqH³ëãáè¾}ÙÐÄþ; ——8„Yrýu¹U4/ñê{ï­ì#›=„lNr…ï¾ °mÞvíŸþÇ¥ÙðÌšÃWC9¯¸ãŽJ=j׎·ßÎŽ¹u¾ç¢êÔñ6G¹zŸ*LòǢךßÝÍ×ÛûÙg…Ã1ë8ö|þy6T²ö¡¿Z=ÊÕþxŸz?12âwËkf„€`û=ëþ R+@ל۶þ® *ǘ Ðõàœ9¼sgÐT ¶–šÁ]?ñBZÇæMך‡Üß[Tõš÷uµúZ#y¨íkîÿÝÙ{ÎéÓ•*uuåŽcÑ—W¾³~<>ÝguoÔˆK®½6W‡îqñsÓë8·¿ñFn̓îïó6„»þ=è´{ðážž ¡Úýù·×rÒýV¿%{8IVû–/Ï/o@@@@@˜ èm>K ±4Ÿîþ•+³pA½C*û9‹BÄC«Y«ï»/V¨Ç¢/q˸§‚n 8l©lãÓOgs¯g½Ïœ c9qñát­kui©ð[%òØzrÇÃ(û°.Ûð›ÿh®d_oÌÙºqðm¼}¯¥Ây èýçþµÎƒßŸ^kHx 3RàÌ™l¾i© ëww‡‘]»r‡› Ð-׊>x×5£9Æ}ÑC,þºŠïY¶nχäÖ[öë[ì«Ò—zPH=åÕvõj£âñíTÏðTy÷~?·ž¿éá_G*„×=ů£×‹ÿëê°õ•W²pÙFôÐà ãÃCUMè´{°‚ýøxôïÿÝé ôÛ‹ÿ=ª:8>@@@@@@ CÐÛtbÔãRáRÜÓ2ì½õîö͉ô¸7Ÿæ ¶íµ´Þ‹V‡Â1ÿ}êµÚ§yjÕ+4.j¿ßF¡øŽ·ÞÊ}f¡‘ï©¡š­(˜òuÔûÚÏËluiè©à߯_ôZ»B<õþTH¶éÙgƒ†)¦ 0[& ÐSÇèýä'¹ÕDûkxÝ£æ¾_ÿØc¹ïýºµ^Û:¹ÊZ|£Q@Rä§Úñúwÿ6¹·8@çk÷÷wÍu ëžÚŸÿLûþêöÛ+óŸû::íè:o6º€o7¯@@@@@˜Éèm:{©ž‡>4‰_× Ç뼶ã]‡¦¡Ôã}½_÷ؼœFß_ä¶U ¯Þ“ñö P¤Øç+ﺫR˜ìûz–_?üP¥ÿÂ誛‚i2t-k/µÂ_¿q€®i"ü÷õ¾öݤ¦±OuOjäázôÔ:úÌŽ1 «v½!¾êÑüñ¾tÚ=8Ðõà@@@@@Ù&@€Þ†3ªa~-T±å¦gŸ š“W=<5\¯æF·ï´ŒÃq5+î¯SO€®z4„±† އ|÷û·×¢ÝJ<\»ŸÿÖ‡S{/΋z]ZQ¨duk©¹Ê5WnáßïŸ Ú>îéiõ• ë|dCÙÛX"0K¦#@ÿú‘Gr×»¦ª(¼Ö¿¹l{íµ0ÚW=…D+§!­u¿Ò~7oÎîÁ‡¶lÉFž°{S*×þ}ôÔ:“èß„†Á×<õšÜö™Zê{_:íL€îϯ@@@@@f«zÎlÜ Sáu\4d¸Pâp\ë· +ÖœçfÝBb ]®aÏRkèvß½Þ»li®©¯~çÛ•uü¼íNÞ¶õ¡¶>ÚÚU©C=Xm=-kÍu¬¶©ÇeïgŸ5©â÷ÕJtÙøº°ÅÇžÚ?Ÿ!0S¦#@ß½paîz×u¥vÚº+Z¥©Ïã‘/4ô|\>ýùÏ*mM…ãZ¿Õ½õêltŽ•wß=Ȥ:OML„C]]aË‹/&jŠ-:éL€ÿŠx € € € € 0ÐÛpVý|à õ^ápVΜ }KW /Ü»hQ.4>68]ye%àQ=êÙ®yÈUÔ3Qḧ÷¯\™«£ç£*ß+„ذálì¿gÎTµcïÒ/íÛlùÙ^U©Ãö¥pJÇã(ûNK˜ù ÝÖÙðÔ“ahÛ¶J[f+Lò½3㡌ՃSmóCÅ«>}fêá¯pªž¢žðÖ[*,£ 0“4t¹îze×Çù‘0ësý lÜxîžôÍ+í[²$w],¼ì²p|h([C×|<ºÆâk¯ ã‡WÈtߊÃkMg¡ Ù®MµQõ,ø÷‹+ûZ7/?—z¥Â&_Ämð1]/¿œk§Ö?ÜÓ“Û›ÞûðZëø‡ƒÊûýèþ×±ü¶[+ǨùäÇÈíC¶vÿ±å‰£GsëtÂ=X¿ o<¤¼F±û¯–ƒ›6 [OA@@@@@`&  ·áìm|úéªPDA‹†P÷‹&¶Ô°Ã* ¤ì³x¹âŽ;ÂèÞ½…߯üñÊm{íÕªõxmyá…°óƒ÷ÃÊ»î¬ú~|ølXf•(ìŽÛ :TRõëã÷B÷õyì_±¢RM~½ì–›+ÛÕzQ4œ}j$€Zõð$Pô»Ž¯½_uÏosM_uï=U׺m§ 7ž2¾ÓÒ÷šŽ{¡ûõ| í?_÷ؼ\[Z}ãG—°ýhß¾G¹}î— öUâ Ø¯£©,üÃIþ;½Öƒ`(z­ÞÕ*ê)^´Î?úaÖã¯è{õZ·’ ¸‹¶Óçž|Â6­,Õ«0ÞFáJ*ÈWÀõø\ü_WWÕ×kï5´¼Â]s(Ûw“-ëíE¾åùç«êL…Wñ±ðNˆ‡×žìZQh_â-üö …uïñŸù×Q— O=U¸®ßN¯ußÓ(eµ7ÞO=ïõʦùó ·_ûЃAEõé%+©½h;}î²::ᬑjµÛ·æþßYÓY"€ € € € € 0#ÐÛtÚ4Ü­¢Ø š|d×®°æ*¡„zGîxë­¬5*Yá–ßF¯µŽzŽŸ<~<9w®†ÖððVjõ¢ôu+ÀêùðÛ,·Ô°Ë~hu =qäHe‹¯«j˜øÊš!hXöZ=d5$¼õu›eó·k¿~?©×™á;ïøM _ë–Þtc¥N…‡ׯ+\Ÿ/˜ _?òHå7ºFì3]OêEí‹î-ö½_êºÔ}`ÇÛo'¿×ãþ«óHooH ?nuëa=£ûY;J!ußШëŸx< íý=ZíÑÐö*º?§¶Õ:Æ]ºßرØRÙ³§r8þo뤖 ¨‹îp–eÑh!þxôoÉà¦M•ãç € € € € €3Q€½ÍgMC‚X³:h.p+•¹ÐÛ¼_U¯}í]¶´ ?t({­`]ˆcÍ->Ùœµ ›­ž8ìÒ>TŽO=ïë)ê‘~¸»;&Ys#ëõ‰ÑÑz6-}ÍÇ<12Rz½TˆgtíëË Ñ}Bás½÷ŠV OŸ<™…áÚï5kÎötŸÂ9ºuïÔœó äuŸíÛ“µCÓRèopóæ ûòde6߃';v¾G@@@@@`ªЧZœý!€ € € € € € €t¤zGž… € € € € € € 0ÕèS-Îþ@@@@@@@:R€½#O B@@@@@@˜jô©g € € € € € € €)ÐñúÐÐP8ÿüóÃE]Ô‘€4 @@@@@@@`vt|€>;˜9 @@@@@@@N @ïô3Dû@@@@@@@¦D€}J˜Ù  € € € € € € €@§  wú¢} € € € € € € €S"@€>%Ìì@@@@@@@ Ó:>@ çw^¸ð ;Ý’ö!€ € € € € € €Ì`ŽÐûûû³ý‚ .˜ÁÌ4@@@@@@@ ÓÐ;ý uhûO„±ñSÚ:š… € € € € € €4.@€Þ¸Ù”o±³ÿXX½c$÷×{ðø”·C;Thþç¿Xþàâ%ÙßÖ¾±ii;EòöÏÝgFñLùÊÍÕxlüTXÓ}$w~ô~âäéæ*d+@@@@@H  'Y:çÃܽ¾V[hmËFZyúLGÆN6²Ir]ù¶-_YÒŸ\¯Ó?¹«xªÿ{O½¾óµs=˜7÷Í]èó[Ñèz€é¥r爽sÎ-A@@@@˜è3è<þã¯×æ‚“z›^v€^ï~SëŸ8Vn êl rÍE¾¥÷hXºe8 žHm–ûLs¹oÜ=¾Ü<Ôþ‹MÃY°”[©àMÙº†„×±hnøVzö4wÒOœ<ºö _m=>^;ÔÃ^6gš|®alüTV׆]£A窞¢}žš*@Cü¯ë94Ǽʶ½cáƒUANSUtÖtdeîWäëødÔiÅßþäç˲‡mþúº•¹û…Âs=„£?›ÒAŸÝòbwåpöÏmcºŽyŶÃÙ9>z¼ñão×9©4¼É:.Ý?¶öÕUC+×{YתîŸþAˆéÐû‡&²ë\׃îÁË»W®ùº £•tmé<èa±z‹Þê8žÝ¯´­îÃvmêß}Öì}°Þ6øõd¢û°ŽEÿ¦•Qý}–±Oê@@@@@ zé¿ýóܽ>7ܮª‘±³Ã¦+€ùÞm_ç¾WàUOÐçC2(“ýÏw…©?ºwC.pùäëÁ €ÔþÀÖúþ !4wzja…ó©rððD®Ç¼ î{kw®šsYÃGû0OÇ¥Ð.nÚðÜ¢}ᯮ©-Lú»›Ö„·—L5'«OÇyÿÛù6ÜùjOÅA›vVB˜dE!dçêöWv7Û·-u,7½°cÒ:Šê®÷sÄñïÁÚ ¥æy×(êµµOó¿Ûúz­ÐçonX]ù̾SïäZYÎÉ·.¯vÐöþó[_Ú™5cpäDÐyŠKZWX\ÔÛÖæ¬·6iÛïß±.·ª‚ßKÜTõ[²v<ÿÙþÂ0íÕ/úsmÕ6ß½qMV¿B±KÞ\UïÏ©=7øÄÉÓAÓ(ÜT¨Öî ÏŸ»§>îËÚ® ÌLK]oVtLöÝõó·ÛÇ!ÐeªkßÖµåeó¶=ÈR«´rNjÕ[Ïw:öø>«ß2QyyÉþÜýIÇ¥ëYÄE÷æV®÷V®Õ¸-z?]ºœÐÃþº¶ßƒ-/¾SÒPHÅ#¨<ðÎî0ïÃ=U×–~§µhпkºŸÄ÷kƒoßâC¡õ¿¸zEîw¬íÕ&]«q¹ùÅîÜ=Ruk}Msà‹Îíßß¼6Ù–ïürUvíûõíu™¿O«“% € € € € €@yô@/Ï2«iíÎ#¹ÿIoÿS¿oàxöýÂu‡’ß[ÝZ͉ÓZë*¼³}׳TÝEE!|Q ©RE¡dÑ6õ|áñ°Åµêˆç5W˜_¶¤êѺ©àYÇ©^©à<®G¡´z¥·£lß;V·­GÕ…Oq[õ¾–‚øTyeý¿1=Ä¡òâçû“ûW,d÷ûòÁpÜn ÙÂÅߥÞ+,K]g ºâõõ IJ®áš.©°UmW°?¢­Å?X¢‡TjèW=ÑU9f½¶豋ÿ¯¿={Nm[¿lõœøºšy­‘|[íõ#ïï ÿö»Éï´Î?ý&?kõzoåZ-:îé Я|üÜoÆ¿#¹¿ÏøuQOã¢ÐUÛè ãúy¯^¯¾ÄÃЫ.y(œˆÃJßÓVu¨Ç}#ûÖº’8.š‡>4«‡tÜÕ¡½žóïg²÷ñ0ôÚ—Ô_<¹µª·¶¾‹Ûà{hêûÉþtñˆúMès¿­Bª§?ÙîycWU/U ÐÕËßoã_ë!‰¸\û̶ÂõÕõ(öuØë¸·©}®ßkfù0ÙÖ«g©ó*ñ(V—F7hWñÇûÚ—ýÙnjèþþ `ÔJ#ºŽKÃõÇ¥Œs×Ùè{M)`î,ý½£Œë½Õk5uÜÓ ËÆ[ê^¨‡R#èºõ¥‘ lO}¼×W‘½ÖC-ö½–jƒ~Ë CËøþlºòÛùשaããcµõ5z†Jêß#í»h»¸çz¿Ï*>@@@@@(M€½4Êsí;4^ÕKÎt­Õ{ðx6D´ýOy-ãó\mç^5 +öõOöZCïÖ*â[!Hܳ¼(@W] K} ¡×ê5¬^Ѿ=ÿ|ûº ¡²ÿ쪯*Ÿ«·°/Ú¿‚6ï|u07Dº‚B¿ÕíÐÐé€Û·Á¿V žê÷TVðjá‹BYoÜcú§mö‡QÊk¯ö-{D c|Ñçþx4wµ/šú—Ñ9Ðú  4§°†Õ=÷æ{”®Šæa–¯ÙõzÞ{*õüæå³C¸«ú Ä–Ö[_óÚ«ýúÛÙ,hžcµÇšÐ1뚉Ïëü…ûÂè±³ÃÖkhfÍÑ‹õÐ6kÍÓ^Ô³UŸë·/}@½¯ýñZ]ZêX}{íõ{+üj¥¾öÈM¥V€®ãµv½´ølum“ ÐußÑï\ÄçMÞ¾”uN|;֔¯,I„¡ß†×׈v½èÜj>o+ñ±6s½·z­Z[ürºôEëe-]ýÔÖ !Ñýï_ׯýž´ü‡[ªG\ÐCUñõ*s…âºwþË]ësu¤¦IˆƒpM—â‹îmzÈÚ¢TôïA<¢„îóú·XE÷QMÛ ) tÏÑ(Ú¿Õ£å“ ú²wäà?×ýEÚNexôdÕ=Këëß0_Zý}úºx € € € € P®z¹ž•Ú4W¬ÿŸì>@×JšçÛ_v€®}(8V ªpÜïKs2ësûSèÐHñu)|ªU|`baGÜ#\!—Š7Q/éTQ ¥ùt5¾‚?õ¶U ×ñÂrWçà‰}9‹ßí­8È#£­ qA…/©¹s·Eë«—v»ŠB{…2 ‰d¨yú0UçJ=ÄE!øó¨ Ë—¸Ž8„Wî·×k™h;µCsË|dìd6'² AûQL¿½†ÙWQö¹–P‰+…ì*o-?PYWÛ¨|ª¨wªÕ©eÑÃ"©à;5|}jhi¿_ýÎüþôZ¿ Øüºe½ÖoAæþw_+@×~55Cמü¼ó©ÝymÛ¯ûœ/eŸ_w3¯õ»‹Ï…z ë!'_dåG&(ûzoåZõíÔëé Эú÷B#è! =Œ¢s®Ù;Ý¿ýCRZ_0X=‘«#Â+èöûÑk=è kWÓ~è¾$k]ƒºÇÅ?„¦{J|½ê•xØyÝÏTâ¡ìý1d+|ó…ñ¾­ºÅ¥Ùßg\ï@@@@@Ê @/׳R['èÖ˜x¸Ù8<²õê]úP ‘]„JÜ;^ÁŸŠïºêU,Ó¸7±o‹m= ³ŠÝâ!•-¼w«$_*¨÷õ7òzà›p8Yq“*äñas­öÄ=®µË8@—½/ñÖ–Ûõ>‡pOµAá–†u·ðÉêˆçÖC sã:ô£äçÀ1539IDAT8WïP•›^h|šÕoÁ™µÅ–q€÷®¶õêY*ô×ú«Ç®zØNu™,@Oµ'Ðÿúº•¹ÕœústÃïó¿›²ÏInçM¼‰J{¡ Vue^ï­^«q;§+@×Ã:µæ÷¿‹¢‡â]½µ}ñ÷wMK—Ô=ÂïW¯U‡æ²×èq‰ÿ]Ö})~¨Ê‚{ß#^S$Xit kŸF0ˆK³¿Ï¸Þ#€ € € € €”+@€^®g¥¶øÔ«—›/¾·µþ{;z Ûþ:%@—‰Ju€~våZºæÐ¶ ¢žeÙz÷=[5¢}ÞÈÒ‡aþ¼Äút„Þ¾=­¾.#@×}Ê—xä8@/ûœø}7ó:(ŠÖSʺÞ˸VãöNG€®ÑL Žë ÐSëøûI*@—…†R¯÷z×´ ¾h¿­yM!â?Ók¯@É_>ä·«õ>~HIíjö÷é‰× € € € € €å  —ošÕè ž|Ñßþ¶Oe€^.ûöÕzíÛÝHôft Õî÷©×w¼Ú“ í­¹¹57³zûúuŠŽ1îÇEÇ­¡Ñ}ý6‡²æQ.ú{àÝÁ†/ª·ÑÏã!âÕ&…;êQ¾ëÀñ°cßX¸ÿíݹ¶¦Ž1î¯SO€®¶+×üàš?Øû¤^¿ìæÚÖ¶êmë©—¶z~ê½ê4$üßÝt®îßwvXwmÿ«ç¶W¶×vÆ¿è\Øç¼¿'3J¹— +„Smÿà@j¿íúl:ô²ÏI«6Í”e\ïe]«±ÁtèqÏs]£º–Ôh²jûHð½ËSḎc²uê ÐU†‘×7Žßî!EKÿðZ<\»F ±íü}GÃÁÛçZ>õñÙaݵoûþ;͹n÷—Ôò·oìʶ{Û«®fŸÚ–‚ € € € € Ð>ô6ÙÆ½i5ï·þÀëu;tß³[ûJ…º ø5§­B·ÉŠoûTèq¯VÙÆåÎW{r¦EzŒÝþÊθªltõTôC¯÷UŸ³ø¡_‘ÂlõªÖ²Ì2/Î\½'㲬+?qŽký2t  @Ìz§kîa ®@=ž'X¿™ë£aâý¨*Ü~W œ,¤R¨¥‡ì;=”`%f[ëÖúý*ìÓpò6¼Õc˲t…bšgÝÚ¬«w¾Öc»™²åtèeŸ“V±š (˸Þ˺VcƒéÐã^×z0$.þA—vèz`J£Yèa»ÿj: =¬ k[C·ÛugËWäšêï'zèÆÖóÿ6Ä­é>w¼ñè1ºÖ‹ŠÚ¦KtOÔë¸4ûûŒëá= € € € € €@¹èåzVjÓÿ0·ÿ1¯¥¯¶Îzë}ÿŽê!cBÆÿ]!Žþ§þû+²¿¸·}®¥†E··ÆÄÁ ÂI] õ5g±†´¶¶¦q së÷eëj©6Ùw ൮…>xùá=²ð2Â]Á‡Úîƒ~m§9wUü\´Ú§Þ[¯^…¶ ƒ}ïA­SXh®mß~…)šc[Ÿßûæ®J/h­£6Ÿ8÷@…º¶½Ü´{øAÉÂu‡‚ŽÓÖ‰ç6›f—ñ|àj¿„×î<þþæ³=¹­ êÅ퇑WÏèx®jõ¢T¯N•‘±“ټݶ½–ú ù:ô€‚ÿ^=4ãß_4]÷ì¶Üaß÷V¾§¼Õ·¦{$ÄDØw:OVt}øß—ÖQ(¯aßí¼éX_ü|ð#>ÄÃ:ëüéZø×ßž;oªKa½ý¶u¬s.û¸÷ªµ½hš޶¥ŽW¿Aý.Õãßö­¥BM}®?Ý‹ì²Æè÷РßÒá³íÖú²ôuê^68rªe“J…M¾Ð±(äG×°S-å {Hª´z½—q­ª]r×t Ön=`âÏ~óö~§íøÅ×™ˆéØø©ìZñëèõæÞsÿè8ôÞ‡×ZÇÓ:_¾Pס‡ìØU—þ-ñEç2þ·@ÿ.ù¢ã¬[j¿úÍø‡ì;-ýƒ9>h·un}igн×îzH÷7?ʆˆ®Œß§?&^#€ € € € €”+@€^®g¥6 ¥mÿs½Þ¥æ*·¢ ºÞíl=Í·›*qoc[?µT(áK£søªNõ Ô°ê©ú¸ÇºÖÓì>@·må? ïx¨.¸Ø6¶Ô°ÃqQhß׳Ôþ­Ô:§>òõ– ÇؾdQÔ­£ Ç‚B­kÛù¥‚'@~eÿ½Bh+šÜ§× u®4ü»BêøÜ(ÌôeñÆ¡ª:,ÈJÕ¯}XÈoõÄ=ž}›Š<|€®°ÑoSëµ\ÌÐöŸZê:NÕ“ µ}+ŸÛT{4J€/~®ùx}]7—@/ã÷·‰÷ € € € € €å  —gYUSÜsÚþGzÑÒ‡­êQX´^ÑçúŸò©¢ÞnzÅòºê‹ç©®ªµCHQ/\m1®Ï4‡y*@?Û£²º§qѾýç:æTI…,~;{ŒªKç¥( ±íl©õ>Û0”jBÓŸé\úa‡m_õ,$©7k­uãžåñºÖð¢€;^ßÞ+€SÛ}ÑœÀö½-5\¿Š‚ê8È×C©rÛËÕ˜Õ/õ›Ð0ÐVÔ9^§Öû8Œ³zü2bÛ곞Ü~Ý2_Ë×öUÏÒßoÔ ]´BL?RE¼žzßúÒÊ9ñõ4û:…!n¯õSéû¦öÝÊõÞêµjÇÞÈCv\e÷B×ù·ºYêßMaP´¦vÐ_Ñ÷ Ç­¤î¢íôù­/uÛ¦•eêßV ·¯¢QOâú~öȖʶöB½î5}F¼nÑ{ -ïoeý>­=,@@@@@Ê @/×3W›ÂTˆ®°8$jxd+Šy²Þ}þÖ+TXTÔCôŠÇº’ÿÃ_= ¾kÈõ¸¨Î¢ž¼~ÿöZ!œ†ÄÖ°ÎñPºZGÉoß›ï¯ú5Ìî#ïïɵOõ8Öð¾~nÛŸ–šƒV=öU·}®:5yª(ÈÐðÊEA¸ žÚVŸ žÈ¼Š¶×¾Êõ”;ÿ¹µG!®ŸcÛŽYK…ë6„·}®vúÞø©ÞáZÇÂ.­kÛÚRßßüâ¹0*~Ûº~©íâáßí8´Œ{×*|¶¢¥¾. ]]T4êAjhfÛ^×Òݯ÷䆡W] 9$Ûzµ–Þ°¨ö¹ŒíºÑu !î§¢üê¹â Ò›î7qøØÛÖ×°ÖÿÉ}I'¹ûpÐŽ³ÙsbÛ·²Ô0õúíÙ1-ußÔðèµJ+×{«×ªÚUä^tL?}¨zäZÇWïw2jQä|Ë‹ÝÙô$þ­kÎþMÒý9µ­ÖÑ0îš¶Á®\ú7@ÿ^X©Õ“Ýo§óª‘0RE¿e?´ºÚ¥û™•øÞoëi©^åµpеc½ðýveþ>}½¼F@@@@(G€½Çšµ(LV°jûHn.ÕšµéK]êe¬ÐH¡´æ,®gXê65§ájÕvõêVÏt+ñ<ÎV8px" ptnT÷‰“ù^Ò“Õ§¹n5T³‚ãýÇ’aâdõ4ó½Ú®}jßêîç(o¦¾F·Ñ¾uH;~뽆ðÖ" ÍüÜÁ©ú'NžŸo ¯ÌÍ¥mënÜ=šÍc­0¶žß©qõ"U}j‹Â9ýƧ£¨-}mzˆb:ާÙ}vÒ9iöl»f¯÷é¾V­ý­.uÔu­kK×­Fs¨çºlu¿¶½î÷®K· g©§½^ë¨PúËÍÃÙ=x²6)0W=Z?¾oê÷ª:uŒõÞ;Ô#]÷*µC÷>½¶‡¿¬í,@@@@@fŽúÌ9W´@@@@@@@ èmÄ¥j@@@@@@@™#@€>sÎ-¡Æ\så¶úÇpà3ô@³@@@@@@fŒúŒ9U4t¦ h®ö?¸xI˼³{¦Ðn@@@@@@f„úŒ8M4r&  Ïä³GÛ@@@@@@æ’@ÇèCCCáüóÏ]tÑ\:/ë,8täD¸â±®–ÿ¾Ø4<‹T8@@@@@@:O ãôÎ#£E € € € € € € €³Q€}6žUŽ @@@@@@@ aô†ÉØ@@@@@@@`6  ÏƳÊ1!€ € € € € € €4,@€Þ0 € € € € € € €ÌFŽÐÃyç.¼ðÂÙèÏ1!€ € € € € € €tˆ@ÇèýýýY€~ÁtÍ@@@@@@@˜ÿìäêª4‘<ºIEND®B`‚param-2.1.1/doc/_static/wordmark.png000066400000000000000000000141771463636336300173710ustar00rootroot00000000000000‰PNG  IHDRăuk pHYsÕ篶tEXtSoftwarewww.inkscape.org›î< IDATxœíÝyx\õuÿñ÷¹3’lɲfd 8Å!à`²˜ HÁ,ÞˆCÛwIšôù%!mRR(`É¡i¦IÁ¶ OÚIÍÒ,…¦Y ¶dNHò# ŒM{llƒÁ²/Úï=ýC2„­{G3Ò|^Ïãç±Fºç{F#Íѽ÷û=_cS–­9¢,ò£ÌRÇD‘M7óc Žˆ`*øT° Pc0˜ Tj@æ P퀑A«c»qZ |7Øvsÿ]èþ,QðlÇõó¶€ùHy‰ˆˆ$ͦ_Õ\ÕæD‹ü$3? l¶ÁIÇåÈ©xØá¡Àøÿ!ý÷v¬x_[ò‘bÙú¦ˆ3ºbÖDÆwö¬Xðt¡‘‰Ç²õMãëò¤s¿ÿÞÚ}“›õ:™Æ_A|ÅN°/§¼û+-ï-t2""2¾…N †£À—‡VñD¦¾é#àÅ|ÙWDDŠÜx>C|Ãï <õ—-óž/t.""2þŒç3ÄWqì¼È¢‡kÖÍ+t.""2þL˜‚àp¤{´&»´ùC…ÎEDDÆ— U¥0ÿzíÒæ:?&bAH»ù·2 kÞ]èDDDd|˜¨`’yðÝ#rë§:)~¹ ¼­¿«çs…NBDDŠßD/ˆŸÉ\y×±…NBDDŠ[)ÄI–N]Sè$DD¤¸%²0ßàÁn²ÈZ- 54zÃvRå^ötö¦**Ë¢¾²¾ ˜ba8KÍ ‚è-îÁ[ÁON$¿ Æ÷¥¼çhµx‘ᤓâØ³í+çó0ùÙÁT_¹º.U–~¯¹}x?ÉŸ¹N‰(_ü[ÂqEDd‚H¤ Ƶ÷†Å-À€d®^w²ÑÍÀÙIŽá‚ ¢ˆˆ £( âÁÚWÍÛH.wNmןwøI]J5/¹õirsûã„©[ú£êÐÒ3!=÷cÍ8Â#1›æxÔìëüÛÝ[Øî¼Ñ“éþ౯Ÿ¿?‰§5&r·—×ufêÂ(¬óÀ¦×¸SaT:Ñ^sZÍhõT°³µ¬ü…¸ßç]º¡¬fêîøL?ÖÜŽ68ÂÍÀƒrÜ«ÝH›[/Æ~pv9¼¸ï Hm "O}o«—Ý3­ŒÞéÞLs| Ü2nž"òÀÍÊÌè!b¿c{=`›‡¶mOUÙïòõZÌÌ­ŸÔ¾¿ïD,<˜aMø™ÈÃð.·àùÀƒ-“ûÚ¶lÿÒ’®|ä!’”„š{Ûm+ç/‰çÕj뛿èø?$/ìŒÝËçÿÏ¡|mÝg›Þ†v20÷“Í8Þa&0-T"ƒg"øy`ÞuOº»ýƹí ÄeÆ·OîœT}r§s ?x+0ƒÃ»ŒÝóÆVÇžÀýQÇ7‡)ß´où¢]‡äèÜêÊžžòwº3ÇÝOÆì$ð™83€Ôá=»!íÆyÀ[gÆÚÝËç?ž@ÌQË4¬‰Û3Nt· ?88(eØNà!œ_FÎí«<4ÚüŽÎ­®ìêIŸKݳwe‡x¸áþKÌ~–ž\ñã]¹¹ûF›‹H>uAä’ÛSµÇUÿʱS“gfŸl]1ÿ¶¡Æ©ykÍÉ)ü\‡sÍ9ÍÞ„ÆJ/ðÃÈý¶ŽÆ÷‚É$™Ë×g¬¬÷l7?ã ÞM¾¯[q~iÆ·>³ç‡Ü±$<ð©i׬>&ê+?×ñsÍ8 ˜E2…ïPmÄý«VéÿÑš[´'o£\r{*sÜÔwf§s|à®9@&oc¾âqÇn &‡ß8¤ç˜ËÙžÓÙ‡€?*Êc?ðýÈ즎óL(¦H,Å]Ú†uóÜ£æD‚¹­jkœ¿ôõc¬ýº»}4‘1âûw–µ7.XŸxäÜútMg÷9f\dØ9Àƶàl_Û³{2 bæê5çXü´@¹¼V«a˧N.¿eKnnwÜ`3®¸}rgyõbüAäv¦Á@uyÆÑŽÛõéÊò/y¦vÙšŠìdû˜™]îpBžsYë°¬}å‚Gò<ŽÈˆŠ~bëŠyë€ç’ˆeÆñC=EöLñrº÷f뛿U›[35n°éW5WÕ64} ÛÐôÍlWÏ‹Ù=†]¼‡ÂC0>øì0U>]°\^¯ÖñU]=›§-k>=n°½eÕg:ÖänŸ7¸ÂC€ æÿÜßÙóX¶¾yñ+»eë×~8[<ÙWÆ ,4x ¶¾ù‹äní¥a‘ØŠ¾ 8vw2qxÓPôÞÑÐüÃÞlœ¶ôîÙq¢ô¦øWwþ ç/Ú„’‹ÍÝ7üñîkß÷¦ÐYÝŸPÀm§öÏŽm6£R*·K D©0³+ ƒ”†¢/ˆ5K›ÎO¦Ã.þŽzÊ“ZÚ‘oé°¯ì’B'CkǪ…#½ññ‡‰ùŸ:…rvݲ»f: ™øŠn·‹Ws ‚uŸج >‹¢a;Ò´ß8·=Sßô¢ÁôBì2|k„m `›;ͽ ö¹G¡T»{Æñ£Ì‚÷àþn vÇ™W=ãÀIÆ+ŽÜ«Õ vñðŸ·.ð-8[ÁŸs³fôàÞnôA´—ˆ©V V˜¿ËºòÌLø‰¼«nÙ]³Z–_ôd¢qß^ bp—•ÙÇÀ’¤&¿½N¥>\—¯ø"Pä1³´ùïp›eº=<ÒçÍìqܧ3Й#د1߈›­¢ó¨>çÖ§3ÝÝ‹qûT‚‹žO;:·ºòùÜâ΄â½Û¾çQ7 Ü^pëߙިUUUÙÞÖ½2ýe™ œ*Àf>Û±w㼃ZÄ>b#g÷àq¬—/9üx„È6yÔ¿¹ý†‹¶ŽæÔ.[sQp©ÃÇIè;ŠRçcRÀí+A`/àÑŽÞÐvíÛ3­mð23än/¯é®a^dø‡€Ø-ç†g¿r¢»ˆüçië{¨¥ñâ½0Ð&°;͉)çÜý£]p?Dɳ¢-ˆÙ†¦¿Æ¹>ÁmU¿Ú8Ò˜û·ÿnî¿s÷µ‹‡¿ßx8rsûÛ7?®mhú¸»Ý>9fÔòÎîôi ‹1ýYÛÊ…¿ê3ƒ{9umƒ= üøÀç§_Õ\Õ“â”N‹ˆN,5bÓò >à\˜­Þ]qÿC-ÏÖå‹.Ÿ¶¬ù_#÷ÿÄyWì æg_çÐÛÔÞ8ÿÃ~:·¤·càþëÍà·d—6ý \ŸÀÏÚñ»ÜýVÒ·v¬¸`ÈKÞƒ{|>¡z›ŽµöUó6fë›þ ˆ±§OÎv5£-¡íÉòÁ£p¹Á_w«¯€÷ÿvØÇݼ¨‡ú¦UÀë7æ>܆پM$) D_”­_;Ä NK3°÷[PÙ‹C4X÷ <¡Ù£oÈìÛc7åí»~þKÙúæ5à£^>ápT’9•&û:x¬­¢ˆ bǪEÏd뛚€‹âÄñKô£ÒÚ}3[Ü@Ì} Ý#!J^%uVVvÜëÿñ{@(dSêN‹ÂåÑObiyˆ‚òû€Þ81"³¢’ØOô-£>r`"Ì!ÝC‘é’©äWÑÞÿHЭ‹¶:‰×Š|ø&‡(ѵ¥èÅëçïÇx!^”¨ëÿ‰Þ›@˜š8íêÌ,«"’g½ >‘ž\Q„g‡`{ÇòbžÐ1^˜ëu0fK±"ÒöìþÇH¢Gl£.ˆamŠ=~þ‹@.»HP·ÃŸîÊͶ¡wµ¹5S­38Ñ?· fS"÷jóW.;ìÇØÀ ñ‚§|[{ë;¸íÔ¾´{whñfÃÏè8©bûˆ[ZMp¹õ麞Îãú£ÔlÜ2f>c Ù—¿& ¼Ã£à¥ ðq¶ÞóÜÅäîÞMŒ×ÁŠøO$ÏîXR¿vÛàmŒQKy8ú5î[â|Ÿ© J^MÔ‚¢}åÂG’V}åêºTºì,s; üÌö.Žuã•iÜ{ô;à¿€¹‘Ͷ„Ô7=OsNQ_ª¦tÎ/ÝPV;mשîœó°ÙtõÌI•|ß¿™Î«WÝ9€aæ/w ­‚Úú¦—b ÇÆÅk`°Åc>× Hz.@GÕ¾mÙ®©!ñf»jJÉ«‰X#w>ÞÞ¸ð?â©i¸3k^öa3¿ç, xÕ›î襀Ù8ZGøFr¹ ¶ûŒy·¼ß#Kì~ÑĜy9ž8¼7FèÑèÏsKz©ojêb¤PAn}šÜÜþ1D†5Ñ â~3ûXÛÊùÿ9ÚÓ®Y}LÔ_v ÎGÀ«âžÅÉ(äÖ§k»{ÿOÔåW:h—ƒ8Ö÷‚¥YÌÙâNWÜž9Ù®þª6èˆEdh© >nðÁÖóG×ÚéÒ e™ÌîÏFý¾”Â.)i5KמtõÜì0[Û¤'Ǹ×ln±ú¢ºÑ÷5íl"½gI‘™?\+§ôîYµýKKFõK_·ì®YaÔò]¶ ’B¸tCYm¶¥ÑáïH¤÷¦¼šwÅý¶º{Yœã“(Ê"ù4ž b;ß Êì m×Îß6Ú¶lÓ–5ŸF~'ñîmH Ó¯j®êMµÜᰰйL`¿ïfЯ;RÌÆcA܉ٿ§Ãþ›v­ºhgœ@™ú¦9Qäw³¥”Äp醲žTËÜ+RDdTÆKAl1ó» õÖgÚÂK¸üì=ÓûÂþ;Q1,¨lf×-`*†"RpÅZwƒÝwwLºÿá¤6Š= ? ÿ˜‘dL9<ÙúæÅà—:(XA´. ¼c«9¿‹àw†=™r{¤¥qÞóù=[ß¼Øñ?Îç2²™¹õ“:ºzÿ¥ÐyˆˆHAt¸Ç͆ý‚0l'U··²oßîÑÎMÐgó;Û ¾ÇfÀx, Ô0ñûÇ’Ž®î‚“Ç!Ú€VƒvèrRd€¢ï?*"c/‘‚hX[ûŠùÊ×¶&1` 5 ͧà~Frmø·Ýí~R›:Z3Û¸íÔa)»Õ^³nFØÍ2ìL3¾\ãŒÙ§lzÐ ö#3šÂÐ7–WU<9RÛlýÝ5‘û Až€ÛuÀÌÄ2‘q«Xï!æMŠh±'³ÌmF}Û3_=ôI>æ­×² ØV·tíö+É‚˜iX;ç]ÉDóï•¥Ê.éº ^<Ô#ÚV^Øl6d—®ý4f3“ÉEDƳ’+ˆîvaaÚIÙ{Û®›ÿh±JN€]ÄÉ¡;ÿØÞ¸ð‹ „)µûYnÀ;âF1ø¤Šaö΂¬mo\ b("‰)©‚8íš;&öNóþlë³{¾ŸHB%Ê=:1~ V%‘‹ˆÈ%U£žô´ØAŒû“h PÊ ‹û:x¦²âþD’TRÑRAü FÝv$JIó˜½ìÚ’›ÛT>""Pb1Jd‘ÇÚG€˜¯ƒk{.Ƀ’*ˆnìÃÔî-6ëŒ`Jæòõ™DRTR1@Atü<.[S‘D>%Ë}ØEó‡Ê&uk«(ITIÄ ¢|;ÄîR“™lŸL"Ÿ’e¶=vçïÉ­/¹u´"’?%UÛyÅÚCÀÌ®«ih>/”J“ñtÜŽší깑KnO%‘’ˆHIÄA›ˆQ¸7Õ64¹nÙ]³ˆWZ-ŠH\¥÷&b¬Ç™Ÿ@¤2wÿLè©Ïdꛞû¹™?þœ»u¦Œý™Dw+«qçH3ŸÞÇ'ÒQu*K•­ï ûh*kv櫳]=Ô7ýø;Oï# Ú-p#›QÙà5îÔ¼ÉáHàí±s‘ ¡ô ¢‡k µ"ɳÀg Ü4 ˆ0ì÷òà˜½|LÉzéº ^¬­ozÈá”ÃÖ›»9î`æ$¹6Dd¢(¹K¦m+/ÚìðëBçQêÜøZ¡s9XÉD3¿±Ð9”ºTÔóm`W¡ó9 $ bÛŠßÓYbaµ4^¼ø§Bç!"r@ID0Ç¢¿z I)k›\ñqÿe¡ó’-ˆÐ¾bÑøÿ}¡ó(i¹¹ýXú/€–B§""R² ­qá­_(t¥¬må…Ï9\´:)m%]ZW.ø<æKíqX í+<âQp>ÆÖBç""¥«ä "@ÛŠ…«c°¥Ð¹”ªöUó6ö÷õjØ ‹ˆ”&ÄA»W,¸§ªwÏIîü#ããò]kYØ:‰$í½aqKëÊùbøÃ7:ŸCЇ³§ÐIˆH2J¯SͶiIðÅ#rë¿ÔßÙóWn|Äà÷ œV?ðÎ&Ì6D›,ôM­‹¶·8±|i]¹°h®Yºöüûfï/‚™Ÿ6ඉ(ÚܶçˆßrÛ©}ÎKD’ÆY™@œÍ Ä(ƒ»bÜÜ2õêu'¤‚hžaç;Ѱc|ì°ÐlÅyÊá7`› ÂMíûyŒ›zyˆwzÄŽ8‰y@ìíšF££qáO€ŸÔæÖL:í3;8˜TåiØ]ÀSãlv|sö?²÷†Å£ž ëm%Š÷{þ@œãÍìgÅëèf¿s|„}ÃÜº¢«koœ"#(å–š£sÙšŠluê‹ümù,Ǧ^1³Jw*È:ìñÁ‰:†Gt8ì ð}nìóÈ_ ÌvzÄ aŠö´ÖmÕÙÆ¡rËÖßóæÀÂYQä³0ÞŒ[£l²ãYƒJ‡ÎGìÃØ‹³gmÙÎ ðÝÃPþ\ÛÊ ; ÷¼DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD^ëÛ‘\Ùu_²IEND®B`‚param-2.1.1/doc/_static/wordmark.svg000066400000000000000000000223751463636336300174030ustar00rootroot00000000000000 image/svg+xml Param param-2.1.1/doc/about.md000066400000000000000000000024671463636336300150420ustar00rootroot00000000000000# About Param is completely open source, available under a [BSD license](https://github.com/holoviz/param/blob/main/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.holoviz.org](https://examples.holoviz.org). If you have any questions or usage issues visit the [Param Discourse forum](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-2.1.1/doc/assets/000077500000000000000000000000001463636336300146775ustar00rootroot00000000000000param-2.1.1/doc/assets/param-is-powerful.png000066400000000000000000002330101463636336300207560ustar00rootroot00000000000000‰PNG  IHDRX‡µ»üesRGB®ÎégAMA± üa pHYsÃÃÇo¨dÿ¥IDATx^ìý |Uå½/þö”y$a c@ ¢Bµ%J+V+Z‹ØVí xzÕÖsôöW¥½×Ëíïô`ï=íñTý ==µZEj«[l´*¨H˜ S™§qgOÿçyÖZÉÚS²“½‚|Þ¼6Y{íµ×ð¬gMßý –öööˆˆˆˆˆˆˆˆhجú_""""""""&XˆˆˆˆˆˆˆˆâÄ Qœ`!"""""""Š,DDDDDDDDqb€…ˆˆˆˆˆˆˆ(N °ʼn"""""""¢81ÀBDDDDDDD§s"ÀÒÞéF[GþŽˆˆˆˆˆˆˆhl97,ݽ¨ijG   """""""CΉ‹ÇëÙ¦tt÷ÂÏ( 1çD€Åb±Àãñ£¡µ~ÿÐ,@^¯Oý%"""""""J´s"À’žŒd§Uu.ø†`Ñ‚+~´¶vŠïúõ±DDDDDDDD‰sNXRœL—†¶.tu÷)ÈÒëñátm v®AgW/ü ²Q‚U„@fª)IvœilG¯Ç«20ŸO ¦tõxÐÚÕ‹ãÕMèv{Ô8"""""""¢D9',­­mèéîBAnj[:ÑÓëÓ?‰N6†ÛÖéFK{7 Æe #٪ƴ¹zT{,DDDDDDDD‰rNXNVFÍ™ZUM¨µ½Gõ&äÕK§D"Û]q÷zñéÉF/Y£höä\t‹qUµ­èîñ°Á["""""""J˜s"ÀÒÐØŒ>Ü…ìô$¤¥8P×ÜžÞÈÕ„dÜD¶ÑrìL«ªNT×܉“u.NÌFNšgÄwkÄøáôFDDDDDDDDɘ°È’&===8vüjjê0)/ Í®n¸:ÝúÁ¼>ê[:qøT£ªJäñúqª® Íí=¸`J.;ªêÛÐÖÞ­ƒˆˆˆˆˆˆˆ(>c>ÀâóùT›)ííØàSŒËJÅbACk—ê~ÙLVêìöàЉFt»½ª[fÙ‹¬.tìL 2Ò’‘›áD‡˜æTM‹š/« Q¼Î‰,²TJOŸ”íƒÃ@r’M5`ÛeêHRd+§›QÓÔÔF‹×ïGU½ m]LŸ¥¾¢¾-m]¬*DDDDDDDDqû%Xd)Ÿ½*DMM-ò³“UWÍMmýÕ|d äLC;ŽV7«ï˜ÉB*²ËæC'àt&ajA¦*½rèDzÙ£Åiì7r«­Õªòx¼>ìø° ÉÖR’ì8QÛªJªÈÏdƒ¶2¸"§‰D–SinïV¥[²3R1.+gš;Qßà‚Çà  Oww÷ذøe ½DŠü[~è:;e$=½hvõ I¼ŽT5£­£G•V‰Fö.t¼¦½>?fæÀa³âHu3Ú;å÷XUˆˆˆˆˆˆˆˆ†FWΜ9sXU½G’AúúFTWŸFj’»Mõ${ jlë jw%¢ÐÑÝ‹ê†v8““P˜ŸWW¯ê¶¹Ç¹Ûg"""""""¢hšššÔßs¢‹×«?d€ÅÝÛ‹Ý{ÊáééB^fŠª&$»aöú°Y-°X ½Ô7ÂÉ LmsZÚ{0kr¬â;r-m,ÅBDDDDDDD1s¹\p»ÝjxÌXþþ,†cÇOâôé3(šœƒIãÒQ“Šœôd¤%'!U¼œì6+lV«êÒ9”ì}èÓSMð‹Ï Çeˆ÷>œ¬kS çÅBX –ööö1]l£¹¥ Ïo~Ûÿö¾>Hr8pÅåÅøÒò/ÀéLV=ÙlvÈ—}â%K³øx¼~ÕKÛãÕ†=>õ^þ•R“ª¤‹,ù’Ÿ•Š §ó¶«Ïˆˆˆˆˆˆˆˆ¢éííEUU•þî°465ã÷›·âíw>ÐÇÈ*@dge"??©)ÉH¯¬Ì ddˆWz2Åpšø›””¤J±XívØl6Œq8’ ËÃx<~¸½>$'Ù®J¾8İ]U""""""""HKK š››õwç@€¥¡± Ͻð ÞyïC}L8«Õ »Í›Ý‡Ý»þÊÊJGnN6òÇå¡  ³‹f`úÔB8NÕÞŠl@W–^‘"U%""""""""Ф¾¾íííú»1Ü‹ €È—×ëƒGoä6Ùn¯Çƒž7Ú;:Ñæj/ZZ\pµw"ÙéTÁ•ñã`³ÉvYdPÆ¢Úi‘Wˆˆˆˆˆˆˆh(|>­ùØ,Á"W²§§­m.ì;pom'NVëŸF&K¯È@Ь:4«hæÌ.ÂÔ)“Té•´´TØÅç²”‹,ír¶*FÐÈïèž±ØñÉm« @©A"""""""ÒÕÖÖ¢³³S7F,2ààîu££½MÍ-8SSc'N¡òø)õ¾ÝÕ½Û#ƒl[Å‘ä@zZòÇåb„|Ìš9E3¦!++©©)HŸËqePål“¥lZZÛT hÒ„ñªýôK´jS@S[Z»TïMÙÉ—•Чƒ"""""""]cc#ÚÚÚôwg)À"ƒ ²êÛÝ‹îîn´¶µãøÉ*TTž@Mm½jw¥££KUŒR’,ñ!ƒ²”JNN6fL+DÑÌé˜8¡ùyHOOƒ3É¡´ííõ¨jC²ÔJr²S}?YbFNïe#¹‰æõzUoHìÜ…²½å¸ýë_Uëîp8ô)†F&GX_·ì ÉØ{¨FzeÕƒ•Œ‘=*=+Irj»Ý ‡HÛn·:ƒ¶7|b?¥8í˜=%LÉUA-"""""""‚jE¶Ãb•‹V-Ư2˜ÑÝ݃ú†&T®Á‘#Çp´â¸*ÝÑÝãVÁˆhdà`\^®_q5Š^ŒÜœ,¤¦¦ªê6’\Žü¾»×ƒ35uª´ÈŒéS0»hºúÜÌÜÈÀŠ«½ÇŽŸB]}núʵúñ3¶»©¹oþåüýýUi=øß0cÚUÂf8ä<>=ÕˆêúvUÊD’;QA¦OÌVmË ¤±­ 7 ³Û£ rÒPT˜#æéÂÞŠ:±¯´ùÊÒ?™©Iøò’YpØÇvµ&""""""¢Ñ"Ÿ÷Ož<©þJ£R$A–Véè쩪3xýÍ·ñØñø¿oÂþv3Jÿ¾SZäçW$ùpŸ—›ƒó/”‰ªÄŠ\‘d‰Yæ…—¶ŠùoÄŸþ¼]½D&@gWÊöÀÓÏ<‹¿y;?Þ£šrm®üöù?à¯{OZA6Лêt £ÛYG¼dÐäT]ºzúƒ&‘Èž“êš;PÓÔÑ÷Ý&W’’lªG%Y‚ELÒ' ¶ÁÝëí ¸‘V !==]7Bù/I5ÔeIYb¥¢ò8:;»‰¼ÜlUÍ'+3éHINùI6þ*ÇËÆleÌ¡Õvuu£ôï;ð‹'¿¿ÿš[ZáÖ«…’ãd°ã—ÿßoñŸÏ¾„ÃG©Þ‡ü "Ȁϻï}ˆÃGމõ늸.Õ—•Š‚œt5OãÕÒÞ­ÚNH·Û‹Æ¶nôz}}ßËLsªvV’vÕæŠì¹ÚHZt‘ã’b¨zDDDDDDDt>ÉÌÌÔ‡`éìîE“xx—UWŽV5áxM ª\htu#)%³.˜ƒ%K®À _þ¾ùõ[°ö;ßķ︫¿öÜxýñÅ«¯ÄÒ+.Å% æâÂ9Eª­ÙžŠ™ê (ƒ¾»·ÇOT£ªºFUù‘U"»tÞ·ÿ›ZT•¥Ðî•AVß‘ÙÎŒQ•'QdÛ(“ó3T»+†ž^N‹ôö¨à‰>2Dk{7ZÄþÕŒ$4™4.™iIpجb83&å '#EV&äe`îôü¾€ iœ²7ãìl5œÐËÑSØ{´GÄßµmª-Áîõø´ª ÕͽhìºÉ$e"5;“¦ÍÄÜ‹àsW}×]÷%¬úÚM¸ý_Ãí·Ý‚Ë/+i 6z÷Æ2` « VÍHcQ£ÝXÉiå÷< àxIJŒºV‰ÕÔJ‰èïc¤-ǯ/§7ârd;+ÙéÉêe¤‡¬ÆÓÖéV¯Prd©¢5­ª‹$¿'4SÇg‰¿²·%‹ ÜÌ/*@ñì ¸dÖ,˜U€ yéªÄËËËCJJJ‚K°tö ±µéò!}F>ÒSœªM&W7j›;T!•§[P~¼A_>9\#^µøäH=>>\½ÇšQQÛ‰¦. rÆàŠË.Q Ùù×j±Žúþ xÈvdº{zT·ÑÕgjU{2µµ p¹´Ò2Zb$ç¡–Ó­/çtxÕ¢¾¾íí*à¢l´ˆMZ²3&æ¨Àˆr’m¨œih‡?¼>ò;=½ªÍ^šF«&䦩ùȪ@’œSJ’]Ud Y’E&¿1YýKr†ò"""""""ú,›4il?þñ×ëïã&^­kéTA•ü¬dŒÏËPU„"µm"CñOvì•/_@Uq‘ ªº½>t¹½¸pÆDkÖLLž4A+"žñµR±Xd`@µ·Òã6½zmW}d‰‘?¼òg”¾»CU jsµ«Ò,Wͳ×ãé«ÎSQy¯l݆²=Ð%ÆÅJqZZÛðò«o⃟àLm½ª$—c´)#Ûa‘›†Æ&U"æ¥?¾C‡+Õ42È"ÛPq:ìªTŠÜ^D‘%Ldɵ Ÿl›¥Ím2ø£F©^¦OÌÃ.Û´ÑÆÉô•Á1UŠH|W¾doCSÀr^r¾r¼[¤ƒ|Ãòo¤Ñg]Â3IOu¢° SM9rª iN;¦ˆ÷GôEÉàIj²ófä#73Yµ "Û‘¥022³0göLñ}»œPj"õ"•XŽŽ.|ðá'}¯?.ÃÞ‡ô "“ à~ôñ|üÉ^X1ª(Éê9²DMNެ”¦­— ƒ2ó§?oGUÕ™˜K±Èî¡ÿòö{øh×^UEHnd)99Yõ¢$«¥¦¦¨®©Å¦¨ÀЉUª Q]]ƒ °ÈÀTANZ)±hY’DHd HV ’¥WdÛ+2#“/=%I¥±Q)Vr²Z’,9c¼œI6¨‘mº„­ÒFDDDDDDDgSÂ,ò!» 7SÆe ¹ÓS5-˜,†Çç¤ëS„“áMËWAYÈa³aJAZ;zà‡ E3§«`ƒ¤J° !( gjê°ñ?_À¦ß¼ þ>#þþqë›ú‘>]ƒ—© ‡Aöhtiñ|ÜÏwñ“ý·ßv3 'O "T?…²}å1•b‘ÁŒ‘m­ ØJii)¸r饸á?ýÖýð^|õÆ(ÈÏSŸI^ŸG+Žã½»TIVñ‘휤%'©Ïå|:{<8ÓØ®‚*ݽªªN@ÖdW×ùÙ©ÈHs-X%LŸ‰%ó qÅÜÉêïåM¯É¸dÖxäf$ÍO®— ä}Ö%<À"%;˜699âþx­KUs)ÌÏ@Z„‡mYUeÚ„,õ’Ã’Í®uAœ•–¬ª åæåaö¬ê3Ybdh1­Kfwo/zܽê¯,m2X¡ýå‡Q[[ßô٫Ѫ›¿Œ /(Âä‰ãñ¹%—ªàÇ„ñjItÊQ¥K#«=vBUE2–#ƒ8—-Zˆ[o¾3gLÅ”ÂI¸fÙçp³XŽ,9c”8éèìB…ønKK›ú®lO%7Söø£}.ÇɆ]n´v¸Õ_£fU–Ø/SÆg ¡¢U¿ ±'ŽKWmåÈ¿“ó31!W«ÒÕÑíQ K2è”%öáâ '©÷DDDDDDDŸe#`‘¥XdU¡é2ÑÝëÅÉ3-HOv¨ªB2PaHätÙéN\85)N{_éù¿ÝnUÝ«À€- K/_¤â“ÄKvÕ<2v!æ—ЈD`Nœ¬BGg§>ªšNñ‚y˜4q‚–U…23ÒqÁ왘{á,}*-°!Ûd‘ÔFV:~ü”*½bU.[¼ùù"M’“UÀ%;; ³‹¦«`‹AZÛ\hhlVïe5ñ¹iªÊŽA6N+ƒ,Uu.ÕvŠÜdYªDN—)öÏP«IrŸ%Ùm}/YeH–9&ö±l‡E’³ÍNKÆÜéùŸÜÛÑgшX$U2eR.rÓhpõ µ½ÓÆg!3M<ØËâ)\].á²äEhÐDÖº‘ç2`ÑÞãÃäÉ1iâx8޾’1ÓÊ Åø‚|L/øå_ñÊËÍÑ'&—'K–Èà‡lÌV’ëš••‰üqZÉfÙY˜6µ0(X!«56µè=5„L'×qBÁ8Èö^ ²4HFFf„.§«Í­ZéÙ6M~všjÃÆ ÇN· E¤½!ÅéPU±’“"÷ì4*­z½¨¨nFS[d{/2ˆ#—Q49“Æeô•J"""""""ú,±‹ $';0«0>_5í°Y˜)¼m6 6+fNÊÁļ 0Å úd§§ UÌÃÕÙ {’—,œ§J ¥ä…,#3ÿxßwqÿ½ßÅ?}ÿ.ñ÷;¸ýë7ëS„ëŽÐËPzZ*’’«89N¤‰ÏÌí°xÄw;;»ôwÑÉ@޹ôŠ”™žŽ””þ ‰A6r›““´í²Ke¤‘I61ÿ£UM¨iêP êJ²Dˬɹ˜:!K•J""""""":ŒX€ERÁñYÈÏJF«jð¶U5Æš›™Š™“²1] ËÞg"‘«Í¢Ú ñúýhïö©jB2Ða.Ý1(1°˜5sŠfÈ×TÌ 'MÐ'ˆMô EZI=ÆÑg éûXäwõa|:NR‘ˆ1‘þ‘2-s2“‘•.«ÿè###ƒ²dIªÞnÓÔ‰‚‚q˜]4Î%I¢Q«X‡Ö~‹ü+_ª7¢(äüå´f²´‰l 7T¯'K«A I–6IKMÑßE'ÛXIÒ»y6´wt¨y†òz¼ªA[3¹]©)ý¥Qä_™Î2È!«a™Éà‡,½’‘êT%ˆâáóûÑÖéÆÁ èìîU%edð&/+ó¦ç#-EìWS‰""""""¢Ïº °HòÙ?;#EuÕì÷pº¾ y™)ÈHKR僑ÕL²3’ÑÓëC·'€‰ª–‘«z¢‚i)HOKë ÂÈàIk[š›[êôȶZNU °¤¤8ƒÚx1 ’ìeG6¢+ÿÊêNùãú»_–šQ[×4?9,Ü=UuF5ÐkHMIQmØç/»¹žZ©ÚAék4Xü‘…~d#ÃN‡-rA˜ÉÕêv{q¸ª Í®nUMH.GËdcÅr_Ų_‰ˆˆˆˆˆˆ>KFåIØé´c⸠ŒßI‡x· XrÅL•ŒÈL-ªJŠÍfZ¡am­L2 )ÉN} ÐÙÙ]eûU—Ê2@âñzÑÓãÆ¡O+p´ò„>•&'; “'OPÑ-HÓ,ñy}8}¦V}W6¾;}z¡˜¦{Z[ÛðáÇ{ÐÖÖ®‚9òÕÙÕŠÊ“¨>Ýß3‘LƒÌÌtähdzÉœ&æ¥÷•"‘¥Ydµ Y²%ÞÒ+ŸX£g"¯Ö“\¦ ÞÈ},ç.«ÉR-Æ‹ˆˆˆˆˆˆè³nÔŠd¦'ã’ &bfaîòÓR’0>7]=ÔË|s鎑 K„̽h6&N(ÐÇhÝ<ïÙ[Ž—þø:ŽTéS§ñá®=xýÍ·qút­>•F~7?/«Uµÿ"ÿzÜnüõí÷°wßAd™>µéú§PÁ”½áÕ×ßBåñS8qª¥ß7¶ýMwŒm—U‹fLŸŠ¼Üà†o%Ùó´‰Ù}=øÈ¶Y¦ägªvQŒp‡«¡¥§[Ðíöô…ÒS“TÕ£ö.7ZÚ{ÐÒáV¥[šÛµÑgݨXdIŽä”$¤ £UÙ¥ð¤üLõPß"ÜG£TDá䉸ܒÅA¥Xdu wíÅÓÏüÿþÿýÏ¿ø Nœ¬†×§õ #É Ì‚‹/BFfº*©3¥p¢j‡Æ »~–%Xž{á|°óŒËËÅ•b9²‹ ”ÈŠ,½òö;àÉÿø-žxú7xåµ·P}¦FŸƒVågVÑt\¹ôÒˆ¥ydÉ•üìd¥ÉöV¬*@U8^V%Ò'&ϯz jkïÑÇhÚ:{°¯²ïí=…÷÷W‰—þw_>Ø_­OEDDDDDDôÙ5jùp/öåÃh‰‹ÁÈR)Iväf¦ÈZ7¦ 7#G6R»pÁEXzÅ"8Ij}>::»PW߈35õhhlV%RdPDÆåå`å _´©…*¨"»]4]U’óä´²±Ü¦–V•ÙÙ™X^r%Š^¬M#–#«µ·w¢¦®5b9²íY-I’•É“&`ùÕWaâø‚ˆi)«_%‰eËÞ|2R’Tã³²±à¡¦{(Glw¯ ´˜Én¸e»,íݸ:Ýpu¸ÑÞÕ«^ò=ÑgݨXâ•$«¹d© D$2t`³ÛTû)ÆK8"·×¢DÌÓÊž…ìâûù]ÀøÊõ_Ä•K«’&ÉÉÉZ›@@A$ù=Ùuô”ÉqÃu×à²K +S«ò#×U6v{ã——«@‹l3Å锥JljY™™ÈHOä‰ãqó+°háÅ'¦—ˆò{}ªj’ ŒÈ^ä´E3§aõ-7ââ¹s&–Œ¥ŒËNSm±Èt‹Öv±¨6UŒ—l4×®§¡_l·,=$×ÍxÉêHjX|WÅoô—lÄ×øGDDDDDDôYgûñ¼^Óäs{²Ó®â8‘cú©R!M­ª‹cÔ/YÒã‚Y3‚ÚR1tvuáø‰ª¾ieeê”ɸ´øb} -È’®5ä_¹Äw/¼^¯*‰"©0¾‹.™‡k¯ù¼ªR$ÛS1‚:20"_rþr:Y*FÎC~.ƒ%W\V¬ÖQ[´åORãeI™^½!]I~oò¤‰X¼h¾|m æ]4Y™jÞQ Þ¦9‘–œ¤zŒØ$DiëèQ½¥§$!S|?'#ã²Ráõ‰Ï:{Ôt²—¢X_L nˆ—ˆˆˆˆˆˆè³ÆÒÞÞÎ"DDDDDDDDqˆ¡Þ „"""""""¢81ÀBDDDDDDD'XˆˆˆˆˆˆˆˆâÄ Qœ`!"""""""Š,DDDDDDDDqb€…ˆˆˆˆˆˆˆ(N °ʼn"""""""¢81ÀBDDDDDDD'˧Ÿ~Ї‰ˆˆˆˆˆˆˆh,ÕÕÕ °ÅÁÒÜÜÌ Q,]]] °ÅÁâñx`!"""""""ŠƒÅï÷3ÀBDDDDDDDK@Ї‰ˆˆˆˆˆˆˆh¬ú_""""""""&XˆˆˆˆˆˆˆˆâÄ Qœ`!"""""""Š,DDDDDDDDqb€…ˆˆˆˆˆˆˆ(N °ʼn"""""""¢81ÀBDDDDDDD'XˆˆˆˆˆˆˆˆâÄ Qœ`!"""""""Š,DDDDDDDDqb€…ˆˆˆˆˆˆˆ(N °ʼn"""""""¢81ÀBDDDDDDD'XˆˆˆˆˆˆˆˆâÄ Qœ`!"""""""Š,DDDDDDDDqb€…ˆˆˆˆˆˆˆ(N °ʼn"""""""¢81ÀBDDDDDDD'XˆˆˆˆˆˆˆˆâÄ Qœ`!"""""""Š,DDDDDDDDqb€…ˆˆˆˆˆˆˆ(N °ʼn"""""""¢81ÀBDDDDDDD'XˆˆˆˆˆˆˆˆâÄ Qœ`!"""""""Š,DDDDDDDDqb€…ˆˆˆˆˆˆˆ(N °ʼn"""""""¢81ÀBDDDDDDD'XˆˆˆˆˆˆˆˆâÄ Qœ`!"""""""ŠÓXʰ¡¨E}¯ bÌè*{Ô¼ü^‹WàÆ›ïÀoFY­þeúìrU ô—wàÉ]úûarU–âÉo?!_Ÿý¼OkØõ,Öýx+ô÷4xP¶eî½q æšÏÍß~w¹ô‰âz]ØçyଓéöÜ:¬ûc„Ü\»kLÛZt׿Ï~žl›ãýœÁëŸfÓa s…k÷€Ï­Ç½7¯@qßþ)ÆŠ›ïÅúçJQÑ¢OGD§ó»‹8ÙÜ¿[¹«®\uoTéÐgŠX).^5ïÀpÙ´ÀJ1Н]ƒÇÞK܃<X¹q.–ܶ›k{ô±tÖy«°ùÞeXõÐFl;Ø·>Z›ßÛŠo¦>‚úèU7/ÁªG6£Þ§'¢óÏCäFÅ–uX±XÜ>ò,¶í¯0ݺP±ž}dø¼w–ˆÎu¬"Ô§›°væ©•FCÙSñV4ex’•sSíf<,+yl9G¶â©íÑöK ЦëƒÔ§á•‡ÕCÕÁ.}Wxšª-÷båC›Å]þ`\ØñèJ¬|´lT‚,o¼ñÖ¬Yƒ?üá8~ü8zè!<ñÄú§Dt.c€%H6>º,ÇBD4ò\'*‚Ï·«žBÙáJTVŠ×Á§°z‚>ž(^Vc“ÌWÆë׫‘¯D‰PŒuæô­\'Æœ˜cÊÁ'±ö¡Òþ€Iê\¬þùVì<¨ïŸÃeØö«ûPœª.T<óá[epå?øþ®ß¿ýÛ¿1ÈBôp^XÖ¾h¾Øé¯ƒe(}q=VÏuêSéÊ6£´R&"¢ãî .VrÙ"dÚõ7Îs3QÌÜØñÒ“¦’+S°ö™-ذjòË‹=³–?ˆç~y'ú¯8UØø»Ò8K=¬¢"zydùío«¿#¢sÑÙ °Ô–aóãàsÆâU|í¸ã‡aëþ®ŠáÌÄ”ÅwbÃoÅ }”æ *«Ã#ײýg¹7^[Ü·®EEs±äÆqï#ÏbG”Fr¶¬1M¿›ÅtUo¬Çª+çjã¯ßߊ ó"e›!a qaî•7âÆ{ÖãÙ÷"7hiY²®îŽgD:÷­w1Vܳ[šÒWŸ¦oæ/ÁªnŒºMƒ‰¼.”¿öøvÿ6_{xf¼Ú÷)¿ˆ´“yeó®Èéa4j¹ê}„nãmú÷uÑ`Ý*lÔÇh6b•±ƒ5†(÷g„ô-­Œ!×îÐòñ]•çôÙ†yˆœ•|)O‹ï«ü¼½®Áò€¬s¾E柱d¾þ}ùÒC}ìµòðyV^¹¥ú(åuXb|ÿÑX› i¸Pÿ^Ã{Ïbý=ýë$Ñ{ÝŠòXöÍ0ÒÄýÎúþu¯5["ä¼ÐÆ:‹À¶ëSö¨‘§äëF<XNÞ Y¶Z¿êmX›qÜÊ|µ¿ûåwÕçK Ú3(}hIðw¥ Î×âó¾MÄãÆUŽ­ò¼d>ndã¼açè*<»Êø®|EÞ'¶þÀ<í*<{²ù¥Ù  ÷Êóè£÷bÅb}ÞòúóhŒ¼ûŽ|ì„å/5¯sN"ŘB¹nÅÓ1®]·¢‘òÉH]óc;Þ~ˆ}¥šá佨 Þ¸kpCÓúçòº"·½ïÚ®g6D:§ÕPÏaµ[±Æ|}Ó?ðFø„U/„œk®ÛÒW]'ÆFn‡q<ìs@¢Îå‚<ûáAç;b¼hصY}·=´<>Ð}œYèõvðóÏAìØb:Ж݋µK"îW®Àú°òÚnŒÄo­²äŠ,¡rÿý÷ãÿñõ±á~õ«_éCDt.:+–ª7ÖaÅ•«°î—[±Ãܰ¡àª'Ä?>‰n'Ο•Æöðœ¥(Y¦ëªÌW7Ê'ôk׈‹Ñ6 z(v£áàAl7w\YŒ{ÿ8xå"WéÜùƒgÅ ­¾Õ-ØÖèç|÷®ÇÄÅ'RC\â³Úƒ8ø–¸À|{ аUƒ¤«üYÜ»d î|;úÖÛ…Š·6â—ië+nÆÖ]¯MÓ·N]âaöÄ6­HL›4'Ä2¾º+ÿéIl}¯›\•;°õÑ;°äzóÍIyó%¨”_DÚɼ²NÜLûIìˆåny”µykäþŒ¾k®Õ÷ADnTåøàϦy^‡EEúp"óžëoØpû½xVÜ~R<4¯Äò¼d>nD~ÜvŽž‚’¯š+ lÅ›ÑÚsríÀßÞЇ¥e«qÝ4}8Õï¬×®»Ïlëï-C^žY‡U_™üYÊŸ‘çôü¥æ%Ï9KÄÃÜà×Ò‘çÂŽ§W‰Ñ°ÑtŒk×-y ¬Â“ûßÞѼæG>ÞºpÑMg7ïEÓ¶#î×¹í}×ví<³QžÓïÝ<ø6G4ÌsØ„•xô§%¦’ nl}è©ýÕ§z3ÖÿÄ|†‚µ?}0¨ŠÉ`Fï8çr¯È·?÷Ââxxò;‚Î!;»èªÀ³?(Æ’ÛÖ©ïö¯‡X‘Çûïã6¢<Ò½ ñýëmðùç1ìÛŒb<¸ßTr} jí ÁÕUS#bâaT 2ª d™;w®>DDç¢Ñ¿Ë­|ü –ƦÜ8øë5xx¤o´ZÄÃÈ;ú°nJ~Ï®7Æí¿<tŒÌ…m?|`_{J±á'CÚx™…ûîZµÄ–mxø»OÆÔp™ëðÀ‹¥XÖ?¬¾1"ÖWþ¢q×½Ø5L_ÿý1” ¾ñØõw‹e ÔÀhåFÜþwDHc7ÊþõÜCµ®÷÷¯Ã¶j}Ęð,îýÚz”FÝŸ2ÏlÀÖûH5Êöȶ·»ë ž¼ýÖ8ƒ`#›OŒÆåËÓ±îk·‡?°ˆ›ºgÿi e÷s܈5bYû!¢Öã-œÅä¾ù®Xïù1®4É\ŠënÔ‡¥×v`wP’¹±{çV}¸ß¶²r}HW¹ošÖÍùåÏaž>œÈ¼Wú³uØ’³¾7®NÓßœ³vో”çèµÿÚßXâ”å«Q¢K[ߊ|^s½÷¦xîWrýÕñ·òñzÜ~׳ѯ»rþwñpáØ‰û%øn}t sº¥ v-ïlÀ¦ý¦« }3ò1Þo4¯ùÑ·oÞxó^TâúøÍ ØõÚ#rÂöuƒns$ñœÃò¿ú(]nz¸îÚŠ7ÚðhÀÖŸŠkºÖL¹kŒR*"¢Q¼Nä¹|ÇÏÖàÞº÷ßû3tÙSÜ?­Äú%B¹ÞÛ€[ÿikpɲ¿ï>ø$îX5Àuñ6 ô‰'±M+9W-E"C¡m® dùú׿®ѹhÔ,e[ÍÅ%Kðà‹¥8h4jx¸L\¼ô]éÏ6…Ÿ°¡Ë…*ÙuëmTeÄ»hºq±¬Â«¿Þjº Ì¿ê_߃ïlÂ}¿úJeØCÝçò Øf4°U¹ kË«zãl5]f}kJé–bÓ·féŸhÊ>>8ð…Sp._­»ôõ}kJÌ¿²ˆ «˜=œ‹ïÃïÞצ){m}ð4ÕÏâoa¿~…n±Mιkñø[eQ·ÅýÛRì¹¹WÁ­gL·!©Å¸ï¿Ìéÿ;âúõóÕbö j,qÂu¸ézmPyíÍ¿¸º°ã-ó#î Ü´\K‡üÒlÝb}d(™_ÍçO¹/ugк¢Z<`å%!Çp(uìdbÅÃâXÖç~^,öðLm{†—Ë^Ù€Õæk¾8Æ×‹{‚þô‰l4¯ù‘Ž7gœyodhÇPæõë°å}}E~Zoneoì P "îsX>V>œÝÏÝ‹ ï¸Å=É¿àas¯g…k±á¥ýù?ñÜéàs¹[·²Øµ¿Ø¦‘Î!]Ï¢4¤>Tƒ8¶×™ÒLÞ nz[ßßæáÞþ0ž|¯úªúþ¬UúîUä1ò»ï‹ü­&¬{ø¿B~LŒ« OÞ¾ k~k¾×,Á£ßÚ~ˆ9¸"ƒ)F@%ZåßÿýßqÍ5×èïˆè\4Ê–5ÿº5)V\2N£QC{&æÝþ8¿=Å_]‹õ¿ØŠÒ÷×aiœg¹þ67L¯ùÅ(‘]·†þ2~ý·pS_‘Ø)¸S<øoýÅz¬ýj1òo_‡uËû××YX‚;îŽqW4ÖëCѬÀ£ò&<Â6M¹í9”¾ò8ÖßµÅîĺÿ§SŒéœSPrÇÁõõpi©wâ©_܉yú¨S\`¿{È‚SWâñÿïA,Õ ™sïÄÚ À¹å•qÖ»Ëøí:¬,Òoä¶<¼.¸¾«¸ÀW5ꃒ·ÏŠ[Óekÿó9B¨ÞˆÿŸ©ÚTCe¹)ÌÅÒk‹1Åtã«Òóçw"ñJ¬ýéãØúöN¬ʯ‰q˜r×&<'n®cTæÇÿsÖjï•wžÂæ¾HFbÒ$ÿª¦€‘Û>6ÝP†”Lé³sú›¶-å²KçkC#‘÷VüËãâ¡ttöÉè Mqý7ÿf~ø17–˜‰’kÍÇþVì-öåÚ7_Ó‡¥ë¯C‰9€8lN¬üù¯ûÏŸr_._Ç0å³:†#™õÃß᩻ű¬ÏGÇëþŸàkéŽæ ÂÙQò3m{†—3狇T±ÏÍ{ØýÜ«(0ê1Š×|!òñv6óÞŠÄï_‹âB}}E~ºó¡uÁÛ¼³CÉ 9‡®Æúÿ\UèÙGVâÖ‡Ì÷$S°öçâšÌèÝ'þ\.Ï!Ïaݳ´ãACÄ=ñíÚ§†ŠjÓ9DÜÇmþwsyŸ¬fJ¦éKÕÏCOþd)f]µ÷ý|Sð5\|ë¯L›eòüººï^E#KxO™îW*žz)öeOnƽ׬ ®™³þ´ +Ôƒ]hpESŽ=ªÞK¡A\¹þzs4”ˆÎE£`ÉDùácÿ¬\&Û6ÙŠò“.¸Uô\ÜÄýt'¶ü«x¿q¦dãÊ2\ò—Ÿ‡Výz€Ô)˜w£¸ñù×-ØT7W“•Z ižiׇ¢X~–F»i‘›ù+qçOÇ–÷CÑÒœZÚþj ¸´¯–„]ü g-Õ‡t_½)ì&jÖœà_GâapÎÃܶo‚ìÙŠ'ÍŠ·¯ÃƒæÒû¬~àAq«c7B[F¶õ÷ØÍÅ×/úeN4?Ô„*GéK¦‡“%+°Ô|Ìèd[ý·Ënlþ8ŽbF#‘OjßÄ«æºüóÅØªHÛíÄÒ{4m Pö+q>Ї3óÍß9ˆ _[¢Ú*غ¿ .ýžH–¾ÙùâãâFo%æMËì¿YQs±æë~áJ]Š[טª°cŸ¾?”&˜°%z@DªÚ¶»ï!¸a_iÉ”T§iýLí°xew}Xº]ì5áHä½\·$èŒúÙqû¸)BúLùêa%¿ g.»)¨ g·W ­¢±òË%Á×£a[›–‡Ï©`BPÌ`‰Ê¯a"ŸgÍýœ>4VˆýøÕÛ[xî z¨Œ¬0ÍkþÇÛÙË{Ñͽcæ…ž¯‹.ÆðsBâÎaSn^\š¦º¦ÒFC®¤ŒÖ=ðHœË#Cœ˜·`€û€“!yÎŒ¸˜u×ï°í¿ǃ«J‚¯áGJñ’éûK¯]Š)a×w'‰íëÓµ;Ú Cõf¬ùʺ êѪtÍŸŸÂêµ;)¸"ßËñfæ ƒ+DŸ £`‘'•âÿ~îZÙ;ÏXyM1æÎÑz`xl‹©­Ñ’³ëÿ N¬.B¡Ü®TìÚ*Öû^Üñhps’ƒš9ehušÝ.4T–a«leÿ[‚êþfÊÄü ´Ž$–iâÛ2v ºF*~tóWRqË¢¿ú°ä\vqpQb³œb”,ч¥êÝ0~Hw.XŠ•æ‡Ž®ÕÛÆ7— x®Þ£Çãâ†*–™ês˜å&,ôAÑø%>Qi"–€¥·˜‚8ûK±[õààFy™i _}P<èÃB_;,G>À6ÓƒÂÊ%‹´};"yo¦Œåš>qX*ò_Äc",xl*—Y‚›ÌçÏ•š~m ­¢!F"E†e~ŒBq{ûÏE‰Ë¯¡f¡ –q5˜o‹ÄÃ耑&±ÿBÒ©ÇÈ 9+ð`H»fªG_®SÝäνq6T¯$±¤LÂÒD˜µø:S©-ñ ´[lwHÉ”•KVcéUýSí°TìzÓtÓºW/Õ¤F"ïÍ/Äàg¼s“3ô¼'–ŠkNÿ®}¥F±ü*Îo­ˆ»zlŸ\q.Òb®–“Èü¬±<»›‚=ç²Ñ¼æ|¼¥¼7€‚˜2Â@y)D¢Ïa²ªÐÃ!¥9…•?¹oˆUƒúÊ=ðHœË‹òƒKSEá6Ý~…žC Rc9 ™ÄºßCT6 r7$KƘ~…›r×ïðÒÏVF(3"»°-ÆŠèï.³hî|x6½RŠm?Zuš/0zWtÅF—в+»œYXqû:løÕV”þy=\y‡>+ª]èÕ?k¦\¿/½µ ë¾Z<`‘uÕ‹Éñöªtޏ ·šžw·îÜ ÷Éø ï\9‹ædbÞe+ú¨T;, Ø]j ^.+‰^}!Vå½ìÏ'N±OV›ÎŒªî²¦_“X½\/YDç´Q½ær¼1ïEíæ­ÂŽwvëoúmý×g‡ß°üY¸ŽÙgä>¢ªmà­p¨0ýÈ0·~uiÔg€¡bp…ˆ¤Ñ°HöLÌZ¾o)ÃÁÛ°åëqçµs‘éÊøå±–M0÷løš© Û¢;±éýJ”½õÖß½%ó§„•PX:}œ>4Tnìø¿æ®èdE;Q¹kžúéZqá.î’" wicݸéÁ¿U TFØÝ¦nû¤~†n Í=Þ øZ‡aö‡3"ò ƒ¡3'ö_³>¨K¶éºÌ¢¬ý×-(;¸Û^|ëo_¹"íç l|êÕQ¨ZàFÛã8 Mû<|î˦í§¥e¦öW ¯Ã"Ù¼Æü¥X­¶bÇ[ ú[anÉ¢èÕÎѼ7Zê»Úô¡!r.ÅMæF¤UU qþßþ¬>BH½7RcÍÑ$ú>ç4‹c\gᚎ佄‰óÖðÚz¬7÷d¨| >½•¡Aö=ðY:—·¡CÉ’A˜{Kð5Hï€î.s± "äçêƒqbp…ˆ g'ÀbâÌŸ…âïÄú§_ÃÎý•âb³BŠO–V tc7r\Û7ãYÓ/KïZƒ’ºz¼Áßá\¥Øü[Ó¼–¬ÁšÐ.½ò–ÌDÜ sicÞ”YÁm®”–™{“ qD<\êƒÊW÷Bp.™0óÌ7YW¢z¸¿”MÓ‹‚~yu¿sÀÔIˆ–ƒÁ] Ï/ÁÜhOýÎ|ÌZ¼wþô)¼öþATÜÖ5.Þi¦ŽœÐî‘ûÉöƒÌJæèEMœ&s—ö‡NPý6˜{k¸vT3¢ÎEXz££”þûS¦_ªçbõ•¦”;Wò^”bìõ £ÛòÒÁÒÝ‘yîr4w•:³C®Å7Ügªâõ,JßÚ†Òçô·‚óöëPœ _T‡m¤ŽásÅþ8± °åû‚Z§Á¼¢alìYºæŸyo¸y«ÝЇ‰ÞwÅ¿þ/<›€SΈÜ‘s¹³°(è^¬êãò!ýø‘_|¸#AÏFw×ÚkV' Ç WˆÈlÔ,î† ”½ö,6üð¬øö³¨9é;óçaõ­Á…bgéÇ”à(7ÐÖr©•ÅGßÚ¡¿‰SW[pÏ7î° {Õ{Û ¥}âÝ\”ÏmîJÏ öÁæÇ3÷tâÎk‡ÓøÜX1KW™Ö¾ëYüö­à|xN˜°%æ‘öoÀ†-Áõ±5nìxê1lÓßIÅâøï»½të JÿìÜqíá7´Î|Ì[µ:¸ý¨”`rãÙ_mFUèMk×¼´ÉÜ~P1JéM‰JsI‰©·š*˜ ¬,6ziÉDÑ¢þ[ܪjÓòŒR.}ÆhÞË)0= Õå¨ }ðé¾õ…Hi9‚ÞÙ„ÎIU|V<¶ös®Zô¡\\Åkó6}g î»!‘¿#S‚óë¹gžü²°ë0ª_ų¦€„ì]ÅtˆÅîl]óÏ…¼7l‰:‡¹°íчƒAr×SxÜ\úeXÿPø=ì`FçxŒœË§-Âu榛ÞÙŒ7#¥ö?‰¯½ëŸÙŒRS˜»4è>ÐýÜoƒzý+ÌÁiçÎøæ7¿Éà Ñylt,-[ñÀ’XõOë±ñ;PñÞzÜ÷³­¨hè¿­pŸÜÇþoÐí)V_v–Ê#„q>ø7àY£Í–rlþÉZ¬3ÿR) »´-äw)y3ûÛr¸äÅ×ëBù–uXû“þFå—ú³Ä¹k~b¾É¨ÀÆïÞŽÇÞ«Ò»2y¥zž\³2h8—?Šû®øn¤â°ö {ƒr »nsœ¼n¸j]á7áÃæÄÒ[ï3=œ¸±õ¡Û±a»i»Êñì?,Q½ lxn+Ê*ú>;¦`õךŒÝ(}H/[ÊÑw¸ËcèÇ«°æ×¦‡¶¢ûð“UÆ·\ØúC½Aé_oÅŽÊXÏzl5ooWv<þ³ÁhÍ*+Q!c±ïêýcîíò˜ÜŒrý†Oí—Zƒæƒòúoᦾކ‘&&!¥Sú•`é¢þºýÁ âö›"nâƒ;Ë£yÏYˆ"S·ÔêÁ÷QqÝÐÓLžûnHºŠs’ÈOUÛׇœ§Åë-¾öy(ùº)'˜æ.¼%éÃ1¨<\¡]'ºGvŽ ÁùõTñÌí¸ýñ¨2òÚ‘­X'{ÑÞ*Sî¾ix ž­k~óÞØ“˜s˜ëÿ…^3¥KêXÿÀ ¬|ø)Üiþá§l=~>j¹®p#t~#çr‘×VÿÀ|W&®áë°y¿~¢’×àý›±î¿?†ƒ•Ûðì£ë°ææ,yTïB\ÜÞz¯)TÛ%ÒOö¬uRO/õýg±æÊ¸ã‡âþüµ² ´Œ¦aËS/Dk°YõÂ7®r3ÖݬßGËkðÍë‚)‘Æ~¯ÿ>ïÛb­©¤¥ûàF¬¹FO/õýõ(­­ÀŽ?nÄúZ…·?6z GŽQ_}õUUåh ®?F½ŠÐ”UO᥇—=tDã\ü ^úÅê„u6dòbø¯+X×L¬øéï°Áü+ò[»‡Ùº¼|˜|+èÑ#óúõøÝÏVêï¤mØ}Hü,²OÁê§ÞÁï<¿d^µ[ÄÃnq؃¤¼]•ÆKõõ±þä›’›WÝÔô«G̳‰‰Å?ü6Ý57ÊòLRçbõ¿oĺŃNyˆíxx+¶ý|5æFIƒsîj<þ§MXm.N,®ÆSϯ‹­§›Ôb<øüSáó@1V˜3«n÷ÖwŠõ’Ѷ+êº$ ML2/+ ®%8—\lúåR°ÏÅÒ¯êÆTÙH·>dlæ½)·=|“J¦÷/Ö…¥ÅHZú“Mx*jw«À,q­ÛøÃ¾ç®Àš°}0k®ø2›¿÷Uíêo#’á›_Ï%rÿ˜çŠÄ¹éב¯9±9‹×üáæ½sB<ç07J[MèÎÇÿ¾¾ÿĹlž ª*TŠuD¨.EBîc:Œ‘s¹¼ûÅV¬7¥aT9+°þOa¥ùqn_÷Ü&¬;øºÉsP|ÇäÐ>|)))¸øâ‹õ1‘1¸Bt~9 Ü:1ïîß¡l×6lúéX1?¸åtçù0p6¼Xвçî þåù,˜òÕ§ðÎkc­©…w¹Ž+îÞ€-ï—‰ íR\}•é¶¾úI¼´s˜¿®ÄSooÅãw›zHIÍÇÜk׊ô؉²¿KK–š"ªðäËzQÊÏ*{>–~ß”_ææ÷ß,äÌÒóŠH›ÿZ‹¨=Š‹öãoÿNuóÛ—×dº^µ‹¦Ç~C‘yýãxç9Yb¡´üº…±Ï&6b»K~òv¾µIë1§È´qúºß÷ó-(Ýõ6\?–‹ã;Ńæ¼¶«[~~V^5«ÿÆRåí;±þW۰󰲯*M0çüµøÝÎ2lû•ÖÓBPïAAi±÷ͼ#æÝ#±+Lé˜Y$Žã/ÏCþ~¨²æß'ÎÚñ:KHÒÎ cëûÑ×%iÒ'´ aõÒÐâäâœ[zøj E[½±˜÷T°u'¶Ê}Øwp"_<(ÞùÓMØ&Óû2ÓzŽû,¬ø¹ØOÿe>'db–HŸuÿµÛ~¾bÂKæ`þj,òàÔÇ>÷½²-(ߩ劇®s‡››’Àüz.ûWæ9Õ®i›3‹–båÿÃÎ76`E¼Á¤³vÍfÞ;W óæ~gî}Δº²4Åá?²9QZUèuX±}¢Hpë9`¬œËSgáÎ/CÙ+㾯. º†Ëí5Î!e;Eº^á”_‚u”Ç¢vпÍ!é%ÎAq“C$K°Ì˜1C‡¾ÆsÍ\!:ÿX‚>LDD•aCÑ*lÔßk±å<ížø|%ëí/y¨¿M Ùeè¦Uñu•:Ï⟖bËíc9`JŸÌ{DñûÚ×¾†={öôõ$•••aÕªUj˜Á¢óÓY(ÁBDDtžóVàÍ̘㦫ø€K£€y(!d!éßþíßpíµ×bîܹ ®,DDD£Âh§ÁëÂŽ½ëËô÷Ò²Õ¸î³TņÆæ=¢„[°`RSSqÙe—áÊ+¯Ä#<‚^x»víbp…è<Æ*BDDƒb¡ó]"ª•=Z„UÏèo‚8qç•aý ]Ì óÑè` ""¢Q1)´bͬ»ŸÃ:>àÒbÞ#"" °‚¬q÷úaô8ôðÝ9%óÑè`!"""""""¢8± Qœ`!"""""""Š,DDDDDDDDqb€…ˆˆˆˆˆˆˆ(N °Å)¡½5Ïøúй/÷ø?ëCDDDDDDDDc """""""¢81ÀBDDDDDDD'XˆˆˆˆˆˆˆˆâÄ Qœ`!"""""""Š,DDDDDDDDqùnšÿÏ?"÷Ö|ýYÜ3þ ú»±†Ý4Q¬F§‹ëºfü€1^];2à<þHÓ'!""""""":WN –íèZøkôè£4_EÖñyðÿó?£}“x»æ.äü™°h =ðŸ©i§Â¿#Ž¥ÉZÀFÎ/¬tŒ©TŒš_z_œÆ4•Ÿ ù÷9¦åD/EÃ,DDDDDDD«±Ñ‹\ñ¿d”pyW2÷ß…d} Ž‚ríó¾àJ<ÿl|çøÄ4ο~UŸ^ïÒ>ÿçc]Š\té[F>’ž¿JŸ–ˆˆˆˆˆˆˆhxÎZ€%í¯—Âæ:,¡²é×h™ñ?Ðö#í3à=xʃ˻H¾ÝÔ‡„ýšg%\¤?Â[©öéç÷úw6×v|¢—XÑ–a)ˆÔ> QìF'À’9©ÇÿYU»1^΢¸#VÒ>O•Ub!K¿ôÍS7õ{úQbœµFn›ƒÚ>1+—ºãMõy׎ð,AŒÀŠ©j‘;¬ ÑÈm°üŸ©°éA˜–oÆVÂ$yù$Xd£µâ;ýU‹ˆˆˆˆˆˆˆˆFߨ°H™°éƒ²ç¡˜ªåçô5‚›üüÿˆ©ŠQ¢Ëþ îÊ|8õ¶TT·Î/5™“àX£O¢ç›¥ð ¿m—Ô‚r­ZQÑT¤éÓK@Їã&«ë|VÈ  Q,ÆN!"""""""¢s,DDDDDDDDqb€…ˆˆˆˆˆˆˆ(N °ʼn"""""""¢81ÀBDDDDDDD§„vÓLDDDDDDDt>b """""""¢81ÀBDDDDDDD'XˆˆˆˆˆˆˆˆâÄ Qœ`!"""""""Š,DDDDDDDDqb€…ˆˆˆˆˆˆˆ(N °ʼn"""""""¢81ÀBDDDDDDD'K@Ї¢¾¾^""""""úì+((ЇˆF–ÛíFGG|>Ÿ>fì±ÙlHOO‡ÓéÔÇœ?`!""""""¢ÄkjjÓÁƒ ²äååéïά"DDDDDDDt8‚+Ò¹²ž‰Æ Qœ`!"""""""Š,DDDDDDDDqb€…ˆˆˆˆˆˆˆ(N °ʼn"""""""¢8Y‚>LDDDDDDDcT}}½>”XŸ|ò ÊÊÊÔpZZ¾ño¨áxèCç–`!"""""":O=õÔSxùå—qüøqtvvâꫯÖ?¡¡b€…ˆˆˆˆˆˆè<´oß>œ>}Z Ë'·Ýv&L˜ ÞÓÐ1ÀBDDDDDDtúè£ô!à‚ . ®444à…^ÐßQ,`!"""""":ÏÉêA\yþùçÑÑÑ¡¡X0ÀBDDDDDDtžkmmÅ–-[ðç?ÿYWFªAÝÏ2XˆˆˆˆˆˆˆÎ3²ýÙ°­AËž„Þ{ï½¾àÊüùóÕ_Š ,DDDDDDDç‘Ý»wãÅ_ÔßE6iÒ$\qÅú;ŠÅy`éééÁ‡~ˆW^yÍÍÍúX""""""¢óÇÇŒ?üáú»~2 ’žž®†o¸áÜwß}j˜bg úðgÖÞ½{±gÏ´µµ©÷²û©[o½6›M½'""""""ëâmeÇŽøÓŸþ¤¿ë'»g^°`þ.1äs÷ùæ3`9|ø° ®ÔÖÖêcú}îsŸÃâÅ‹õwDDDDDDDc[<Ù¶ŠlÀ6Ô×¾ö5,Z´H—8 °œcd×QÕÕÕèííÕÇô«««Ã‰'ôwáRRR°zõjdeeécˆˆˆˆˆˆˆÆ®áXJKKñ—¿üE×ïæ›oÆe—]¦¿K,XÎÝÝݪhÓô1Ã#‹@•””èˆˆˆˆˆÆ®áXÞ~ûmlß¾]×ï+_ù –.]ª¿K¼ó1ÀrÎ5r+¨ýãÿwpE’ÝRÉ0DDDDDDDŸEû÷ïÇòåËÕËðå/yDƒ+ç«s.ÀrôèQ466êïâ'¿%"""""":|éK_ÂUW]¥¿£D²­ôá1KöÑýÒK/©>¸OŸ>­^‰ÒÒÒ¢Úa7nœ>&±Ê_y/¼õ¡ê":êëP7f^Ò‰mO¼€{fâ’éiú·C5 ô7›°õD:®¸p˜Å­¼Œ'^øÝ3.AÔÅ ¦¾¿yé&_2j‰˜ç@ÔüÿÒ—V9m¥xnë1¤_qâ)t&÷Í{Þ+pÑ@3‘ËþÀß—Þ ïü›°ìa‘é¾i+ö ˜GÊñò ùhdhi³g˜ù@[ï¿„zK¨Á£³ºŸGS豇øöÿp /¿ûzÞ«‰ã¼:Fú¼:FûZ1,ò|ðNL:‡Óù<vŽƹL»?;tncDt^êììÔ‡b×ÕÕ…cÇŽ©vJ/¿ür\}õÕú'#+-íü;ÁŽé,2¼øâ‹ªµãh Uàe°—l9//OÿV0Ydj¤Ì»ù~ÜÿkùT12s¾awÿwJ¯M>ˆ|”|GLó<ýý0\|‹XæwPÇsDùûàÒ‡Gž¸ùÝU L]Þ—V,û޾q¤‚ºûø”>•¾l“üD,{DÍÃ-"¾³,¶5Èå'žØ×ÂohûX½¾؇ß?ñ”Æ× `tå1"ìØÇÌ× SG‰sNä¯Ǿóî 8÷…^‹ÏËsÑ\sÍ5X»v-î½÷^5L#gÌXd×ÊÛ¶mSQ¶ÁD ¨„¾d¨Ûo¿]dBÕÔÔèC4öÔ£YÜ5eæ|¦Ëœ¿¼Œßïu¡ðšÐ  &~ 2]Ø÷F)ô±D#¡áÐ €ç""""ŠÓ˜íEH¶r\^^®¿ÓÈ_¶ê0¹å–[ðòË/ëc"ËÌÌÄ¿øEœ9s[¶lÑÇB•j‘ŤfÏž­Y²HêöÖøFX©YL]þŠ¿Óo7ý¢Vˆå}¿ÒȢ̿Ǿìåý¥Xd‘ë·Í¥,2±àë”PQÓ»LÓèó4ÿô#K‹D,%>­|0©¯Íóšé8ñ¶éW¤ó‘¥äuŸ¨Ë¶M[Þç›ä<2õtÑÒ ÀµW_¶šgAôí ¯,QeôO¥¥ë¼CáËÛgúüêå¾îû¥Ü¼5CJ I~aŸØÖo PBÅX'mmb}¿ž‹ÅwûVÑ<°§A¸hËJ{%Êñhž&ì2Î7–4Þ®B,Xè¾ý?ò‚ó»yœi¯õ¯SÄc?â0æ˜ Â>ÓÒ*d»õåç—Ðu`=¥HÇeèú‡í—ç9`Þ*ÐÎ/Æ^<¯‡¬o(ù+a× #-‡wî5 ´ŒXŽpƒ“¡y@œ‡´yœ˜±™âš¦Ò{ sÆYÊƽÌò'°½/ôyׯSèézîÕ·A›®šoä|lÚQÖûþÏ£!,Í—o‹mŽ’FúkÇù@ù˜ˆhln7ÍgÃù؋ИmƒEX¼^¯þN#K¢„¶Á"']t:¤zr¹\_ ÚoàrZ#@ó¹Ï}N]òó#Þ¶Œˆ†O?Äñžñ˜VO¸‡><ކºãp\y?¾s£,y3݇>ÀÎ=F[]8±çê’gjm˜nJ´é¯ÀÌž="íh;¢þ><îÆø‹ecýFF>à~ï6\£Jû¤ãô[Û£´i†é—\ô± í;‹åBÔ<PwÜ«îÿn”ó™ÑCïï j/DÞÌl=â73ßÃm×ô/+jÛiÓˆt‘A…ïÝvj3¡ëä¨sb¦ÚF-ݪëº1ǘ·ëòÅÚú¨C¾L»k#ÕÙ.ÀE*ýEz7iß¾VÕÑŽ´ì }&çûÉ>|"òXÓtmÃ÷ã0ÒBê<=b1aþíMhëäÖ§ÑÖ·ÇOÇ-zZÌì9„Oö~‚ÓizAyB›‹ØR-¯AË«Ó/ÔÒâè§§õ¼%nT_Ø —¼ñÿúåj›´e¡o>ÚÍ«|¸Ñó„L‡=Û±½¯í“Øýîq¸Æ_Œk¢lsš8)˺›ÆjEM7S{-ƒOrEЗnMÓûóPÚi¼»óƒþt3ݬkûYL’ïOƒp}Ë>.ôï‰eÿåí¿„,{+Ž:My\ŸæqÌ x~XŒË#Ër΃æMyÞØ„õ¦íJ;„­;å9Ö’FZp~WAÈM;Ñ ¬¾}£i›õ¶afˆsJر/õþóåP÷YZj«˜ß)ø úÛs*ÿà/8Þ¸ãû¿sà=üEœÊæ\}%¦w²ž2ýBŽKù@¹I¤qÿþ Ù/Q·ýݾ|)_Ë<°wχøð€éüš×K×°ý=Ò׊áŸ{ÍÒ¦_¢¶UÌJ[Ö¥rÞ,Àv¡b8&Có«.øÜ©ŸNªC¦±¯£,ólæ u/S_‡ãΫôyË´þû>ß3ÍK^o>Øiä“(ç^•/\Èœ!ç­OS§&ÜbÚrÞÆõÇXoy-Žt.+€_¾?å ¾2ƒ"AÕ6´ö9‹ˆ(~Ãiƒåla,cHww·>”xÓ§OÇw¿û],^¼v»];Fˆ¡þ_»ò1oF&àjF¤8eC“üɦsL¿X­½úÍŒy¦_Ɔ߆Gá5¦åÌÃt¹ê-úš‹=ÙîAæÂL¿‰e]SœÚŽ—ãíu{êe¦ù&v»eÞgúvË_oè[VÈ~é´“‰×÷ÿú™¿ì2‘kÄê4 ¥âM>J®—íTTcû+åâDþ"<ß` hh¦ÎéÏræ6„êÔ/£1WÿÒÓ­ðó¯zº¹öáï2Ýb™&f!Ûwñœ tkxçc‘…¸Ìœ§ JpÙT1Íñr™ƒ§ATòWUÓñtñ-ªý¦ê]Zu)cÙËÍ¿jëÓ¸öþ]<ê óüKÞÔÛ¬:ÞŲ¿±ðì·b¢µÃ ÒÅ”¾ùËnBU³aì3ý˜¯®0J\jó(œ*Óì°ÚRyEµHØé˜'ÒuèëYŽ¿ï  ¿aº>ûE[†š§ x†Í³?ßh‚óõ¼Y2W‡îÏ༺Kصb¨çÞX óË19$b;>o:vÃÝ<¡1ç =­Cæ•Ñt1Æ…æ˜w€.d?–,VKGCŒóѶ¡‡MûË| ”sª›fÙúñ”)SôwšiÓ¦©¿C Ȭ\¹ú»±%òÃfä› íÆE<ð>ñ~óÎPoq¥y˜£nþ~'žxyè7€A2‘iÕ[åc¦¸ù3¾£SêËqÂ%òà¬\‘’ƹ[ ºhAßa¬§ŒÌ Ý/²Ñr™õÀPvU:M¤yõ|ˆt5KܵbDÚææ9$–crHÂöeˆ³š't¶7aÛþÁ\üy-ÔøÔòqð/DDD‰7æ,MMMغu«þ.˜lŒvÒ¤I˜3gŽzŸK/½TWÚÚÚÔ¸ŒV;+£F<ð~Gö¶"ï•T0A{ÀJ°Eõr$ñÒÄ´yŒTPÂ…}/Ë0^æºá‰3ºÛ5£—Ñôýb<Féù€Tr#€P½³˜·ÕôÐZ/>‹}=´ûÅ2MâÈ*úvÉv ¦.6zOaºAÓ š.ý ýÁi0Ã>? œ7Õƒd$2 ™¬Ê<¿¡¿b>¯©àHðwƒÚÒÄpö™ñ+½zˆ–èòWò‹e‰ Në­]‚‚CYOõÐ?­ÔÞˆŠ3]‡æì‡w‰ñ˜L¤±'FÝPJ„>ù'BàŽˆˆ(ÁÆL€¥··ÿûßñÜsÏáĉúØ`¥¥¥êïŠ+TößúÖ·Ôû?þ8¬½3ÙîêÕ«‘››«ù,Ñ‹¯«®mµ‡;ù05¤-ê/}êÁB%F"QˆåÆrB^#RugÔ¶k8F9-D«$‰´|%†”Tùƶ-[-œŒ‡U­¤‘ñËuD²ñp%«ÄRBb4KQhiaÚ¦bøALƒ("” èðËG~¶>8¨áœΛª$M$<ði¥}"Ï3ÖW¬Ç…¬2éû±W†ºÏT°P ¦¨jê×wm?É¢*•R5!!ëÙ§¹#œõ»¾ƒ9{çÇáC†rLŽ–‘ÏcQŠOVbõ "ú,±ÙlúÐØv®¬g¢‰‹ ¨¼ð (++ÓÇD&ôùõ¯O>ùD½ß¿?6oÞŒ={ö¨÷™0a‚>ôÙ6ïfù0 ÝÔ‹x°PÕ@ÄË4À|£D¯Z‘øvGBŒÐv ÇYO‹˜DøõóÀËzÕ ñ %Kj{1zûb¿BªâóQÛ5h@é.ÎÑ«£¨z‰3õˤX/YT>–iBÿÅ:¬¤‰^"ªð4ˆ,<ŸjU´su!3õ!R ZqÿÁαäMí4|šÑ-=‰^5PµL6ZúÄo†ÞVˆ&–}¦/»åïjÿÎÒB*Ÿú¯«<ª ÃXÏhÕSúö‹þ€ojóEÓ€r±ìøªrŒTºFvö¯Ã;‡ ÷˜”¢– ÈY͉5b玾ö‘^ÖŽËŦöqˆˆÎaéééc>x!×O®çùhLXd• ÖÖØÊ×vttàý÷ßWÝ2ÿíoCmm­þÉùG+vò«ª~hÜàL¯âTÁ¸ùŠþKv#9Äz÷ZãŸÕo›×W<@¿¡5Â7pc~CÃvÕS¢Ô§×è7¨•°ŽQM‹¨uö}П^ý%U "MßcŒ†õƫߎV"Hö4þð¥=l˜Úïó‘£V¿ZD~_ëz³¯¡E=Ý\{_7¥[ÿz©t‹eš„ÐóFÈUpºÅ˜‰ýajê ž óË1)÷£ V™½Õ§º³›'†Ç¨¶óqÚ&èÜ9¯éãªã2üøUU‰ïgÍDtîq:ÈËËS] Õ—\?¹žç£1ÑMsUUÚÛÛõw‰gtå< ÖMs,Ý7]Ê.&.e×ÓêuÜ%Hï7õ*"¨ëOÙ=©Þ­£ñýÅüe7“æžBhÝ’ŧbzÕýdê)Ó<õ‰Bºù•£ .ìï"²oY² d½›ßÈÂÓ¥¿{Æþ®’ƒÓ-–íÒºqüôÈñYס ¼§ñá§Gq@ÌCvO9­{°e‡o·´ÎÃL ½›fW\gc»ú_Z÷™Áëº\6+Ëè*S¤—ÞUê»ú¼d7§W9èy5 ÿf+Žº3±à«7cž¾‚Š4þp/öêé—”We7­z÷¶ïö¯ãñ6Yô?¸Jê*U,Ïöí}ËïÛ_F÷®ºðtÓ»ð¾y^_º >M”®BMbI7¹œôšw±ó}cµt»eÂ)8Õ ëŒ+Qreli`¦-; .¬Ãö­Ú6Èu‘]Æ~y†>‘ÜgF÷ÁƼõ®foÖwP,燰cYä™Xò¦¶íÁö·ôùèÆœ…ù¨« =Œ´c0m:. =öõt1§]ôc?ö|¦À¯º”weÎÁ5WêǾ8ZeWïNÓ8)–õ é¦ÙèRXvû«­WÈ~‰0O™dwÓËô Œ˜¯ƒºÈÕÇ +]Ãܵ"dý”ØÎ½aô}tT\›?Ô»až>è9$’ÁI©@uyÿvÓÈ.¯tà@_º ~~2œÍ<é^F›WWýŠ~Ýrêó’ëtÞ”]A}:šú¦‰²ý!ëºÞ‘ÎeŠq\N½ 7†¤§,õÂ[‡¢t5NDD4<–€ Ÿ5‡Â_þòý]bÉöW.¿ürõ—ˆh¬“%O~¿7sðz""˜¬2õÂ>dôÃQ‰*B²t‰ÑÝr"ÝvÛmª1\WˆˆˆˆÎ/ZÓ#PµŒˆˆ(Š1Ó‹ÐÂ… õ¡Äõ¾>›½Q4F×ðª4àUž‰ˆˆmLT2lÛ¶ ‡ÖßÅçŽ;î`€…ˆˆˆˆˆˆˆFŘ °ÔÕÕ©n—Z¥éÓ§cüøñú»p7nœj½˜ˆˆˆˆˆˆˆh4Œ©‹ôî»ïbÏž=ú»~&LPÕˆæÌ™£!"""""""Æ\€ÅåráÅ_Dww·zŸ••…K.¹$ám´%ʘ °HÕÕÕØ»w¯ªê#+ÉÉÉú'DDDDDDDDcϘ °KÆL7ÍDDDDDDDDç*XˆˆˆˆˆˆˆˆâÄ Qœ`!"""""""Š,DDDDDDDDqb€…ˆˆˆˆˆˆˆ(N °ʼn"""""""¢81ÀBDDDDDDD'XˆˆˆˆˆˆˆˆâÄ Qœ,A&""""""¢1ª¼¼Û·oǸqãpË-· 99Yÿ$˜1ÝòåË1oÞ<}lâœ:u ¯¿þ:<zïp8pà 7`êÔ©ê½ÁXÃ%—\‚/|á ú;Í«¯¾Š“'Oêï€iӦᦛnÒßizzzðòË/£±±Q½OMMÅ­·ÞЬ¬,õ~¬°­ôa"""""""£püøqÐ8}ú4.¼ðBý“~æ ÆÌ™3QPP †EÎÿÏþ3,X€Õ«WãŠ+®@ww7JKK‘žžÞ·<9ݻヒ•+WâÚk¯Åĉñþûï«ieEM6oÞŒ¶¶6|ó›ßIJeËÔö|üñÇøôÓO1{ölØíö¾àJZZ¾ýío«åUUU©éŠŠŠ¢™ÎV""""""":‡Lž<­­­*8ª¢¢B0FŠœ¿,Asùå—ëc †å8ù™$×kÇŽ˜4iR_©ùW–¦9räˆú¼¾¾^ý]ºti_IùW¾7>—>úè#õ¾¸¸X½—JJJÔß½{÷ª¿c,DDDDDDDçÜÜ\UŠ¥ººZ£‘iÊ”)ꯙ¬ÖóôÓOã‰'žè{É&YâdãÆxã7ú>—ãBÉê;²ÄI¤’#ªÄ‰\Y}hÖ¬Yú'šéÓ§«ñr½eÀåž{î‰X…INÓÞÞ®†[ZZTàÅ\G¾ÏÏÏWó‘Ë+`!"""""":‡8NUeÆ(1b‡œœõ¹™ÑfŠlÿäþûïW/Ù>Ëž={‚‚(]]]ªdÌ?üÃ?¨ibm¿Åív«ïÊu’#8’‘‘¡þ†jjjÒ‡ÂÉÏdðH~WOdÐÆ˜o(¹L¹ì±"áÜÅxˆˆˆˆˆˆÎ‰nã‚( 1¯•d5sc¯²ôÉÅ_¬æFneC²2Pan×ܶ‰,•bž÷PÆ•ó?sæL_C·r^²tLh÷²dËK/½„ .¸ ¬±[ÉɪErB×ÑLÎ_V7Kݲ!""""""¢s€9RXX¨‚²Í‘Á‰?ü7Þx#*++£KŒyŒ^{¢E#¿#K˜{N€ÅøL2‚&çZ€…U„ˆˆˆˆˆˆˆÎ1F;$F5¡'N`üøñ«Ò˜Û_‘Á ‘Õ€dôñˆ\ˆÑ6K¨HÁ•ÁȶYÆXˆˆˆˆˆˆˆÎA²YÙu³lª£®®N5"J–yï½÷TÐÂh[%–`È`dµ hÁ£í£-–PyyyúüyþùçÕphpE‹dé£ñÜP©©©aíÍœM °ƒd5!iß¾}êo¤ö€B 5Ý$‡ ®œ+ÖØ’á·ø`±Àšœ¯?€^O'¬–¬6§øÌ©[¬ébš$X`Gâ;H‡Í– ‡=MŸ3ÑÈ–@Ïqø]ŸîSXmðÙÒàƒW ûÅ_(I‚ÅâcRÐÐØƒš>Díëxä¾"¦žˆ›ÿߟ↠úøQ·Oßý´øXtï3¸§X;»ÿãn<½K£pÛ æOà‘Wk€ 7ã§ÿï "‰ˆˆˆèlixç7øý^0u9î¿yž>6œ6°àëßAI>2!ÊñòÛQ¹ßøN Ô]ÿ—ñÄÛÕêSM!–ß B×®ü•'°ý”þFó žyæl–^‰G ^R–À‘áµ-Úë§7åIvãéÿ`ˆ…ˆˆˆè|1}F!\ÇËbi@ùq pj¦þ>Ñ €lñ\ –¥•¦¹åbíSiÞÍË!Ö'ékwàeUr¥p±^âEÈ_vdº°ïƒr} ѹçt7°¿ (‡¦Ñ7ú–€¾æ¿Â×þ >/`O‡?©~{>ü–løéð2Ô«æ´gŽ×Ým˜91 Ó'CNv.ìŽTøübt—MͨªªB»« >¯XÄVÙau¦ +/³fMÇåÅ`Îô\œØ¿ùÓ«ðôöê+sè¥?î¾û¼^«’Umî–ãdP¢ìimX=ò'-”!Éê1jüÿ|]phBÇ÷ÍÏT’¤oÓr#éÿ®ñ ™¾ìU ªŠÓ=X¤Fj&~姸g±X|žùžù“ó ÙCÿz/–†!"""ó.šƒB× ”‡–©/ljìËðùý½‰*mòĦ—¹IJó~óN)^6>ÿMi„N>ò³ÌY·'%ß¹?jU%W‹6ó†&Yš¦sLAùÝy32S‡Á ‹d@¥I<æÊ†4|â?9Ì ËèÝ‹¯þ†·p…5à…Åj‡%%Ö$\IF¯ðøp{ìh¨í@ÍÑcȱu#/Ý 11¼~ º;»ÑÜXÚ3§púdΜ:Žê•hi<.W=z:›àótC5-cs )%9¹ã1ÿâi˜Y8'ñöë[ájkÓWj Ùõ4î~*8œ «Ü<]¦¿‰Ñ¢ï½$IíëxZ¶["L¼éž¨%g"µ©"Ö¯üÏþ Çîô¡ W`Q„ù,úÞ3!ÁÙÖK„ùÖ¾‚G‚‚'² ›»µöU‚Èï""""¢³M–$1•Ñ5:ÌYá­ÚP&–ëUt´ª<.ì{áå ‡kï>À¨îc´±bÞÍ÷ã;Ëhs±¾2¤¢aÄÛñ.3W¬q$.4°šƒZ=ú€I¤q4²F-ÀðuÀ[ÿ=µ°ØœXí@R6ŽLdðD¬ŠÏ–Ž^_ Ú;P¹ÿc̘œ¯×‹ý‡Ž¡ª¦Í­í8]]…ƒûö ±®ÉiI˜:}*\²)ÉIèêhƒ·«Þøzºáïõ¨îœm)p¦fcráDÌŸ? 5'ŽbçÛo ½]žjÇÙà¬V妿tH_P#f‹pϽú·eÐF5ª+L¸÷Dm€¶5z[`²¡ÚàuØWL%i‘–±40[ó§Wô Š,ñºm¦ªD}%cÌiðSܬ‚85xåÉÈ%^ˆˆˆˆh,ÐJ€W’Uv2CJŠHåøû^2~>¨áÙyŸ[€LTãð}„”¹ŸûþP4 ô}p¡— „òóFªÑÈ“¥VBEG#kt,~¼µoÀﮇ%ÉorI9€Mv½l…ÏŸ ‹5€Íõ­ØóÁ{˜=>GUàôé,¾t22œhik„3Õ‰‹çÏAÑŒ °ø½Ø_~ûD£Ë Kr” Ï Ÿ·S,¶ŸVk2Žøl6ŒŸ0))IbÜ(ûàoðxÎbu¡PnÆÍ}½ù,²ºÍpߣU×é³÷ ØëÎDL,Ô†´†jeõ¤‰}m¬ ¯g ìþ0RÉ™E¸Ùh¯eׇ*ÓDZ,Ö»/ &â†[ô@Q­˜Ž¥XˆˆˆˆÆ¬ü‹¦#Ó\MèÀß±/{NXï=cn¹¿¿Ô‰lpVUzABB¨¶U†¯ü•ßcŸ˜iá5á½%Ú¨X|õoÂ⩇UVÆÃáHQÁ·Í‰^köqðÒÐÓéEë™*ä¥zañvcñ¼I¸`ödìúä0*TÁêî‚§½‡ÃCsG®üµ¸ìò+pá…3`u$ÁÝÛ ¿ø‹›¿–ÞNX$9ìð÷xàH²Ââ³ àéÄ{Û¦¯åc©X-ºÜ(#,¾B/5Ý¢ï%F4F E¾Âª)U×ÄP¢¤§ô ÈÄÉÁ[üÞTz&d:LœšÐ4!"""¢R0ÓMÕ„Ê+ªQ¡zdneû)ÙEòý¸ÿë²KâÝ0Ë…ÌÞF£µÍBD4|#`ñ·ì„Õs–Ô,À™ ‹s|¶<ôÒ`Kš ›-‹ ÍuMxkë6ÆÉ“](ÿ´ï~| 'Ï´"àõÃïëD§îžX¿ûw„Ʀð6ÉÇ®ÔÁÚ×ñJXô2Ða4 »‹ôªB»Ÿ¨G­1Z­jùÊOûƒ,FP§øæþvQLßJ² Õ˜­lÀVµ­b®vô´©¡ÚÝxÅhÌV/YÓWr%(Tƒ×_Ö—¥Q]"""";úª ½sÕS#U꛵Fgó‚+ÿÈqã/C"ƒ+²ZV*&RÉ­­•ÐÆl.ž£¬3Q F4ÀâëØŸ3Kµ·°§~/¬üŸøk…Õ"[ÝñÀã÷aÞ%—`AñBÌ›W„%—ÎÄ¥óóh?Œ¿¾ùtuöˆé¬°Xìâ;ÀÛƒö¦:Ùû1êíA ã$>ÿ¹¹¸jé…Xzé…Xvù\8;5 g°ííÑÙ-6Óæœi8~ªö¤dlø!ÖÅçGFªG&®‹¹jMÐËhÐu˜&.¾B¯.#ƒú<lÍÊžîëµgѽ÷às¯BQºG–£]óú½úL¼éf}qÃ}7ëëÑ”1O+çuÞ“PF¦uîë=¨º‰_¹§¯zÒî§Œééïú¾Ú!"""¢1A¯&´ooôêA(ÈUUª+Lýé¥Z⥵¹"ƒ+”йøóX ×ÑÔcQÃ;¯kßûÃ+D4|#` tWÁïsÁjOSÈn“>ñ§_/,¾.X^øý²Í¼¾ôô¸••ŒÔ4;ÒR“pÑœ©X|Ñxø½­ðx\ðzÝâÕ-^âû~òÒí˜7sΟ‡ìì d¥¥!'']|? Çgãêe‹pÝŠ+‘‘™-–‘ŒcÕ-ÈË+€ßâ…ÕØâ?±^›MÕÕ¨ª:©¯ý5áüÔèȰøž ª<*èaT êk0ÖÔ«Pí+x:´G ,±òLèüÙ«PP#·r=žy&¤]X¦¹$µìHÓ†M§UO ÞaÂÍbY?Úµ4%ùÈÏ–£T’ JðÙÞÊ©í}í°<ñB3.»¹øVHàe(êKññ)9 ƒ'ú|ͯߔB+³ž’ïÈn¡«±]ÿì÷{1pP†è<ÓÒÒ‚¯}íkøÂ¾€ŠŠ }ltr9íK/½¤‰n°yËyõ½b™çXa úpBy›þ*þï†6ñW¼,6øegÌr0`©ð[3àñ;áêIFÙÎh9S‰KLAnn6:\í𶻕é€ÇæD’Ó†$‡Xàóxà÷zUÀÅnMBröD4wv¢×í‡Ã‘„t§Uü À"«ù|ª'¡žž^¼±}fÎ(B» V1Y€& VÈf³£§Û KF®¿i•Z""""""¢sÁ¾6} Ä‚,}`º»»ñãÿ[·nÅäÉ“ñë_ÿ³fÍÒ?LA~øa<ÿüó¸âŠ+ô±á›÷ÿù?ÿÿñÿ¡¿ë7Ø|ÇŠ)Áp×Ââ«GÀç†ßï>± ^ñ· ¯ £ø¬›ö¡±® ž· xTW7 ¶®‡+káêMÁñz\@sK'</,+ìÎT$eä %g¼ö,¬<†Ö^´´ºQÛÐ…ýŸžÆÁ£µhjî„ÏãF ௺»<Ú úèõzà øÅ:YÔgIÉvôv¹àjoÕ¦!"""""":È%+V¬P¡8~ü¸ ˜äååéc 6oùùŸþô'5ŸmÛ¶¡²²ßûÞ÷Ôg/¼ð‚ ÎŒu#`ñwC · ¸U•Ÿ¿¿,ÑÒˆ÷ðyTðÅíµáä‰SèhoFm}-Nž:ƒ#!ÉÀì…KaM/DC‹ ž€¾€«6»øk³ÀÝÝ…S•ÇÐT߀iÓ.ÄÅ/Äì æ 3g"ö–Ÿ@ÅñÓb e[/6ØÉâ{ˆ¯Áïó¡±¥]]p5× ­­=ž$;øôÐ!}+ˆˆˆˆˆˆˆÎ~ø¡ €œ>}ZU߉• |ÔÔÔàÒK/UÁ‘Hb™wYY™úÜ<ŸýèG*Ðòøã#%%EËF¦‹k|žVXX}½ðûzà—‘ ‹ V¿ pøÅg^x}46¸aóÙÐéjA}} ¼ž.,]<©6·ª 4£hòó³™–Š$G l6?ü^üît·Ö"=É»ÝO »Ã‚‹.œ‚KçÏÀ„qbm,ð 9Å «Õ¯p÷zPwæ8|î&´4žÆ‰cbØëAùÞAû2&""""""ú̹ä’K°k×.ü·ÿößô1ƒ“A‘O>ù'N02ؼe))##CU#:Û`Ix€%Ð] ‹¿V«ð‹~,‹ªÚc‘í°øÄ"ýVø²E–|òÑdgd#/7 Y)H±Î1Mvvð7ì~÷-XºÛpààtºUÍðùð{=HJNÂÅ‹¡¨ª®Æ‰SgP¾Úë`öx+&eYàéi‡Çß«ºz¶[eõ"lII°ù:QãDGw·ø\¬§E,79ã³ÒÐÚÖ¢¶…ˆˆˆˆˆˆè| Û8ùÃþ€œœ}LlšššTeÙ²eú˜pƒÍÛ(#=÷ÜsAÕˆdÛ.çJ%áO-z{¼xzU›*V{ ¬ÖTÀ#§í†?à†ÝgÓqüD· l8~twtÃê·câÄB$'¥ãÂé±ô²É¸|álx6Ìœ1Ùé™°°[¬pÀ/^>¤e¤`ÙU#ÇÙ…LkæLJE2Üh­;ƒ¦šz1û$1}vxás÷À™š¿øþ¸œTt÷t!#%¹ééÈNKƒÝ#V³Ó•Çô­!""""""¢hÞyçAÛ_Šhm°ÈއƺÄX:OÀèA §î&øýÀž«µðt>7¼6+ì)Exùå7qÁŒ)(ÈKÃÄ y˜8iÂÁCÈH+kõÁëëVçÂfGÀbCo—;vîÇ®}GÅb¬HÉLCWWÊ+Oã´«ÎÔÕnKO¯¾€iÉ)b£}¨¨¨Esk7Æå¦!-Å «ÝŸ%€º’ Ö•ˆˆˆˆˆˆˆ¢Š¥ý•XȪE²Š‘dž—Q*¦¡¡A•”ë` ´ž„¯Kl¸¯V/,Þø{jàs7Àïí†ÏŸ„o6¶¿}ÝÍHIv¢¥Í‡£½« S§LDowj*öà¼4¤dÀí hÕ‚z\èßéhoÇé3¨mtÁšš„KÎÄÕK/GåñF|ðÑaü}ï)L,ºXŒ¿Döà _°|°ÚXHKÏ€ÛïCrJ2’1¶¤t¸Å4>‹Þ¯¾5DDDDDDDI¬í¯œ/`ñ¹NÃæ“CNød@Ã׸eƒ·b¤-Ë8´w¥¡¹¥K/[‚>øþðê/q¬ú}$§y‘d·¨k‹.š…qb'½òúûøÂÒEHOM¿·/ìV¯˜_/:,þZà´Ćt`öô<ÌŸ7Ÿ_²…“òaqXá…_5Љµ²Zm—“…Üì<Ìž:>ñÁ'GàÓS•p÷¸Q_sï~ô¹DDDDDDDE,í¯Äʘ‡leß¾}jXV?’â-!3Z_‚=ªÇ‹Í‹ì¢¹«>W <Ípw5£¹¶}²íz ÇþŠ–¶2¤§µÃéÓúzðù‘ìLEFz6¬v&ŽËÆž²½¨­:†¦ê#8SQ‰}»öàè¡r4Ôרt=^|ÞN¤g¦#%ʼn´Ôd$;l°ø}b~^U¥H¾ìâŸ^1]òò ÐèòãïŽàpãi¬>ˆ·?ü»X¦§ê[CDDDDDDD‘œ:u*aí¯È†p6W¾ùÍoª„þã?þC½ÿú׿~~vÓl³záôÈÆXàëiƒ»« Mn”mÅ'å'Pyú(\ÅNèAÀV‹´¬^ää:‘™é@KK=ÎÔÖÀïuÃðÃãéÆò«æcòø\±¢²‘\7þnäd§"73Íœ¢ÆË°XÄg>Xü~X<Õ¨­˜zº{±ÿà Ì™3^1ÞnKÂì9spª©•õMèðt¡­§• 8R{uÍÍúÖQ(ÙþÊ|€üüü„5pû£ý¨/ÈbxþùçUðå\` úpBôì¼>k ;úïïý;:¼mè È. Å÷äÇÆÇ^Ö6ˆˆˆˆˆˆˆh Ûצ„X¥ШHx –fwN4Ú±ÿt/7Pëvé)È›œŽ¬qÉHNIÕž„®.7ì6v;Ž$Øí™ÈËî;N×wâäéV8S³áñú`q8aMr %7Yã§#-·Iã`OÍ„lŽÖb±Šï;aÓYlIèñpüL3ŽUµ¢êtjjÛ1}V6ŸjgÅåjESS£X­4d&gÁ;œIâåLBR²™yÚÆÅ á–56nò!{Æd̸h2&NÍ…3- þ€¬Æã@¯Ovµ€ÛçAg‡ >¿Ý$Ù&á’yKQ8i:\Ýü6ø`ƒ×ëFo§o±º~ùòûá÷öŠqnøz;a xÅ´1YEÈ"ÞNg R²'Á–593áËõy^ ‡Ã€×O·¾žL)˜€ù³‹èu";:º=HMÎÖ6†ˆˆˆˆˆˆˆ( °dg ×ã‡Ýž„ÔÔôúüèîîBÀí<>8í¤ÚÓTµYE¨òh;^r5fÌœ-¦Oƒ5`ÇÉ£•(š^8“ÑmËEê´/ལxåÍR?ò)<îø,²DŒV¿>ŸìŽÙ*þÙá—íµøº‘™•„äd+RÄ<’R’‘žžª‚2½½Ý°:IJíɰXí¨mªÅéújÀfS½ùLš –MDDDDDDD£„XÒÓçÀ™=δLø|nx=b)8RRÅ+ vG:,Ixvt¶[pòTêêêáõ{TÃ'ÖÈžj6¸zSá,˜üŒ¾pù\\0{>êÏ4¢áÔat¹jaÓ^Xí6ø­>±ôüînÄ Ï´´µ¡»K¼ïö 9) î€~›IÉN´´6á}/üå|zæz|²/"8“€é§«m!"""""""ŠEÂ,ùSdÏ‚6i°X’ÅXz|½èõvÀíî@oO¬–.¤eô pâD̘>©ÉN1. Ú@éî  ¡© >K:¬ÉVX¼-HF¦80kzìþ^ô´ÕÃ'æis8`±ZŸÜ].´5U£½Ã…Æ& NWÖÂÕØ À›Ý ·×”4ÙÓO’gS1~|¼¾^X,~tvwÙä„1] ’RÒáFN5t¡¦¡gê\8Sß…ú+ŽîÆ‘ÊtxS1aòT¤§¥Án·Áb›g±¡§»'OU ²j/²ó,¨méBOÀ[’\‡xÙÕzÏ™6³§]¨†‰ˆˆˆˆˆˆˆbe úpBµ´ŞݿAKC7ÚÛd*6Õ»3 HËÊD»ë]HvØQWß ørpáìŘ0a:Rœ)p{}˜~A1’œ~+߃¬ì\´65ãÌÉ#ho®Õb…Õž gJ&¬¶$øýŒŸ4©É²1[Ù³í]ípu´¡±é êZ*aÏðÀë±ãЙvø6Ømv±^Vxý>1À×>‚Å/Õ·€ˆˆˆˆˆˆhìÛצ„X ž¿cõî»ïbÏž=˜6mnºé&}l89]yy9n¸áL:U›8rÞÛ·o×ß©©©¸õÖ[‘•¼1¯¾ú*Nž<©†GØúôôôàå—_Fcc£>¸ä’Kð…/|A§ ]^¤ibe[/èà •’’‡S§ŽàÌ©:dde¢±¡)¶TW7bâ¤q¨©=ƒÔ4º;½ðz††FñÙ¤Øíâ;~dæM€3Ù‰vWlìœIIÈÊÉEö¸ ÈÉÏGzZŠXV2³ÒU‰léñºÑÞÙŠê3Çp¢¦IíÈ›˜Œ€Õ‰=µðX¬Hó‘ù½±¬ν_¿áÛúÚêÜú@ˆñ²SßÉ`ESSº»»ÑÙÙ©-¡Œ Œlöã‚ . zÄKÎÇŽX¾|9n¼ñF\qŨªªÂ|€ñãÇ÷-OWä:ÞqǸòÊ+Õ:¿ÿþû}Ó´µµá…^Pcš‰'ªiΜ9ƒ /Ôj®ÈàŠ\æÊ•+qíµ×öM#çiû3"U„ ]t=:º½èêè@o§[AŽœ8…¬´ Tè+m…§'€qy)˜:ÍiE8ÒN`ç¾?áoïþ5u'ÑÕ%¾— |bX̧Wkƒ%ðÁïõÂaO‚U6Të%Z,ªêQ{O'>9ð!š»‹y¦!9U|æp¢ª¡. ~¿]õ2äöøÐëó ƒ+DDDDDDtÞ›1cª««U 3ù¾®®N!F‚œ¿\® lÌ›7O ”””¨@ɉ'Ô{‘Á … "9Y‹ ]~ùå*°RVV¦ÞËùx<\uÕU}ÓÈÒ-r¾ *#_2˜3iÒ¤¾’/Æ4GŽQŸÕˆX ÆOÃWV} —-€5CöäÅE3fÁ N©Ã”ÜÙ8Uí é‡Õ€§«Yéi˜}Ñ ΰàÕ­ÿ]­µhilÛݯ߿Å&Ê‚^·NG2¬V;ìV±Þ€šSc3>ÚõW¤d»0n‚ à¸à´ÛÐ-–Q×äAVj2²ÓSá°Yð‰åZ,øÆW¾…¢©³õµ&"""""":?ÉŠ NÔ××ëc4ò}ZZš*%J–yâ‰'ú^O?ý4N:¥ª}þüóÏ㥗^Šø¹$!ßüæ7£VOjiiQe)Ym¨°°P½—äwå{#x"ƒ$÷ÜsOÄ*LrÛŒ‹ž5k–þ‰fúôéj¼ Ò ÕˆX¤¢iK’”…Ü‚T\´x Ò'XÐÔÙŒ«/»ím=˜7k<î\œªîDGí]m¨;sÍÍUÈÎðaÿ¾?"Ùê…Õçƒ¯× ‹¿ðºÑÛãVUüV?<½h9S׊OÊÿ‚IS}ÈJ êXÊÊN¡lÏ1ìÚsÉ©Ib|Šê’Ù™æDVV ¾xÕÕøÚu_××–ˆˆˆˆˆˆèü%›Ó0—1È÷Ѫ Éßþö·qÿý÷«—,òÖ[o•‘m¡ÈyËÏ£?"1!999ê½ ´È‹ÓéTïÍŒàI4ò»rÛdi—ööv5.##Cý %9C5âiáü[‘“™'†Üp8{qÑÜq¨ëªASc 2“i“§bÚÄ…èmˇÅ]ˆÉÅâu kí8uæ6ÿñ?árõÀnuÀ&^Ø ›æííö‰¿v$%Û»í{)™èu»ÐtÚ‡9óp÷«qãËQ\¸ÖÖ$´6¸àërÃßÓ…b¹ßýÚ÷µ•$"""""":ÏÉÀ…, b®&$ÿÊà„¹Ôˆ$ƒ2¸ÚKqqqX)Øã‡B.÷½÷ÞSß•U‚’—'cÑU‹k;F~&—7£`qØ“qÑ…«pd_-ŽînÂɣ혞›‡[¯_‹Û@·]í±¹houÃêµ Õ’„ ›]®.Øí.ì*{å‡ÞÁ±ã»q¦á(Z+qºöŽýX¼öcÇoÞvÔlÅ‘ ¨:ÕÛ‡¶†¤|X4%÷Þ|%þûª›qÝÜ…¸vîRÜÿ­ÿ‰”ä4}-‰ˆˆˆˆˆˆHV“‘ÈÕ„*++ÕßÐÀ„|¿víÚ¾^wdã³² Ð+¯¼¢,fFÉ‘¡Ø¶m›*ù²téÒ!×LVG’%mƧÚk)£`‘ÒÓ ðõ;þ×-_‰«.ý<¦OœŽæ–&tv7Á‹L-lÃ¥ó:qÃ3P<·[¼<¸yÅD|yÙ…Xù¥ °ìsY¸|¡—Ì `Þì̽ ÌjÇœé.ÌžêÂâ³ë®š„WMÊÏOÁM_œ€ Šºp6âtËiÔ6Ôâôñ*äX»±dq1VßñOÈÉ)Ð׎ˆˆˆˆˆˆˆ¤‚‚ÕÞŠQM¨¢¢"¬­ƒ¹ýÙCÏÍ7߬^Ã-b0ºa–= ™½&Z•\yýõ×U€æ–[nékô6£JÒpŒZ€EJNËAÑ¥_ÁÄɲUâFì?tVG2RÒraµ¥Ã‘”%6$uõÝ8UUî.ÒÝÈIw#3¥ iÎ6$Û›‘.†³Òº›Ñ‹ìô¤¥ºŸÓ‹ü\+&80}Rºx%cbyã˜:Ý¡¼=^söô (\ø$¥dêkEDDDDDDD£ÑXYÅG–b‘¥YB«I2x!«Þ\rÉ%Cn[%Y-H6ˆ-¸"Ûbéêê‚ÛÞ7uh)¹n²4M¤àŠÑöŠÑK¨ÁªE2ªÉbs §èJL™ûyLÊÏCjRr’r‘Ÿ>)ŽBØ-“añO –‡ý‡zqà@7ê›ýðz@ €€ß¯^²«EþñÂðÁëóÂ&{²Ù`³%Á&{²‹¿V’ìp8½X¸l5f^y'löðÆpˆˆˆˆˆˆˆHcTÚ·oŸ*Í©ŠŽ NÈÒ¡ÁYòe8¥@dpåå—_V¥Hd)˜H%Wä²ä¼ÍÙÊïÉ`P~~~ßzÊàÊöíÛUü²w¢Ð’+r: -ù"×]ŽP̨X EÅWã+ß^‡ñ“f Ùׅܤ^d;Ý(H`Ö¤<Ì›=[$Ú8tuNÆž½Íð’áp:asØT˾@ ->ÔÔÙPß–‚æVÚ;’áê°¡£Û‰®Þ t¸³aK[ŠËþ˾t»¾ô‘ЀÍw¡è®Íb(’Á>MeØPT„5[ô5ÙµEEk°¹V{“ÚÍXcžGèû1AÛÎ"õZƒM›Öˆ¿Äر(dŸÄb8ûMhØ"Óaû{Lä]ý8ztöbè6#}Ë-†]ú›1!†óN,Çî0óڨѷal¥ýùlç³Aృa7c"·Ë<ß‘H»hB—=’iœ¸íÒöEb¯å#1Ï0#rÍü2²B¯Ÿ£²OˆhPF5¡ƒF­$KÈ`„¬Bd0Jµ ‡lsENn¸á†¨%aäó Ž˜{)úè£Ô°Ñˆ®ÑæŠ ®DëöYÎC6x»gÏž¾õ5Ö}°†p£9kÉ™1—y-Š®¼ þät¸:àjuÁÕÖŽÖæôötÁní„Óâ…·§>Ÿ~K°Yà°'ÁëMÁŽÛpüt6šºfƒÙhkŸˆSÕvÔ5¤Ãâ\ŒÙ‹~€‹?2ó»“¢š°›^\«¿¶ü±[*+QY¹ kÖl×ahíV–b¬ë¹iU¾þ~l)ûÍ:”êÃgW>VÿZìχ¿öqñ:•oVOÐßFÜ<ÿò}˜è¼6¶ÏgcÊ®X÷Ž>¬ŒbÚ…-ûü•¿jäïÆÎut âõ“hÌ’%>d€Ev‰­4‡ ‚È`ˆl{Åh‡E>¾ò•¯¨ï™/ƒ‘Á 9Y:EVë1æg¼dµ!YRE®—,‘"K«ü×ý—úLEÌA™²²25YÍ(t>Æô’lœWVC’%]äx¹\YjÆh´w¨Îj€Å?mf/û —| ¾Ü¹¨lèÆáê&d¤Áksᢋ3‘œj…Åêkì€Ånðtû1iÜ \sÙtÔ?ƒé¸dáøÒ ßDÉM?ÀÅW®B΄úRh¤•ýu#fM;7ÔÕâVfY†^°‹ˆˆˆˆˆèü"ƒ ¡m¨ÈÒ²— siŽÐéä_ù^¶Á"_rú)S¦¨¿FéùÐù„ Oè+´šœ·ñY¤õ67ôe®z$‡ÍŸ 7¸"‰‹!ob-û:nºëg¸ñ[`áÒÛ°ðÊ;ÑØ¶î@.,bm­ð?ü;`KA~ÁxØ’òqõoÀ¸9×bܼ[‘Y¸ŽÔqú\Ç2½8nQÿË\ðqdNÛд0ç…°m «¾œ¾õÓ^©ý:¹ñ61~¨ÅÀÛ×}ërl‡סŸGÚÛq¨Yϰå”'ôô‹6ÚwLy'ä¼szž|ýb›wH:†ó±ïb9®ÃÎ%eò}ø²U:ömöù†-¦yõ}í3ö•yšmø8Öæ¯¶Kß–°m4ÖÁ4ß°óPÈ2û…WcZnT¡Ûy9¡çŠ˜Î›Aó1ÖEßwÆ4¦tRß¿m£*ź+ï‡lðýÐü,ò²ûohþ†{Œ¢:$oE¸f gÞ|GÛ–ðåhûOÛ6mXŸ&t^æ×@ëö=#Ý¢\ctaÇRÔuíŸfÀ<Þ·ÒÖ,Î4Ó„O!yE-c6›Ö?øú,Èiº~†¬gÔsIß+B~%"MJúÀ‹ß˜ùÝÅP$¡ŸïüËÌé?þ—ÀL1î_>Žò^Ѿw×K‘—ñóšw ™F-ç®À‹5ÚÛ˜„Í3XýKw©u7oßî â½7ó_Äè"nc0õ½ t,­5¡ßÓÖÉX¶> m0Æõ¯_ðw4Úv„NiYæí ßßÚ4Fº‡ì=}ƒÒ%bþh¿…o1ó÷bÙÆð}iÜàû%t¾}ßÙÐ?F›Æ´]Ay-túðt ÏSú4‘Æ™¾¶=AéiÛbØg±ˆy_Ëmß—¡ËOËÐõ”¾QDZ~„ï –'‚ó¼!8ÃÁóUß¶}OßǦôˆiÞÒ1l½cÙ‘èóŽe_÷ÏG_G¹ˆ¾mƒ|_–?Có‚ÁœÞQ¦QËSó2m¿¾žaû&LbÒ,r¾ ÉCú:…mc¨Û©¥©9JÁëÓ:G"Wä‹Fטª"ô™°lvªFUC_;±a™> ð·7dû +puHšÅ_” ÅVà¨*ÞXŒµ?/žy³¯˜¦lç$Ò÷ A µ™Š™«¢©£nfÇÚ@h4²ñ\‘~ëTÕ#S±uU´9.˜è­Æ76ØWŒW/ÊlàmÜŒÖ`k¤Æ ‹yæ×«µuë+»dH 6œ J­Åuzµ-Cþts àCÙF“aí—|\}½ÌϫĴQŠðVWŠå– (¤á•¯ôèëq¤)™Ok<Ÿîn ô¡%bÝ)v=D±ïëðté×€£GÄŸ»¯ÓŽý>ù˜}>·(Ë?rT,]ˆ)OèéøÆß´ïAç´]oBN½ö‹!MMN¸+Äù³´¢ZYXP‹¯G@)¶½'–Ó¼Ë𦧩zø}ilÌOÿ0ªØnâcš· O·ä¡CŽýŒ€S$ƒíkýÁ'œùa9Ú¾B0mí,¯v‡ ¬©‡ôЀŠy^Æk•˜.šÀˆó 5øõ1–óæPÎSá ¿Çø5౟Èàj¸á£ƒPÁ„!Šñ;Á÷Rzp>ìÁ<” zˆ<KÐBK´cY Njé2X°e°sf¬y­ô™Z:ˆëáÃx™fñœ+e,¬‘ ,g“~sþ2u «ÿ¢ ~íŠá²ìQyCª_¤åkº³U»6¨_×¾h¤Í(v³,†ií!(d¿Œ(qS÷cqóo* 5œ.;£>X›o®†»ñìýW;í{[Dn•Vq4JËq”(ªëf}þêS>€ÆdI̾è¡-êc¢Åš'ÂJ•¬Å÷ƒ¶Yòél~ x>ëŒFÛ¼û‡ÐW<çŸöuPI3óCt´}{ 7¦í,¯ñ86ÖT©¥»¿r|š®[!¯hÇ„ùp°ÒR,×ÇXΛÁ¥^ú&z0txâÏC7Üct!Áÿ˜Äúó½Tíß°í± ßx]Ë•A1Ý¿„–žŒF+•j¤… ÜÉ`KÔ KïoŒcZ[æÃ_O˜f#s®š±°DDf °œáE/ A-Ù+ýÓnˆxSj¦ßP…UEÑ‹Qžƒ´_o™V7Ó#M¿¡ýR+&;Ñ‹«ª_¯Âz%ÐoÀC‹i«}8ÚC€ñëS¿ _ù‰Û/ÅX§~­Ó޵¦ʪ:EèÃÛPŽ£ 0•Uø†P &\böµL õkuXZŒäCV°Øó„VõQ–fجÎi¦_Dê’´’ƒýö0kJÇ4ïð*L†ÈÇêPIJ¯µ‡å°å•l‰¶¯cypæö š×ƒãˆT•™þ›ÕõÈ\Å$ø—r½ÄOX¯#†¾À Ú<ïèA8¶ëcLçMõ€!/©ü|> «Ö¢ï˰*6ÃO~†8ÑE«>¥#«Åü|¬¾WäQ‘N›åõm`œ¼†hÁáê‹–Çy@Ë9sh×Dc™¥X÷›ô0KHšä¹2VcaˆˆÂ1Àr–ä¯ú¾¸nÄ*ó ¯4ÁZqÙègL»1ä¦4œ~£ÕWü^£ýjwnÒnvCnæô_}Fœþ+OÐMˆ¾†*âþ6~½ {(Б ºaÖýÖßÅdñ:õkÖÆÛL†ÐõqÕ¯º¦Ñðö‹,n~Ó£ÝLêõºÅƒÜ÷Õ:›oÅ»¬ò!¸8”ãh0¡ÛL¯ZÔ•iH0úMkÄö"¢Jо´k‘¦ulØòpüÅöc4”<¡ÞY‡uaç4=øòÐ’nLµ’Á%]Âýr+òj“ /ßÄ6ïâïlPë¶Ä¼¯£«C˾Öh‚Šù÷Oc<ø¨u Ù×}çùÛ•ŠeûËë1ÇiÑÒ‡Ö©ôš.âq¯o÷²þîÿÀâú8ಥ¯±œ7õõ .) Ÿ§B×÷™U¦ü&¦‘U“ÌÓèÁšÁK_E7ìü0¬eÇwŒÌhwΜfFþÿÿ·÷·Av$Y}?žš]–gfyhV°Æê–ï²ÐFkk@-v’Ç„ñE ‡‰ðŒtõfBá·ê;¯þ¯t¥·Åغš˜p8ìèž6ÆP‹˜ÁmO{šÝ…k©†E0bš‡¯aÍÓö?OæÉª|ªª¬‡{owëû‰(©ï½UY'OžÌÊãúðéFg¶•žŸËÀà‡ø‹Øw`¼ÀÁ25hsÑu1Öþ²ã5· ‡Ö^úòΫ†Öί^rײߘ[ëô0õ:–ED; Ó‚ÂÔ—ûª3gò3ûÒ¬Xçõÿá,Z‡ÈŽÍ-ºì„d÷>¹-.«pxÏ)QI¤¼Ïo©0ë°GaÆ´–[v˜Í¹³ bûÞ+À •£íÁJçäqæ:IÏ$æÑÌ0gø5*Η­yônöŪÊK2¯_– ÷Ä…GCÏëÔ£r‚<:Ð}¤Nl=Ñšyyçõl£AžMS:у³êºÔ]YÇd\x팪¶cÔ± Ž<ˆµij™Ž½ÇCï>-«©÷î_?#v <»IJ›ê„_Ö…uµ‰eMz”:™ÄgWÕÞ{bU§SEeþªl=­ÇÐuLÙû"¬÷òžÇW#›[{¨ ¤r? }”çce»))l§ÇDáÐ9žÓ*™¦öÐðÞmêhaÚl%i§_cúRK­_!GƒiËMšæˆGF’ ®=ÈãüH-ÿ1íFÓçh½g"ÉrMµ®SÆ¥uÚV†ÏÏ$ÆÖ^°?ùðWð±ïÀx9D¯â¿Áž…f ùá×f}s 6¯]ž›Ôž#\P—@{(ª¤'„5X Ñω;Ï®×v.îk(R‰ª{k@¥ËŒÂÓ)ÇÔà1ç¾$ÄŸþ•OÒΕ~5ÿ&"Xöj]lõ†lÝAkÒ;xÅ2=¨K 4(ž —ßðò í£öðfyõr°ª·è<Ô~Q¥Ëˆ>°r¨PôÄǾΕi–½ ÏÌQ¨ªf:v6†ââ;}ÌzÐÔ%Pùàì Bor–žpT£³¿Nõƒ-¿Ó¯bÝ«ùžd ì@ÎgÞâOò0qà`p°L,h ,-ƒ %p°´€–ÀÁÐ8XZ˾dG¬\¸(VñÇ6¯ÍŠálŠá쬘UGµn÷C•¯ÉÛ‚ÖéÅÕþ|€x´".^X‘5±K¨^Kû»¶ÉŸ'ÇÎêEi#CYbêÓxäðu¦ìò€Õ5CÓ:7Žºš¢g*›©´ÕhÛ4òÄ6%-OZüŒºvwjmʃe?²1ƒ{üw²Ã|ã&ÿ *ÙY½!F¢/V··Åöö-qî0ÿ@„ÍWbÿîŽqîeiKóüyZŒGŽ@g'¨k &ób Ûè[½þü¸°)FWÖÄâõuYg¨n~œ¿À^$·äÐïÔ¬8Ÿ€=Á£bKþ7÷ÔãæXöp°tEbm/5‰‡ðÒÒìwu˜Ð|†Ó²Ï1¡Û*LøüHþµ&'åo±a ?©g‹Gçå9n‰[Nì-iÐrx¡ã~89‡¬gGŲ¦—‰ú>XP‘nDvµÒ…¼n%Ó'Ëí§]¯Ë«GÑ>÷bÎÏtê•eTWC±b…nÇBì³åž\ê\/Yþøû l<³3þÁ %'™ßáJòVtosesî½Üßâðòë'/YþJêQé9^ú]UÜŸóHywë©»|ƶ—²²ÈáûFì<¯3|Ž×R[ŽœOð5¶\A9•Ö ;]þÛ¹6?ì{¸ú¢£BgJN_Ž»$²kJì£Å2¹‚:åé.­Î…¤\W]v‘2Šéã¡®7¥ç¼S}N OÊ9žÌê÷XÛ-ñëÝ]ý5“×OE {êsÔþ˜H2펛¶]W¬ÏÙ}¼4TºÕÏ s•:×~žÉ\zoÖOLvÇæ¹l¼ïPƒ]Ð ï.ï^8zt÷¨<®¾Åßí¾½{•¾¾ÍŸå7CùùùåÝ÷øó®ükùyë;Nç«ù»o]•é^Ø]~·às NÇÈòÞ«ä5W¥D–͑וO_cÿβ:éx(ÙÜ4ͽLž ÓµõâɯðÒ6éØúÝ?Ì{HP.¦<­´¹ù^ny†drZç©ûÑw¶\ŽìHªò¢Óµl#f—…y3×Åï­åËÓîÅ圧ë–{ö».ç9;'&¯ŸnvŽuoSeוÜ?¸&ö_~¾~*pulÐiëü³Þ¼l~ù{ç¼ëb6£Ë.ÿ.)ÝŒ}„6œ£ä´ôQ§ÎÙùáïœ63j™ù³“¶ù.¿¿Öe$¶ÜR®«.»PÏò ˦$1ñ½jÕ;I ³9Ǻº½¹iÇt›Ënî™·ÀRÒõñî#Éd¶uÍzÓ: uÈbî›R^¶nœûœ§ìœê{«2ˆÈîêÀOÀ~äÓ_à?ÀT@KÇÐéÁ þ@ëÅ—ûBܼ¡gœé}Qú/œy ïŒ8÷‚<çÞq—Îy¸-ÖÄ¢8ó´ ÜÁ^3OŸ‘©ŽÄm3£¥BŽû¢IˆÑëf®jSÜ–ò->{ZJ•¯ùÎó#e½:TéÜ¢˜geªvš’Ûò “'®¸´¦{o –8]µ_é¡ègçHNôÅð”—¶Lwø\¾WÄÎ;HÝg­ëfz·¤þ²4Ò1÷_·ö¡˜é]Ó÷ÉBp˳)çÕü¼ùgHKòÚeK.ÖÝÖ;”úŒ8ýìbnŠq÷5ÒÝÙx^ؾ¯_Ëmåð9q‹lÐBï…Ñ«AÞÖÄàEÊ›g“Ìæë#!N§)íØ½2{ïEge³}n^¶ô%m{UÚàÚ•‘3cZZçÞ¬;×®æÅY™¶¸ÿ@•—¾¿[æþ£óÞŒ­—Nÿ:•ŶxÈß4a¦wYʸ&î¼aYS7\º±e]߯÷kæŸÓõ8k’¡Yïž[Ž)íZµêœSot»°¶U£têÈ,eº–íùÁçÈÖó“Xç’®K)»‡bûžLGµÙ†ø>%ŽÎŸg":Kz~-ÛÏ">G¶Ý#%O{sÛn³·˜[¶±Nu¯‚R{(H—ê}1ºíX{ínfw´ŒtñR_>§rûØyãŽjK©ŽÖiß Ÿ¹Bª²ý©³RʽÕ3Ƕmêkœ’}ÙîgmÒ£»âéê™ô{À–Ž ÖH™•ÝHîÀPZv|uÖ çUK~5P,YþÓîPç„êžšgçdç• Ú都¢5ßóŒÞ¼u(¹PF:p^ºóKÛbÛt³pé…Èæ¾sâXÖÙ—Ãv$õäùá—Tvăûò¿ãǼN°qzØíE1›´q‹+g Á€¼ªósÎÊ Ú‰:i¼¼±æÎÛùÆö»—ã$r)ÚçF;›xÀÊ”Ö#¦Þ~ìœ2vh»ÿâÜ8vã mNÝðèÆ–ÝÁv¶L‚—ÔeguIÖA90~Ó¬'´k•Œ£Î•PGæ@&‹¤:!麔²c›º² +[ÖQ 3Óö3A²Û–9HÇ©óuìÍmcEbæ©9þ«ˆr{(J÷=ûJpºý›{欔zMl³1ª6ÛÒôö­@Þ­‘¸Höç9SHº·×¦«¶çøYqö¸õughÌŒÜ+vÇgä^ZR®ÌÂ3©î~'^¦R¾„‚£edGnè̽0åûí<›í‡æó½ë2¿´*ï);­…Ëš|¸Ãj-sÒ˜e&|³Ñ×+#U>Ξ>ª£Ñ­3@ñ"S,ÔŒ¾ý–µ—¥·t¼ƒ=} yÛƒEuî³èmÿîr²zÔŒÜ&ýAçdßš¡—БþW̧ز3pä²÷—~èh¨Tdݤ%Kš¶k.“­sÝÈ,IªsR®kZv‡Ï‰kjß’úWA½³äE2«sXÎö¦8a›Rí)§(݈™ õ)ógM>¨¶O¶cK/¹õ·NûV†®ãkbðJz‹”zoÝno‰Û¯Ãˆ£UÕ„ˆ,×õ>.X´–Žqfž²Y_í`Ð=¯ƒÊ3©ò3…V;TãÔp¢KÊgƒÄí0³3妔ɰèÎÙZÞáR˜½Tì K9²DvÂÊgÝe^Õþ!#y/FŒ˜WœÇÙ£„ÓÉ–]˜%8tŽ=XtàHu¯¼c©7½³œ5=«å~G¨7–8²,ˆÁqZb‘N¯ï/5̈…éΊÞ}ZdÒ¥e´÷„½§À‚Ø~ÁìMF!(íõëÂÝDh)@°´¡ 9˜¸µ½*úÖ^% WæÔ’˜Zét@QùÄóß“¶CöeÉÛìù-µlËÏ›¹w¬ãMËØÜ{±} ÄÉ&´§ˆ°öAáûúKŠJëQc"÷—iÏIÛ¨·ÍèEÛ§ŽºŠ t àú«6)¶Lç¬^²öÂ9¹-.Ûû]PYÓg»¬é*ùs¡æ#¸ôæy¦UóCå=¥]“„:óè¬Î%”E¢Ì)$Õ¹•×%•Ù´W—©J+_/¬‡ÅTÕ»°Î÷ÄcîÕÒÞ(ýÕK–­¼#Î\×ϬæèevNºç…X•å_I´®=ÛÒÛ·*²·g9ì2RïÍNY‰í2ýû;½GѤ"û€ƒÃ!zW3ÿ Ú@³}ó¾Ÿx’?€‰ƒ0x½Ò=C@j}Ëå1`üÐ>b,¯wuAY€ rPÏúËnõÒ£”¨@€:ÀÁº…fŽ)Ùm“¥U`‰pÀ¡é‚€–ÀÁÐ8XZ @Kà`h ,-ƒ %p°´€–ÀÁÐ8XZ @Kà`h ,-ƒ %p°´€–ÀÁÐ8XZ @Kà`h ,-ƒ %p°tÈæµY1;;›üYˆ±rá¢XyÄcl å5ç<îDÙ×Ë¢òŸ¦Í?N¤”…OJà•_g¤Ü»aß5÷=ÔÑ¥ UmŠßÿrÏW²ï¡v8XÆÉÆH îñß 1›¯ Äÿ=fĹ—·ÅöÒ<ÞÛŒS_éi÷Åê¶Ô™s¬‹á©‘èÑ©0.fz·¤üÑÜd'ñ¥ÿ= 6ÅèÊšX¼¾®u¿Ol7$Ô[û²HgoÔ%pp`{¾´ªëåËçäÓÅgÒmÅãJJY4á`–ßÎê 1Êžë·Ä¹ÃüÃÄ™)í^7¥ãÀ„˜ç®Å¢VÞyóMcåѱ%ÿ›{ PöÅö=!çŽðg0=Pux¸µ&Ä©Ym@5p°Œ Ò{žf1ÖÄà¤ÞXÀC½¬$ W ηÃ3 αÃ]ÕQ) Ó®Z÷6iúiEC0}™üÓÚiã.›ÉC=Ýe%&}}ï¦üóÞ@,×Kx9N^Êß—.ÓñóXXFœçw?oåºÎ–kØzLÑ1çÜsqõÿ£B_•6&K$XÞS£,R8|LÌÉÿÖ¶êÏ’L7ÙáêRý.u³’É–ÿ^,/aìkÓ-+ÖsÜæâ!ú^¹ÐQ¬’cAG³ÝìÉs=[©,ßÖÊu¯d=©£#Fçé|ë~¥6³s _cî™é"°Mßz½öáÚ]þÞÆoCèYž¼¼«t«ò©›Î9*M²%ß>m»³?ô÷Å6C”èBÝ·'T¯àÊ‚û[FÜæ3<}„²T—E`Oþ=ÿœŠòÊŸ·E:ÍÑe;‡e÷î¥m'¦ë» yÕ=åwù3dQaYdv§/cŠì!FYùyº‰é=¥üÂóÓ䬃–Ûn¯‚2³îÊ[7OUhyŠëa®çü>]Ëì‚Îx{xt÷èÑ«»oóçÝ·®ÊÏv—ßåÏ1Ô9Þuï.ï^ß]xõ=þâíÝ«Îg‰§sõ-ý‘xïÕ nº:]_F}Ö{»ËÏËóž_–1|ÿ£ÃËĺ1i›{ÙåÞ?’ÖW~ŸC÷¶ôî×]Wfc‘ë"2…焸vàáÝ×ճƿÞèÊÏsµ¼û2vkÉ˧#gDw®MÆà{ØyH*—ÐÖÌu¹íDàtìs m.»DF»_V–LAÚFÏVZº\íëtÞ=ùerÊ"û\–¯üœR™#66áºT˜·H>bßå×Uë:KÇÎ[`k~:Õö©ËÝJÓäÁ·3‹Xž2û)“/b÷Á9±2äïL¾B™%Ꜳ:^¬ __N9Ù|*í bßF?‘2 ô)¿LFçÞ±r ó”BÔÞ=lkŠåMÉ“N±Ì¾>Ýó]¹ø~1*•€ƒÇ§¿À€©€–=BÙÚSàðiqæT>ËoÖ¾^¶×œ>'._’ç¼vWçZÐgO¨_É{\ºl­§Õ{7кäA–/í¸7K#s`é]-uì‹>å!+¿Mq[¦³øìi)Yy¥nìgȉõ—tŸAæ‹ëV}ûá%NJ†ÉíO’do çtE®PT’Ô¡Õï¬  ƒeO°(fc [ï?P^§Ä¡¸â¨B6Ó1éÉïËC˜Cœ5È·e*²ôŒ×qÜi>~Ìz°úá>8ÁŽHgTw\·ÄkÑv ´LXÎGwÅÙñò`(ÚŸB>tSRñµÈzÐàtl^*ç uþœï“të<GĬ<§>å6Fƒ­[ªìèƒN«–¹5AÛ¢’;K¡ÓBv23»Ál/oq™Çl[ª!oçkìÕÀ&qé_%eåR¯•Rdó¾s ¯<bƒjbF;ÎJRÚµJjå«Læ:L¨.%µ†ò¼¥ë:LGÙZÔ1”bŸì¼Œ-w©¤~yÍ/Y›•fËFxɆCA§Œ=¨S´GEíºZH\晜ÒVTöjÙ\HPÌÊûñ³™ÓÞmGßÇè}ç;bMêô윴!S¿Tgw‡ud|õ—\Ç~}û™g•ó…–8MÚQbo>¿*Ù£ ä\Ytĉõº–}µv—ŸrÀN»Þ“>Cuêõ ±YSLG]g‹AïÝ`v'Uþ+¹Çëuí#yQçÁÇ ÍC2Ùe³géTG¯)ÜYJ`t“ó*±õ¾¥:æcˆ;Hí’|Ÿ’ƒfä`…fÁ×û‡÷¦{=~ï&uŽä9oRôA5ÝÊ[íRõO YŽÎ×¾3­êÓÊæÓÑšâöá ”2Úµ*:ÈWÕ ± ]Úfy[\Nž·D]7ÙIJÂ>•sGÕivJªsê:[±œG$ETè·—ñïU˜A¼üšˆF5÷¢ÖÒŸ c¥ª­Hì'4³¹ˆó„÷Ü"tôL¬w.r:SkžCìäÓNwÀÜ¦ŽŒŸ”þR3ûQEÕ®Øt¶¤<›ºx~Uqo$FJDzy1m7 À–}Àæ5ö(¦t†Vsè5ŸGgêDÕw²ð@×ÜÏ>*ú-¸Ý¿âOÍF¨ÏX.šýeÊ«î̆¡¨:¶:¦.cêÈo Õl¶–™ŽêHžÖÈÁÐ/ïÐ÷¬aÓ×Cun¹ÌÖiY9[Æád颩™äIQe¥fi™ôv­„‰æ+‘Nm³›¶8Y×ÉK3-Rì“£zô÷´Ì‘œ-];Yx "-Åà{µY*a/W©ZDÏÞq9¾k‘ÔVTõšÚÜšØöÇš(˜y*þDóQç©g¹ŽRQ1êyHéëç»IÔQel¤D¶°yËùUŽLr¶LÈÉ’bo“èš:OΦ{‘¥l“,à`Ùóð€:˜YÔ2ôž(²ÛSg†–—?„¯ÑÕ3 º¦#FòÙ>‡Ã_{èÎþYÄwéï#÷+#qçžìp•­µöÖ®gðìgùîòÅûu¨Yëàm ¼\vz²ý ’t¬;eá^‰ÑC5ѳ‹á ¤XŒ Ž>ðÃÊuR9S‘·³ž>¬mðfÑ-j×#vØùûP˜åte˨Òfžmx°Èm;›·k-ò5.:³Í¤v"…:º#‹ Mí“÷Ýy ä­à¶Ñ_ŠÁ¶P›l¹ÊPéÉÝS'DEgE–Rér÷ò”æm…ÓOhisÁ3•—©È{¹ /óÉž,Ãö+ô½‰DÕ}ŽÑKKêùžEuVGJ(Œ&M¥¸=ÔËWt½ìÄ~Ÿ×ÈÙ_+¸ )öÖáó+Þ[%ß³f 2€–qŠvH~8{^=#™£ÞŒ`ÑúäRô&³4£e;Ì ¨Ù$QmDJa—ÖŒýÎê’°sCï‹2=ÛáE,¸›šV¡;U3«,÷ÍQ>Ëc>ûél!š73kë„ËNÚLòÊ—OŠŽãõùå#M_.zvÑë¨ržlš¤]G8ƒc¶‘*Rå ò>bì8âÌ€¾p®Ç50ÝÕ#Þ<ÚY®Æ6oÙœîŒò¦”Dbyø˜ ¯{AûÀŒ>*Úµj½¥ä«9Ó­K)íD ©º&Ü{z®¶k’jûd§»çxփƒeœàHg0el¡ ² ¦‰å3%ècös–(p¹×««ãk+ªû íl.ÆIä½/Ò2 ³‰~ð$6ÅÎqž»ìL¡%¶–SP×—5ïùÞUј¨¥|sì&ö–ŸÙ¸=ÖšºUß~´Éš,Þs¤kRì-圮ñu= –qbfÀh}°×Á¬-EX½D!Õ2z ÊãÆÜº^–ÀfZç¾~]8çÐS SÏw×OC­™_–$k]sï¾½9©D…|ËA”µÅ•9±š…\Ò>ëb(¬µ¯'bNÊS7l;u_}^Ù 7'Ìã‚_ ö‰ÉÛù-¦\¤k=à¤2Ô3&É:~s(„ìë§;=Üvúq"¥,ÙέçÖ¸ôõø–Ã~d¶w°W€ƒ¥kN Åúö¶ØvŽUÑ¿7 û¤C x´"nÜä¿:#1¸Ç$j¾2¿$Ûª—ωþÜ I:–ƒ¼—FB\ZÕíe‘ 'ò÷[âÜaþ @S;݈qîeYç–æõÇqé å°Ï˜Ùßêuú4€Ç8X&‚|X-÷åÿ#q38`ñPlßbqî`ˤ82+å[ïä!—*$ŸBâ³ÃŽpÑ!šÃU½”FýnÂxyy}­Ê™…oÛa÷òPá¹Þµ¥! |îpCË’¥¥äàÐbó]Öï]CGÅò-·—–—‡ð>žt”ÞÇ„¾nËÏùtcôñ¤L'bM~5:ï]+qË5¶¤Á×wN¶"¦÷ *l#JäÊ­ÊäüHþµ&'åoJ-WÔ6;Ë[ø»ÒkPþŸ[F,ÿ±|iû Ë+«Oòo#æ±Uu„×TêAáçñ._Ÿ¬îdiùº îº÷‹Ù­÷]Rº~åsb:P×ö„:ëÊ‚º~¸¡ëôEi_™¼$Wf;úRM¹ž³²ól<´‰:eÁçùÑßWÖ7&hÿ#¶íëßO;(Ÿ¨¹¤]Sn+Q|;hr%‹–3”-+Sý©¸ý§´‹Úé YKë¹w­³ì¥¤--#°§/PP¬ßòëbeùÐ;'L[é¬L_ÆæKïãŸÃ÷)H×-Gƒ[·Ô9òü•L?¾}˜ïókŠÈîWV~|ÿFÏ6…w޼ÖiWÕ½‡bÅ’ÝyÖÚ×zé7³I-OQǵ;»,Ìul*}ºW˜?—ŠüÀãÂ.èˆ÷v—Ÿ?º{ôùeùW„·®î=zt÷ê[úãÛCÿ\ÿú·w¯Êó½°»ü®úBóîòî+…—ö{¯^PŸíôÕýTzWeÊŒw]ßË‘¯q¯cY‡&eýù«–&8­ì;/Ffûó]~_G1Gîí`ôjŸ“’®‘‡õ”_ãÝ[ëÝÒ¹Ñi¦+“®•V¦ã°¬Šó% d’$–±“®ºÆ/sÛËm³uÞ"édö›•‰[Ξ‰@nÿ÷˜.L¹²,¡&a:¡Êω¥‚¾ÎÎ'—§+GÇ&_Ö½T:þçXºeåëŸSU ·,3Ùœt%åU¦g#_X^VÚ“²7‹ðz×Έ@ÿž}ê|¸2øéúçT_“f+ñtíò ¯ PúrËØI×˯ÆÖSìž-qá=ÊeÕ:1÷!øú.ЗQLÞ›P÷ròàË“ßÛÎGVVEòXeYœWþlòÑ—ùζM÷>’È9Ž]–ƒk‹™ÌœŽ)+;]B§mëÕ½.F––}ÏÀμ2+Ôum¡äaôÍ÷sË;® GÙgydé'Ød–Ÿˆœò(®ãúºìw#wDw~]+Í?`b|ú ü˜ p°t†ÿ€²á‡œù-ÒÙP8±x‡A=°‚{¸$÷Ë Â{ˆú°œîïÕrÅ: „#»¥ÓQI¹+S…üQÊÓ5ú uXÕ-(§L=dx×FËÊ»„Ûð䋜Sm9õó¦Ò ì‡ï™¥ï•}$ݸ<A¾ÒlÞÑÿÔ­Hºñs8oŽcøéèÁK+E‡Ñ¶Ð»_ZYt`oœvQ$ ÊUåGÝ£@÷Þun[œr>§ÊVbé]ÜqÓˆ±ƒˆ¬Eé+<¹SeÙDÔ–ìô“lÝ#qҊׇ¸þô¹:ñ¼º×y¶‘'¥~Äm_ß_¥I7.¿+s™ÎƒrÔI›hZWö¸Îâù‹è (?æÚ¨Œúœ ?žÎbé—éG_ËOŠ=y2Eåvm')ÿ€‰ËtÁ¡®Q›ÙZ!’êè‰màh6o<|NÜÚÞƒôÁ ñUáô.þ¾ÎF”YXèBd#É9q¬£Í 瞪·ÑÙLï–ØÞµmž2Ý‹mr÷úP,\YS\:ªmÜVËúÏðæ{†Ã§Å™SB¬m=”æÅÙKfij\N°ßÃácRcBŒ^שÌô.‹¾XwÞàÀÖGwÅ©ã@ŸãÇ"t®‰mW–õƒûò¿àœqúÙEi;ÛB¦X³5·¤H· ‹gU> —p”àê°«¼q:—ÎjûÉ8"fe¹r¢/†òwS~ª|[ÚÛâ³§=yʘç^èKY& ý;R;}qVÕUæÔ¬”ÆfF;.ÿ»y[ÞUß7”ßÖCQ9:<Ü–òEô©ÊÕ]’Ú¼®K¹Îrt¾Å™§=íÙé>z ¶äßAû 6£åú߆h]2¤èÙPÖ6,‹VöÆ2ÞìÉú[f )(WÕ¶ª:înB™…úóRŒ8é×Ô±•´¶:dæé32‡#Ñ“2Ä—UUÕÇíYƒz^A“¶´F_À¯·d©EÚ"Ú´Ö~–e©ØbvP£= ê«¶7·6¸õ7©m*$l ŽÌQ>r#ê?Ûê<·üz]£.×µI&nuÃs(p°tMô-Bò0»ô3ùÚW÷Å*£6­À^ã-;a‹××eú몳¿w°ÖáR'ß²*;Ã>£›Ü‘”ƒØZuµfÛ¤¥×a œ ¼0uÔõ9ÕíÐiäw¸óþÚ]Ùuð;öMÑ›y¶âþ%O”F¶¡;wªlÔ€¯¯élé.oEéT9Ì •œ5Št«àA’v¬íˆ»¯‘óÏë4Fþª“ncë’åHTë*H§‚wÈÅ‘F]G)9Õ`ÑɇÞ/E¡œS¦TÏåèÁtÓ²hgoÊQòæP^aë8w¶¤”«½gCïæ¢¾)ÛB•f1©×Ô·•ê¶:@9tûdöß¡Ãq¶TÔǦímY“hÖ–6ë ð€vR4lÏÆGEÛTDÄA1óM­”‘’¿¦Ï-M“º¼ghõÜ€ƒ ,Ó`c¨¢9úËò!J©äÙÞ±òâ@¬YN'êc°y::ÜQ¤Ãs.ÙhèNöè¼ß1掆IÇ>ì4ÕŒ9¯:%ÔÙ.ïd‡³\aaþÙÉU3[íÚŒs6§m¨ _·~޲C]ËÉÒUÞŠÒ©P˜qzS×æë²«}êŒ8];ŠËr¬E-Eœ\j6ÙB;·ræÕõb]ùéTQ=8È)ŸÙaÕaïØ+íN¹žSh^­í£´¼«Ê‘08YÑâóhE,Énžÿ„×W׸¦¾­$¶Õüš`>—œälÉ, õ±vûßTÖ4jµ¥û“¥}=뚆m“Ù¦©vf¦<Ûš?·Õå=G‹üÀ–) èa¸¥ê¤—ÂN;ž{~ 3EjEv²¯êŽñ Ó±vf.m¼Ýí}ä åuj+B ƒ°u^îàtÞ ¯Œ¤~e§ü¹¶]IžÍ1³ÞìÀiþ«éÎ6fz×tÔKY´L@Wy+J§h†Ì"[Æ0T¶Ö—KÕÄ8ÖF¯ÜQ«¾µtÒÙæ•Î|²Q3Õêm źªÝUo'‹Ô^ aG"„6_¼ìÍv8p””Š6+ É·Ï)zN¡EYtdošy~•¿^N¨õo–Z˜·y|FGù‘&:Ò®Ž8J¹¦Ž­4n«#Ì/‘£É½e}´©jÿ;”5…ª¶´y_À^žâRÏþ«H©gúœ0ÖŠRáçpImS!¡]è¥VeÎÑ”g[‹çVz¹wi‘8€ÀÁ2ô¬³×É㙬rx–Àyˆqäš>ü õÖ4먖dÇø2uä®,ñ î¼èËŽ2ÍfÚ&sYÍRñR$§g:=³É7í%I2ZÊtÞY†›#ù›—ïÙRw¦wþ9žaµdÞY]Ráéíh mƒ÷jq:¥<˜Êö“àA|0Øóè*o±t2û‰,ÏÉá=dy‘T.ç*Ê—Ú_cMŒn®ì©áËF¡ý¹NÉO{1ÙvifªYz ×U-‚zCH{¦=|{N²y†Ó·#*ò()}M¬Žzç$ÚNRôœBó²¨io<ðôÁzPÍéDõÏåzé²8÷ ýÚgOž /D•Eâ5ul%©­ÑË"¼ˆØ)…õQÊUÕþít3Y“HiK=š÷äµjŸ0i·¶5°‡Ès-©=‹œ£ë·‹‘t£$›T1m*$©m*bM ^ÌõE6˜¢¯Ø3ɶÅÎIznÕ©—{˜Æù€,Ó€Bš—ûª“gÖÝξ4+ÖUxslvÍ@áÔ´œF>ÄÌu³ bû³îÛŸ=˜"½zÉ]#}cn]‡K{Ž=Сëtx·Ú§@êÉ^/ß»OK`LX2­y—×P6ÿ®Ö± yŽÙìµ€ÅëC!²te'ÀÞ„ØBwcdžÙSeX½æ?Cvoy2/\™SáÎí6lhl‹Îžr0%®¯ç¡Öf¦žÎ)›í*o‘tzbUç£ ŽªHZÎU˜/ž–%ÝSCvâåð"—íf_æÑ ëŽÈ?{~K-KÈõÚîÂkgÄÐË£ÞŸ¡Ü¾¨¾­_V}ë‰- 7÷ì9Õæ aº²nw¯ ë¨wNªí4!IÏ)¤•E”¨½Åþ6\WeKeoˆ®í(_rQX®´Œ…òNÏ ;ï'·Åeʇ¼6ºmkêÚJu[B׸ù“Çù‘Z.ã–_Q}LiÿÃvº‰¬I¤´¥>ûåß³£Föoy®¥Ô³È9ºí7íbAºNÞéy¥— W‘Ò6Åé‹á³w2}-\iú*Ì_¹’ž[t]ݺ¼iš8€¢W ñß<ÐàGT’ö‘ YIÕ™Üë¢4ë¿ î<[Qn&.5sÁ ­äØPƒ.¯Ã^ôýø |ı—Ç¿×Áäóv(²7úþ•câV{zØ&hE«h§ÈøÛÒœÄçÖåqÏ?Óã3ï ñ‰'ù˜8ˆ` ¶O!ùp®LŒXÔ† 7¯zK‹Z·^&^…Þ/¨q˜}—ÐÑfožTxÏ„r×¢ÈÞè{ÑÉ«QÁžªÐæ¹uxÜó6p°AwxÖ³Ðe~É_ê䇛‡˜×\ªóÏróÒŽpjÓ4PŸ÷ÀRí±ï—*)·7Úd®Ý~@²÷ê#E4yn$÷ü€ –,š.ˆ`h ,-ƒ %p°´€–ÀÁÐ8XZKglŠá쬸¸ºÃŸA ›×fÅììPj¯Œ±rÎÓÇpƒ¿nMU™ñ}¯éÜówV/&ÈÞ ÒKžO_Ž8íå wKZ¹M–i—1ëäŠük²Œ³¾NÒéE±òˆ?î ÊùÑŠ¸øXµ7“{~L•¡Ì×$ì1¥-ñëF—í_B½c7}ÿØ—¶?±òMÃí+ÀÞ°÷Ù‰Á½E1|s[loo‹Á þ~âÌ‹¼ÿ­Þ ²Svã&ÿ]ƒ™Þ-©Ÿ”²)ʘ"(ã‡jùo¦Í‰|Ýçóçi2κQ#íÅëëªïâË}±vea9àØKåÛ°¯ã°çÙygKþ;'Ží…:@N Äê%ùÿÍÛ0ªoà`'‘ðP'$4 µÔa«Ùy‘ ½”ÆC±é„ir¨¬:Ç ŸÔaÉvZ‘POOî‹«›‘ôùžÖy݇iê{,\Y“Dî“…§Ý?Èo«Ù¢ªðâ\&ûœZ2P™•ãóò\/üÙµ·ì‚óˆý•—Q˜?÷~òHYÚQe÷¸z’yÛÐé82W¤­ä•2núi‘ž¸>˜#Ð…÷{R^%~ùÆòZVnŠÎÚŠðw£šWÆÉéúyðÛ "ütïò÷6eu™‹”…–Dz÷åW^7®¸ý3çxéŽÏæ8ïR¦ n°ÎÕ÷çGò¯518©Ï­C¯<õïRÇÁ3!(„rnHEÝÈdŒéºRn‡áªuM¦O/Q»÷Ï‘Gbyè<„ifyãϾý8iäöãÀ×[T÷v·’Õo™Ægµ†~¹Úiéô/J}eçýªô-9"åÔƒ&ú­°1•¯ÂºñЭÓkÝÛçä2•§Î‘¹Eùï–xåµÂ†”n‡bÅ’Í)Gû:O‡­Ú)¿|Yח룺]Ë)± ’£¤¯SetÄÛ»Wݽðê{úã»Ë»äç«o銷®îµ¿ãÏG^•W3ü]–Îî{»ËÏÇÏ9zôÂîò»ôŸ3ÌÎÐx÷{ïÕ n:’·‡©÷·Ó×y=úü²ü•ñî•Jpÿ¡Üi÷×i|—<¯>¾^Ýó]¹Œžìû¥È!°¼ lY}Ý9òpNÞ”Ž\ù\Üü…eS`g6)v!Чã\—¶NǖѲ_Ë^´®òûéÏvÚ|mcŠäÖ:L+·$ñgç:þ.KÛèÌ*ŸLY><›OI7ËGxN©=Õ§¬.ûe¥‰ÕÅòòsê‡$(»@þlÝ'–n–§Îm.O§º,JÊ¡_„o›FÆ0–ÜIåÁ©+$Ô˜ŒÙý#6ë¤å`ÊÛÓe$úž¡í8y ò³Y–/ª .k뾑ü8i]_¤?û<“g'}ÖOv^nŸŽNm{ŒåËþHÑoäœ$óïeò \fÎG`ãåu#L;B´\sÜ4lˆËÎÏsм™¾"åæ½$_N¾CÝEóáß7?Ç|gå¡Ð.¬²ãsû(>ýþLD°Œ‰ÍWbíÔPôíýBNôÅð”£×]ï~ÙÚ7ƒÏYÛz¨?ó:_÷-­Å¦]Y‹×ûÎóÏ Å¢‰ÛjÆbG¬¼4BÊ}-Û£aFœ»Jçäì¬ÞW,ŠáÕsòW†e/™m눤û?Òks¯_³Ö Ï‹Ár_ˆ›½òšÚÐŒÌïc­MîZ§\dJÏÈtœÙ/‹‡ÛB–¶8ó´µ×F­µÓ;âÁ}ùߥ³–½H[xy[l/ïòRÇî3bz:|N\»n[]´ûb5“qFœ~–Òqíeæé3ò›5±­ª™®âÒªµ¿Ûý½X*šõf¹ûË–NYîµ×îæ³iåÖU[¡Òqò.ïµ´*¿©fmP\žu•¶!¥.Ïô.ËTÖÄ7¬rظ­®ÓöÝ übegꦼfd×M™n¾gMž®sÎ8mαyÎ %õ>}ÿòçÁˇ*)Á;ZÆ”rnJzÝpeÔuÌ·Ù³ŽÜ…\ºì´‘±üÍô®IÖÄàE=k®í¸/.[õœÚ‚ËÒŽ¶ ˆÃ§Åªsö¹îŠ;Tïž±K(G/틳–nšïÃ%õ÷\xí’Û§yvÝp# üò±Ix¥è·sóꪯ÷¶·¤5êF 6†¢§ž{ú>ul¨ÿBnëõäµu˜ÒNUñPlK]|ö´%Oâ>_íš©ûë]Ⱥÿ¢U{8XÆÄü’ˆ¾Ì­,ü’áêg‹E1{„ÿŒëD:´´îC/ ÷çKMìa)9|LÌñŸR"q÷5yÅ©3â´Õ %Jü‘vÿ7î„;"µ“]ƒÛ×t¹ºƒµ1Èpü˜[.Š‚ŽºG›ðeî|Ýì¹a¹¤Û½E¬.™y*·:"9íS³¢¤J…¨Áº,? c=ÆÑàÃrûõW rŒœDE¹uÓVÄbÄ1[9GT$ÏŒ8vœÿLnKæÅYo±ù:9‚ùº&åWPv±º¹8çÄí¡3À§ÍEí§-)ÏCÙþW)åÜœôºÑÝ]nyoŠÛrËŸ=(wò%14€Nƒ˜÷ÜÖêçGXï z ¬—Ζ/}J!®¿¹§<Ë;2+ïé9;Ëì³ò9”¢ßîm,¨Óм½I¯i¨ÍlÙ&²ãüHöòû¤ÛßnÕ·n;U ·Í*5'×JÛ5.óàß{8XÆ…õ€¤©ÞE¾ÆŒËýj0ñp+þH÷Ÿ)Øë]{7õ[yÞ¤YæÑÙň¤Ýˆ“?s¨5É"éþܱsÎë©MwŒÄˆ;@ñèIÈCwºT”r’ð½k8[T‡OÙ‡‡ gK»×øª3oѶNU Öq›ôÕQî*”».´Ú9Òb Ûª *’'â˜I¨ËÊá’ >õ Ìw×)¿:e 2“œViÔµ¹œÔæb*Ÿh'Prn˜ë|2vʇÞÌ`íAÛK«RNnSag„v^°ó1p*X>'n±.ì|{g‹!â€t&[RH|•êwÌ6¡mÝðÑv+ÓðŽ<ІhnC]Ë[å¥È&vöi9šF2›v­¸Ì`?ËXØ+/Ê$…7òƒÔÌ.Ô¥¨Q{p';ªK*ŒÔ<è#KE ;O‘‡?üÃ#’î8Hº_¬FÏi^!|Õ™‘ŒÀ1 ŠQ¾ß:-¹¡ŽlˆÕ‰7iÐrr¶9YšÙ}á@]Eº«Sq¸SÊi;‡¢Ü=]å«hÐÏ3-hÖ;!‡MJ]¶Ÿ*úà ¥¯Y~uÔa¤YWÿiÙœ$åyDr®Í¸ë|:Edq°yœæV[ߨü¬H­ŠåA93zÙ&ß—ãäléÆÉqâN”Sõ*×ï8m,Bgu£mhJò:¨e_,·é 5v²Ý9±`ÀÁ2¸î‡7r§©z ®ûMéX8 ¬ú³±: Ù jÁšq§Se–Ž„¯Ô³(mª)¤Ýß„Oë¥yF´Û=X$¼÷†½>}â2T`Ö/›È„úÌë5ø±Ž·¢¡ÝÇÂÎ%î¾»:àÌÛèÅ¢ÁJáZuµ”!uYUWùâH• ^´w4kƒŠä±>uÚ’yÑç½mVhy=³ß¤ü lÎ,ݱÛÉ`‰·‡ÕƒßÚ\g$=RH)禌±Î'.O3˜·²ì˜¼Ë/xùK L¤ÖèY§Jö6)Àì¹T¸¬QRgr&p.òÒº`Ùk ÜçPŠ~Çic:«uhaCS‘·ÓÊ–\5¡¸Ìõ²Ò®—:@·ÀÁ2Øûî<x6Ž?%c6ô³—ŸÈœ^—kÖM›ŠµùÏjdðr gßÿ™Ž^no²˜ËmÖ-ëMG¢g¿/›Eq7ZI÷çÍáFçín>#Z·ãš‚ÙŒÓlÌ×XŽ$jµO ïWà8qx`ì±EÊI¯Rô^}¨ö¾¹Œï ÐÐîYOkW–"ökÂÔ;¬S ý‚ ü6ùb­¶M’ðªÖo gÝh’v!-l¨Ãº\ív£b‹÷ÖªCìyfÊ<Ûä·‹¾Œ8XÆ…íÒÚhùp ‡:Äö f-²Ýa¬B‡¯^²Ò:/ĪŠ$È¡ý2V/Y{eœÜ—í5¸r@q‹>ÛkéµìÃz8S¨§Ç^FÝÙosL9Ú¦Y¯(Z…xæÀþã3ï ñ‰'ù˜8ˆ`ÙëDCl9„>i©G¨S>. á°õ¢×EÐ êÌKw÷ƒ[Ž^ò à‹ZÐ!ÕíöL˜l:–aìS°ÿÐ{ndK °ï@Ë>@Í{ëií0ànѳ¶ÎºßSSžM­â®6§·"ì¿Èi¢£UÜ} Ê–t¥3Ù6h¯°#X¨¼FOÝBôÁ¾!îî‰v,°¯AËtƒ8ÀÁ2]°D %p°´€–ÀÁÐ8XZ @Kà`h , ‚M1œWwøsŒ±raVÌ^X‘¥’’n9›×êÞÔegõ¢˜½(VñS§‰­ÚÞf¯mêÃ=–¯qÓLoªŽÍ¥öÊà´I¿òþ‡®të•YŒG+⢓§öí °¡²ÝûõD·SUvZÛ›:öIÛ@í˜U?ê꡽@²%·7ã±³ýÚGÐå¶xmùà·ižÀ~PÁ¼lo‹[½þ ÀàÄ@loßçógМ‘Ü[Ã7·¥N·Åà§'§ÛÍWbÿc@•-ÿ}ÀÙY½!F¢/V¥ ï¶aG¬¼4â¿53½[Rö|jOŸw¶Äâ³§ERÏá1²³j6ÅèÊšX¼¾®ÚÓí¥ãüýA%´cØïÀÁ 14bNƒ³ ìcn­ qjVáÏ  ;âîkBœy3µyô@¨õ)èö+p°t…ªÏΊá†fLáöè†Ð‡!«ÞïònðO„ZÎ0+VØhö»úͺ¶46roîí† ç¡Ã±{?wù{›Š<9Ää"b÷.#—KÝKé†ÂŽ}yÃPT'$×9‡óáéU/ðBš½û‘ÜúýþL8Xº"Ö‰0)ïAl?ô݇rŽóP ÌD<í¸±ŽÍóWw¯ÊZvóàuláÃ’pˆ:O^§Aâ^ë=0#y´õ#ÿuáËÕ›÷0/êdØ×úç¨ÏRgtÿLÿv~âeSTÖeį±Ó÷ô!©<¢º*¾¶˜y<ÆÓu¯M±µ¢ß½2QùåûÛ·E¦ÊS4X½()ÿT9*tÒLoqâi¹ùòòQ]Ö‰e!,—ø5®Œé²–ÉÏ ËQ¢§PN‚ïáØ¤_SdPô{pN_Êö¶Ê›wÿÀ&ãò¤Ø\Tw%y®Ó¸å);¯ý‰á—QJYû×Ðç ëò^ùu®l>n^3’ÚKÏ~$î½ ÒV:×çÄÎ/µµÀâ2]§\ãŸST^ÎyÕùˡóVX†òò0߇eê–E¼¼XžD}çywe ï­±ÓÎó­¡ÏMì²J6ÿ>»þ…6âŸSP€ZÀÁ2]°D¨cê®›-Z÷=ÿL_þ»%d¡–‹bÖ>iã¶  Úþ3Þvv‡O‹3§„XÛzÈ_xœ8+úbMÜyƒ4_—©?+η®y¸-Ï苳'ôÇœñà¾üïÒYo½q,Û‡Ö^Sž¬ð`&̓ʼn¾J¹G¯›0ÐMqû¦Ìuå&yÛbt¡'uA›lÆ6÷óôæ£ò9GéIJûŽÔëÔȶóƱ&ËììÜ¢÷èPWµnzÑYs¾87‰Õüóâì%YvWD<ü=­<ô>a™Ï<5Ç¥ânŠœ…sسKÙ¾)¶ã¡Ø¾³›#bV–a®~xzËrü˜—¦Îë ¨'qfž>#-D/¥ —@H*åÐu"¦“ÓÏJ[¼·-sßTo㦬¬”YñºÇmO¶Ó)ó†m›d~i[l¿|N§•-?àÐx‡nÚyÕFyí‘Ân×Ò†—å7÷¢GK½.­&Ù³«ß»4ÔÝ«§NÚ†qîÊÓq—ËCë#öL+"­¬ÕßÙ}´¬sÏœ•¹\ÛæqJÏø@~CWíe„¢½<Ôß‘geŠ­U¶QéÁÒuÒ5‡Ï‰[YÛj-QK‰˜äüUõ,ŠÒôê›iËo›e8´$JÖe­»zíDÝ~BJŸ±½]jÙ“i˜¾PÑsÑ®›ü·³ü§Šª> ì}à`™*ü€j^§k±N» ?¼TGAߟ:ê|ó¶z˜)§KôÁ«B1޳ÁƬe·»s`ÄZÝÙ‹ |îÄHÉ$;p/¦íK `Ljv,TÁ²ñ5Ô1¡ÁàjЪì˜ ú«¡zp8-,Ú¶—…¨Éƒú”ÛZBEÏnG׉íšG¾oÝ¿¯ßsLjþêôRÓŒMº¨²W5MÚ‰JûŒØ%NÚNï´ç¢z³•²g»=*w¶$õi`ËTi;€àÎu<üc©øÁ©ZäÈP³.Bò„‚=!ÄÏÿë}WÆ} jRûŒ-í’É#Ý ¶S¥øYÀI:ÿ«Êù38YÑRÕ§€= ,SF=ð¬Pfƒr0ÈaGaH°3Cgcv/îDhGÆ–¸ýŠu¡fddz/Þ(yðòÃÝD™dØ!/Å"ú†›lVh¨ªåˆ•G•ÞbºÑöPô¦ MP–žn“˺ªÌbX×h=ét«êžÂ×µcS~³I«K.ñkýú6iS*k1½Yù1uÝ•ÝÈiä°äV÷ŒëWãÙœ——¨p¾\b÷h¶$ZîøuU“RÖæ÷þ)uGѸ½”XvAr•é8;,H:ß׉ÿ»/žV•íÔ%Îc~nì竹Ô6*OÙ}túιÁõ,ÃÑ »ËïòWDL–·®:ù4ù¶u¨ïoË ñ®K"áþæsì^yþ²ȯóòì–s@DƘN|¹][0ò•ËìÚª)3+OBû6×Ù2{6ÏçY\{"øüB½øénšÑr‹Øµ¾·]F,–v,޾ýsø³}ï:ö`åK—£¹&E·æ·Ü\ùÒl8Œ¬1˜º·n•±Î«¼î%Šî`y¼vróu’éŒ8MåÅå·xýZX~7{n‰×ö4G–O&3ëÞËÿÌÓgä7kb[ˆ.¿Ì”¥-M²íS–®8|NÜ’ƒ>ݱ¶ÂèÏËA‚ÇÜSÙðD£Ì<°zô@{"çøÆŽQ›ÙÚaí,{9È:›©ZË T¸½ÇâÜþK3¿$Ñf0™…Ð/¨Á©Ëœ8ÖÉàÈ%ýþ‹bÖÝaç*¾8k;j$3OÍñ_ã ®“R[8~,³-Fȹ"o6t®Hü²ÖÄœ9 4pï‹U~Kù^'Oô-BòÈgb kÄ]ZUç¨p«°2ä¸Ñ÷[W³Ø¡íýy ®Æ1ŽÌÊ’Ý똙væÞHŒ”ƒiM ^œÌ>ÊI÷&EFÈ{žäò¨p¶(瘪G#Ñ3eXêl ÑŽîifìtè{O’ÞMr”I])ý¦ÀöÀNÝ#bÖ¯¼o‰¹§:"Näí¼©†i&N9+|‡Šm3æ áÞBí¡äÈs䀃,]ÁËi(âC;'Š"¼A®Ï¨o^3KI8…Ë:|vÄÊ‹¼L€¯ìk{»»¿Þô7”c­0ú çÅ$—0p´—®'«*rap²"¢E½F—¯Qr¶¤;YÆa4U{x´"–x9¡ÖMÃ× ;‘*6ç;UãèÞ'M*†— ©åAÁR«íñŽÉ¶'e°“+"cz{ ö+p°t„ž!£SÔ@Á#XŽ`ÏÖò`ÇßD<º+îPYø{áLtð¬{6¯CùËáš¿L€ó4~º»¿¨{û[HÆ!1xµ+£ZQ!ÝÀûhT9mŸ×Ô'EËR"(Ç¥·¿ ÁË:‚%yeeÛÌŠ÷©1o¯Iru±Ç—]/§©ƒv‚û(9í‰YÞ¾²]GÑ9»fıãò¿à:³äÈjOÔ~GTF+ªM±—Úè½NB=›È´´7™`™“AGþ5zóØWÀÁÒz åu®9ªÅgíÊR>[O ±ÏfkçEŸÎÆ!»ïfƒY3àÈ7½5çX°³¦éþ.ïÚ&>“,‹±ò’ìÉ^ZÕuöåsbÿXŒ8XºæøqFvoÇf"6n‹Ñ¥¾|´¦‡ìˆ¾9‹÷b‘,`¿óèØ’ÿÍ=uP†83âÜUY?Åš¸ó\  [n­ qjVáÏ  Åö=!ç EˆKç§Ÿ]£×ÃÛæë[bøÜYþD…Q'„zÒ² ž©ÎÝ+¬´¢á´Rê‡sòwVZA˜ª ª“®¾¶G‘4XÍ®ÉÏy Ò`2y½óJÀ*û<Å­‰ÁI÷\'d¾"uï“:Šbt^žëG6xùË¥"oEøz-°ƒ¢t³pt/U}ä2«Ð|™ÇMGGœv,-Û‰/#ŸoÎËd*¹F³Îs.L:zyA‹PüÃçÄåKòÿ›·Ý4ü2©ˆr)’Ãè8»¶"Ý\'6Eºpqm]–ã†Ö½.“„rã² ìš¿7e,é¨ÐMvoûš /á9¹Íå¸y”G…N2ºv»®8m–Í˧փWÇ•>ãõ>+[Ïœ|ªß†bÅ’%û½ÄvTÚvÛeÉP®³»d²—ÔgEðûþ¡!‡‰9ùßÚÖCýYäÇÓ·ú]êf%³Óü÷ÀvºfÚ~†šsXÏîµa]·ÉôÅŸ}½Ð³oŸry%ãJ7BpSÜv9ËÌÖKlHÝC¦}&•QR7B´¬öó&¨ëVZŽ>Õ}|yüö!×E•͸¶,Ó}‡°(³wõ[Ð÷qËBSÕ¾¦Ëœ¡t¡#±×®,¸iV”‡’[~«£p ØñöîÕ£GwßÞÝ}ëêîÑ£Wå76ò÷ç—w߳ϣo‡òoÿ\uý…Ýåwù³úýèî…Wßã/ÞÛ]~žÒÉÓ§Áç)9–%û,á´¯¾UðYòÞ«äw¹Ì*vœn&߻˻lÙ$:ð>Ž.‚|FˆäSëÔþ.’O–1̧w¯Â¼•”Ÿ$È¿‘Ѥ“ ³˜|:ït„z4×eçdi[öc¥åæƒÏ±äQxiÇdŠË]l?Z¾šu"B4 7¡œòÖKq=(\ûOI×׉Ƴ‰:–^ŒÝd÷K)7>Ƕs‰-S¨Ë‚t3bizí‚ÉŸ}NDŸAôbÒqdfÝØíIaÚ,SX.ü»<=ýLù;éøù4òFËÀ¾Wh;±¶«*_Y:ÔgsM®W#c$}‹Ð¦,¼² Ë!¼>“Õ»gX6¾mFÊÔªGA¾¼2²qäŒØ›Ö•].!•òŽ+Ýañ5™ŽµîÜrrÓÔixéxv–c]§Ó²òäåÑÜ+·ûêü¡üº!ñm:ª_ÿ~±¶!L[çÕJ˲5scGLPLnYÄîäËœcÝ;z]€¯’ò°dÎ죬ít§¿À€©€–qpâ¬è o™ÐÆm±õìé`êü3´`È=wóõ‘§ÎˆÓkƒõïCq-[kkBªë¡×"/ŠáUkýì‰X½D³¡zcç >'Ô¯ŠºëÌÕRÞuko€™Þ51<%ïó’;ÃÑ_¶Ò=ÑWçØ³˜•<Ò{©ô—íµÕób°,õ|o FÎìM ¾~¨l…Øz‡ç¬Ôþ!}±ämM ^,šMË÷Ldze9¢"]g®|Úž|=º2kl™gTÔ•ŸÖÌÓgä7kb»†ú5^:½Ë‘ûÓ¤N4aæ)5GÎù‹•‰©W#qÙ´`Ý:k´P^uæiÒ@ÃtS`[_¼~-·õÃçĵëu[.ÿ{wÄÝl6qGÜ}mMˆKg¥íˆ÷åWêoƒÌÃËÛ%û}p¹Óæ¹{¤´?Ñɘ•ù¦¸M2FÚ÷™Ï7í:ÉùôÛ¿,]4µ1·n}ÞÑ{1øÏ£—WÕ9Íi¾¨ÛÕË*M£‡~®?ÉüsZîr\)ësÖYFŽ^¥|/.l{—\ZÍõ̶âçKדmiÝ <Ü–y0í#˽tßyÇ•n@“zO¸öRë™díå‘=G_±ÚÖŒMeN™‘lT7¤/ÕhW³=Yì}D¸~®]év¨ŽLó¢Oí±±.¿Í¾Eu3Cç-ÍÞ‹©•¯2™“02§´U^€,ca^œu:àÔ Ýr;Dv"¤wÖ ~7Âdx´êÑ–x ;]f€Ñ›õCOSáNÚñc^~bÕE1ÛvI/w>ƒt¢ºæÄ±H§UÒtY¸P¢¢#^´g‚ê$S'´ŽÎâòU2Ö5è e2Ô®PT&þ <€;£ÖR#ÇÔ8Ýb-‰vÕCš­=/ÝwîÉ!Ï3dÙlw´1prH·nuy,Ü<­ýÙyãN4iõ¹ =¹ÿ@Þ]’ÒVp9[TòȺsvNêä£Ê8"£CX'ŽPÎàÖ“¥©íL¨ ÔÄiÄ1+e¬F?[œe³ b †b=s⻎¹l)/rñd¥Á«¼Vú¬åjy…KçûI(}‡ËWKI‘w\é4©÷DWϤqì¸üÏ_ÆI('¶iŸ,´«E{²ØíPJí¨ ÍGfå·†:ö^L|µ¶ýZmUËþìà`ê!f:4XE³ï¦Ãç:³ßÀ¬E¶»Ó¥:fôV3XÒç¤;[t¼f “€Ž¸™0ªêêpf׊P¬2:ÐÙ$½Ó;™:¡m…¢•eRŒåà9ƒZ¤[E¡­;õTØ!òÚ]U×´c#\SQk´1°üV ì”—ºæ—¶Å¶š™µÑž³¥ªýQØ÷4Gû·±¥µl‹ÜÑ …ÊvžÊœ#ÚéR]èT:ÂÚΤÚ@UŸy`ƒãJú‘·ÉÃ{+‰½Eï&EÉs”=V“ï-± ÷ø~NÄÀ¸ÐeŠpžN‘jyÇ•nH“zߘ`B%è}‡ìƒòÆ?&Á“c‡ÆEu“÷2´±wMùªá\ãsö#p°Œ 5ˤ]Ô%³ïö-u)DšRÌkö‚Ã7æ°`þM‡˜.$:YRg2»¡Éì}[(,ÖÕ9¬èZLVg{‘&u¢.j†Ï̦5rJ0v”ƒï j“n…¶Þ°³«œÂÊi`/²àYpmÛ´„]ƒ-•Åר9[,'KRûS0—G›W’¦¶ê<¥íCµ©iÙØLÓ6¹9ÊÑÈöº®¢ÝzÅÎòŽ+Ý€&õ¾ ‘‰ÝFÁއL6ë(]Âd“êì†Âºi;*ÛØ{Ædó5ÎçìGà`f™ÐŠì€‹òÙ÷l€6Txw´N7èŒpH9vd„%`¡gObaýšù%½¾>ÍÉSæk–t¼TE=è#¯÷äâ „µš-,z£AQ¨¾ìXÑ[†ÖY Æ6[^«N4 [ ÏëÜy ˜/Kb¸^•‡PçuiHΠK—­õõ-Ò-Œ` l=¥L¢çðÒƒ;¯Œ¤lUëäyOådÐßT"jzšMk‚=P Y]áÏMHm+X/Û¯Ð÷&ª‡Û÷—–”®ª£«ÂÙ`Â_â8lj;-ÚÀúõY;ƒÃ6°Ã(ZEQ™MåWº! ê}*’tÁ\‰i«|—­Fø&rÂ¥z­›êåqµ(ª›¶S¼…½Û4ÏWZ=?ààËÑË„bP¸<ÈÀ›ÍÝYøbÔfgZŸÍV™M-¸bo·yÍ «×û.ŒDÏvd³'zà©;ÞlwÔÍúgý /ØoDb6gëY³k;«K*”·õÀ™;,YgOâèõ»kW–,™e§‹–œŠ~‘n¹ƒPw‚°,$f¶°0ozߊʈ\†,ãXuÖ3 ¾‘ë•m¥-qû©W'ê!íÖ³;Xš½TìSM½2›mcêÒˆÊÝÙ -]ãHÈ7ŒÔiŸ˜­gebSuÊíRêÜ]ò"eq^GªÑõ¢²ÑwFÝÝw%¥ý1y·Û ·®4&¹­`gŠÔ‹íÜÔv»V½´ÄÃqšgAÄS’¼ãJ7 I½oƒû¼Û¼FË}н±g¨±»êöÚÆì?ålJϺÉÚ¡„þT²n’“9Z7 ©öî÷}<’òÕ힟pЀƒeœðƒ²2|œPpIl¶ÆG>¤oQ¨.…Ó †Öꬌy1 üìœYqcn]¯ÛΠµÜëb(¬}äÀsn9½§5Øë×…»‚ì P¨±Ù-Þ  7 ä¥=JæTؾã|CL¤­ÅæŽ …N»2÷Ä…Ûzëú]8¥pSÎ"y›=¿¥B—Ëò¦Ö¶/Ëáe¶†œÊp5—qœ:kɼzIvØŒ^On‹ËµÖ…Ç)´Ÿ’:¡÷H)'®Ñ¡>âöPX&IáôY&%öIéRyËsò}ŽÄö zï£2[?yGœYvˤN¹é²ðÛ,Z"èµòèÝìK{,Ò µ-^½ |Óæ¥™Þ«Û"¬Ï^]iAj[¡7†tõ×U}1|öN–Ï…+¢² šÚdJ¾:«Ïd»ò:‘Ù®´ YªU¶› §o?Ë”¬d_òç`Ö܆–¨9õJ/ÍŠu•ÏXD‡ð½ó²#¹BÈr(\Ú–"ï¸Ò hRï[pJÚŒ´÷>ÅËcº!¯¹ooŽœJ¤âgx®Ï”þT$·_7Åu*&ÕÞ#}—”|uGÓ¶ "‡è]Íü7˜&VÄE\4@Ól?Ð’×°‡i]'&ɪ4Ã}^4©k©ÞdÝ?ˆAÑ*Ú9Š1Èa®œ#8Mñ™÷…øÄ“üLD°ìô›(Z†¼p€ØWuB…èWí[Ò1䀚õ£Æ8Ü<%.‚ ÿ·÷$Ë”Ñ{œðòÌÞ°¿ê;9ôÒ– G}ËŠèॠ5£hÌ+\Õ,2"àh–,š.ˆ`h ,-ƒ %p°´€–ÀÁÐ8XZKglŠá쬸¸ºÃŸc¤œ3mvÄÊ…Y1{m“?ÇÙY½(fg‡2G²1”i^+øs­ˆ‹RÇà þeLå@÷¾°"5X-O•λ'­¬÷'”·r[‹ 7Æ—wmÛavÔ©‹lKÎõ{E·)L¸FÚŠÍkn»µ·ì3‚×^òÒïž>ÝœŸé]ÃSBŒ^*‰Ê¢YîórPAŽëZB ¸eÇíÊfWûš‡[kBœšGøó´ØY]ƒ{‹bxõœÈ¦ˆN Äê%!ÛÚÑÞx€Ç8X:硺i‡¦ÆÂþ«B=õ5ÃU®«Î1!¨Âk_뤭f–‡bE…ÈêßMÚ:l¶à:†Â®ósÜÖhØ­ºŸuF]%o"¾ìÑpíw¼{9çÄÊ!-ݸÞtöhõÞ@,ÈïK”ž.®>àlJ삯uÇ!Ì–ÜIºò(¿ÆènÓ•ÏYîP~ŽkWaèvùýY/ò;÷ô•y›U¯þH¹6Œò ¸éÒ±µryIÖp³'¿“2ür<1Šut_”ni½0xõÖ+Ç2\}Y×µÏY[aéªÒF¹þ–žÚä]þ¾?ïòˆéºôYª)/³¼ rà¼gúHh³5‰r×¢LÏú~v ï±yÛŽ–è0h?BYìóµ£çŒ8íE¼Ì?Ó—ÿn‰‰ö `Œì‚Žx{÷êÑ£»Gåqõ-þj÷½ÝåçåwÏ/Ë¿}Î…Wõ§ìšìwÉ[W½4Lºv—ß寈w—w/8çIükù³“¾äí¡—§¥åb™³Ï}ÍU)æ½W/D>GäòÉòzùôd÷uÈ÷)•…?‡ù‹¤›]9ÇÑÿîé:€óœß;×·/_±]øvÅxú«Î§S–Ç@þ, ‘s²2°Ê¥øº’ûû2ÇÒ±ì1Å–\NL×'ÅÞ<;ÑpÚæœ@ÞˆÍ5–¾ÂzçËÜ0_L¹M‡òÄÐ2š¼åòä2ÇñÛBÛGþþ,ˆ}„z*³ßÎbDôÆõÒN»Ú†ù[ol#¶NÜüÇì¿³Ò îµÏ@“ײ²õlÖ•—¯·õä1<'Ew…º‹ÜË.;£+íð:“Ž[ŸµÜ^Þ=ÝÙú1÷qäa[Êï–oxN$_› Ñi;më#û.j;ay–Y&wXÎ ?/ž=ÅIÛKǵËÅzʸ¬>Äò™•…uÿRyc¶És„ê|'>ýþLD°tÍ¥U18ÁWÌë5½ñPÏÑyoöðÒeg®ZÃj(úÙ½$f¶øuwv¥ÿ‚•þ#½^½¿l­ù=|N\»¾(Ö^»›ÏðÉ´¯Y{””ÏŽlŠÑ•5±x}ÝÊû¼,ËknÞVù¨#o!1ÙÍ}î ÄÈšÙ‹Ër#>;Éé.^¿¦{³§g²RõVÊŽ^£íèvFœ{yUv Õv!¯yò|Gܵò³ù:¥Í3[)yò©¡_²ó|)ÏÕ¡XŒœ“•ÁáÓâŒ,k?ïNý¨#³¯CÒG³w Ó-µ7λc'îŠ;÷¤®Ÿ)ˆü0”¶'±zÇe!-è†=ƒÞ _4«L³¶NûáðPlÓŒíñc¿— åqÚ‚“·¾ÔfÎüs:o·ûµ#hXG^ý™yúŒüfMl…â¤À{Dô—­}Q¸^f4©wupìa^ôeûSZ¸*ÅÔ{§œÙF¼6f¬t¦»´zažIëu—ÅN}’R:ÏGSOmÝ툻¯­Éò³£Ä¤¾Ú’¹wRÿ À&×+ÊÜì'rÙÊÙÊe™¶ÿ<+mÛŒí¤<3: ŽÜ©˜4W_õÜÕ’§|Ìó`+Öé6uñÙÓ¹,¤ÏíŠ=ã6†b!Ò^˜p°tÌâ\l…nl îÄPO?Ýù¥m±m:Y˜1‡¨:,ŠYûÒ‡Ûr˜á}'Qû%،蠩`€òè”Vˆ¹§¼+d'Ålz˜.o ²‹g•ƒbë¼{ÊB笉;o„]0½ÌaQœy:v §›ª·Rb'∘%çƒ"Ñ.‚ülŠÛ48á´“òäSC¿>&æä¶³,^Š©%s“A} Ó ìíȬ̉)Ÿp°j–Öœ-u2ém ¨ÞÅ:ð5óEÎÝa·WR);¨È–@ðr‡ í‹°ói;,³#sù`¶Q½«Aiâúëß{æ)ª™%Èê-©k]ÎÖòµdcrt¦»¤zÁËâ;,ŸɈÚoþ|œé]–2[ísÔ¡:'Žym¼²%ÕN¤=Šl²ªÌ ‰åËWÔ2ÒgigFÔ‘;•¢½UÊ'•êÑqÿAÄ!4/Î*Ç΂̗7ÉVõ§x¬ƒð8ÀÁ2mÌš^ûHéØZ :ŸBÛÛëjö­ Ý!ëÕɪ ¡¼6é²thJáµßFFuÐÆŸšNôÆÿqì8ÿi¨´ Y»Ûj¶Óœ”çɧNƒN·ã$jC=™]ZF(R•nÄÞØá”aJŒ-Øó¤&)õ®”‚|e³¡ëv.óè@¡=ö¾½›4Û¿-¶ß¤(„é bÂÁln‚=ØŽ œ}åä{xÓ»¯ß”BQ§Ý%Õ Ž¾nûu¨Fõ-U<Šl²ºÌ­}LÈi)çô6'KQþ,K¢”T¹SaGÛBMF©º7RoaÓå^àl¡~Ùé¡h,–iÄð°Cn}vÄÊ‹ÞÌçï홋nä­œ‰Íh2ÐæAEäèL·þ ;#ÒÉK° 5ÃÆQjy·Œ¬nžÒõ›™ìjÀ2r蜈½Î4kÀ•º<¨Š„sôR3w Dœ”Ùþâ™øRäÀa‰<ºüËÚÃÉ`GªØ„ƒÌéØpaý­r8l õR°e#§µœaât »¤zÑ•C¸˜¼}.p¨FêM`KÏ"›¬*óì­a&½ÂÁyù³´Î3£ ÒåN%2¹±PÑ¿œGåT&gKèd1Ž»ÂMÆL8X¦Nx=s[Z°÷ÜÊ(Ü‹@-ÛIƒ‚CQ80G­ 7šËëà,»°PÑnTE K4ÂC£uâïë ÉäïJoºS®·5ìBíaCúXQ˃ì{Jžjè7X?Î…6NƒF2ï{S×ÞÌ€kôŠìŸªÚƒ$Ȳ,׫ºK´4EK'â襲ó_ô³GDá>.ðєҊ頓¡}ÚQSµá‚ú[e #”£m‚t¦»¤zÁë 5Á–™¨µWFò¾‹bøœß6†ËOò7Ĥ=Šl²¼Ì¹ŽyÔËL}JŸ¥5ž>UvROîTòeYü£í?\Æ5q²}”Šì¥[Ð)p°L‘lpb¿>Rv&õÌmÙfe<ûæt¾8J„?ÂÂÎÛNM1Ta¦~D*z³EZ7œw‚ó¨•þ‰òÚ˜Íì®,…²{Vçê —å/ª[~ù1Aoº£V6›Ï-Þˆ%kR=+—“nº¾ve ÏöÂÏSòäSC¿î¦“ò ×.J7•&2Áÿú‘LÍH²7vˆnÊrô÷ái$/orê”…©WÞFÉH}¹ÎÌp¾ ©ÿêR5”zh´‹Ò‡´{Ìu`j˜M¹í È9ú#ˆ%ÚpÐV°S Œ «A¬þf:+^ê¡éÞ 9ËWSØa/«ÊcÓú¤›V/̆ɶspgu©™C0 ?oޤü±®51x1o㩾¨H"¾wÒsÀßô–¨¬'\6žSÁJÛ¶¤g†qY›ÌÇd¬lëÉŠÙ/Ç. cÿõ6Ø|m¤îfg½¶´d?{o;{8X¦ mä¸.†ÂZg-ªsËUáÐ4¢ýKdçË\7» ¶_àµÈ‘Y/z(¯_ÖZ÷žØ¢Püa¦jó99ÐÏå_å‡;ym e÷:ýëgÄsŽÔ©¨È_˜®-Ñ9®ÞÌ ,åñ¢ýÙ!½õæPµ‰NGÔÀ-'Ý.ô=%‘ý{'k㮵ÏóÆž„çé̱I*óë®MúèkÜýnnÌ­KÝKi=FUÛ–YøÌ òpîGuÙßO)¡ ¬#w:=ŸßReQ·í m¾.$‹k“Êv¥tëAØÑROàù ‡è]Íü7 4ˆÔÀFƒ¿ÇèÎftyГ2(£ÍD{÷ce°_¡Èƒ…×ÎìÝ2¥eŒäšêÞ*)J÷4Xw÷R6£œ9(Ýó™÷…øÄ“üLD°€Ç¢×x0Vô^Ý,?‡ËþÌ´Y²P˜0äì“ò¹Ñs¼\£í«@#â›à  x¬P¯#=Ù`OÃËn8ò§«å`œ¨¥ù²Ý–T/霴äÅ_êc–k´Xþ êc^{­¢Õ {à±K„€–MD°´€–ÀÁÐ8XZ @Kà`h ,-ƒ %p°´€–ÀÁÐ8XZ @Kà`h ,-ƒ %p°´€–ÀÁÐ8XZ @Kà`h ,-ƒ %p°´€–ÀÁÐ8XZ @Kà`h ,-ƒ %p°´€–ÀÁÐ8XZ @Kà`h ,-ƒ %p°´€–ÀÁÐ8XZ @Kà`h ,-ƒ %p°´€–ÀÁÐ8XZ @KíJøopøÒ—¾$>|(¾ð…/ˆ¿ø‹¿O>ù¤:þÎßù;|ÆÞç½÷Þð þþèG?*¾õ[¿Uý  ˜Ï¼/Ä'žä`âÀÁ€Ç“÷ÿ\|yë÷Åî¾(ýí®8ôÑqèð7 ñÍVÏôOÞ_þý?»þTúª ñõ_#ž˜û!žüZ>aïAŽ•wÞyGüÕ_ý“óMßôMâ~àøÓÞ…œ+¿ù›¿ÉŸ4ÿøÇád™÷ïߟûÜçÄ?úGÿH|ÅW| Ø/ÀÁ2]à`ÀþäË$>ûæo‰CóOL|#—?³%Ä[Ÿ»ó7üf÷C_!ž8ñ=â‰ï›_þì¶øòÆçÄ¡¿úkþUs胇~èïŠC?Êßì>ÿùÏ‹­-™·¾ök¿VüÐýÚ›üê¯þ*ÿå'Ëxyûí·Å¯ÿú¯«¿â'~B9rDý 8X°ù²xø¿&îÿé—…øP=Ë—ÿpGìþ›ü)Ρß#v7>ÇŸâ|à'~Tˆ|š>ï¾û®øíßþmþ$ÄÌÌŒøÎïüNåPù³?û3åx¡¥C-Ú‹Ë…HÎ}èCâÏÿüσâë¿þëÅ'?ùIþºä7Þ¿ñ¿ÁŸºq°üÑý‘ø/ÿ忈¿üË¿äo\¾ò+¿R|ä#O?ý´Š®tÿ_üÅ_Tÿøÿ¸º/Œlr €}Ç—ÿàÓb‹œ+MØÒûz”Qå\!¾üÛïð_Ó‡ö*±+åñ}ß÷}jß•~ðƒÊÙò]ßõ]ü«ÿ÷ÿþ_þkï@Ë‚677•c…œB­Æ9nß¾í8WºæðáÃâþÃèŸúÔ§Ä·}Û·‰ßÿýß¿ôK¿$Þÿ}>ö/p°`ñ—Ÿ›[&Ä“ß.f>ÄßÕ€"Xº`÷ÿ˜ÿš.ý×­ö\1s%æœ '‹"Eööž+&z%æd±D =ú§*~þç>º¬Œ">nݺ%þý¿ÿ÷byyYüìÏþ¬Ú›åÿý¿ÿÇg¤C‘GßýÝßíßû½ß+þñ?þÇ*"‰œ+ÿûÿo>ö/p°`ñ%ñÎgG¼/¾QÌͤYGæKáæ¯ø+wÿ–iñ'ò'Ù È’S‚¿û»¿«¾3ÐÛ„ _ó5_ÃMã\!‡Êüü¼ú.æd¡ÿ§¹ÿ -¯2K¬lþöoÿV9¬èŸ¢kö¿÷{¿'~îç~Né?ÆßüÍß(ÙÿÏÿù?bggGEI½þúëÊéòßþÛS‘'m9tèršÑ’½æô€&|àÿ'á¿ö4_ú½ÏˆßzôWâÃÇH|÷“)v>ÿHü¹ø:ñmßù­â«ùœJ~ç…ø‹ú³ðO~­xâcÓÛÇ„fý¿ê«¾Jí½Bƒ`‚ö\¡Šh¡W4ÓÀ•œ.ă2' í{1Î=/R±+ä@ùã?þc%7A‘9ô7íBtδøÿãˆßùßÉ^}MK[rb}ö³Ÿø‡¨~£­ ¿ñõn@´Ü‰tNßS>÷Ú†±¹Bެ&S"Nè-CF1(}:–ªÍÍÍñ·.伡ÈZFdï DÑ5¿üË¿,îÞ½«ôÿ[¿õ[â«¿ú«Å7ó7+Ç A‘7ÿñ?þGñ ßð ÊŽþëý¯ê\*—øÃê{‚œ_ôv$âøñãJþÿôŸþ“ª7O=õT–AeIi~à(ÍÄ@ ö_zGüæï¾/Ä7͉ïÿh‹.ÌG¿…ÿhÇ¡ï˜^4 ÿ×ÿú_*ÊÀ^²AûýÛÛÛj¿4Ò@– Md?úѪ¿§‰ï\yô葳ԉ zìuÓ“„tm;"(Ò‚¾#Ý“^Mä ýOŸé{úÝŽÈ ë黽94Úòæ›oe– é‹®½wïžr~ìcã_„²…Ÿù™ŸQÎÃïùžïQûµÃäW~åWÄÆÆ†cã„‘ƒÞŽõþÁ?PŽÄ_ø…_(|£9ihÉEåøF”9W¾ã;¾ƒ¿€tà`À>àKbë3¿#¾øñ½Ÿ8ÒªóÄ'ÿ®]~3jˆ¼^¥3(" Ö)ŠÅDL43Og3ón¢@췦Ќ=ELß¹B]Z²âC¿Ùù›&¢Æ†¾û⿼‡>Ó÷E×ì%Μ9#þÞßû{ü©9)Ž#Šä¹qã†süÛûoÕ>/´ óÙ³g³å_ä Z__WN”úOÿ©8}ú´Ú¯åŸü“¢"™èUÒäx±!§á?ûgÿL凎þÏÿ¹²-:×/#‚îIÑ2T&ä3³’–>QýÙ ^€ý,Øó|ñþ¦øüŸPÌÿ˜øHÛÞËžø‘ïâÃzéLmäuOÐõ2IC³ívÄ ifŸ–j4ð¤Ùš¡7P$-Ñ hP9í¥*¾s…´ä4ò¡ß¦¹çŠôëCßÑÒ+ÿu¿fIVÑ5{ ŠöøÑýQþäB¯i~î¹ç”“ƒÎ)ZâCåY…ý¡S§N)çE‰üÈüˆø—ÿò_:6IKƒ(ÚêÛ¿ýÛÕR6OoÆzâ‰'Ôr-²cã- :zô¨r@½ˆ_÷u_§¢¼LD ½Všî?;;«îuAË€=Ο‰Ï¿GË`þFìüÖšøÕ_ýU>6ÅÑê‘¿ú#±IŸ7j,Uø¦oO|êåH¬æ¾ò|ºî¼~P¤‡Y2CKhJƒDh? ÙÍØÿàþ ³ÇÅ4°+4X&g‘A`Ø+΂tFò(¢†¾#’þiàOÐÿôÙDGØ‘7tý´u_Ä'>ñ õ6²²-Z*CçP” 9Cþþßÿûü«ÆìÿS†ý!*wŠ0!çɯÿú¯«èZNEŽCÒ'ýF{¸˜ƒ dß´ìÍ^&dö²¡ï("ùÈ C‘*f#J7´ô^Û/°€ƒ{œ¯‡¿ë)5ãìß.¾þƒòç~½øvú|¤Þ²ŸCßòañħNÈQdâryÞ>õIuÝ´°—™Ð†¶ØÓžœ4p¥¨Á >íþi`œ+ÉIƒg[NÃ^r®hoþáV‡yÓA:úé§Õwô¿íD¡ïÌ5tý^†ä¦%8ö«¼cÓˆœ-äD2ØĦB‘>¤/мúµ_û5' ÆìiCû§ÐÛŠìƒÎ%ça½yŒÌ‡ä¦ o)*Œœ,ô?90©^Ù5¨,Øã|H|ówU!ÿîññ5Ô“yâkÄGèóáúK~}ë7Š'~ì„_í.õ¿«ó¾uºË<Ê^µLƒdrNƒö!h k/š´ìÂ8WÚ·ƒö‹ñÙ‹Îí[Û»†ðäxˆ ä‹®Ù‹ÞüÇ\9/ óŠt±£sì¿ë@÷ûøå0¡=Whï‚îKvûÉO~R\¾|9zPŒ½<ËÞ„Ø@6Gç”9KhéÝ– Ñ25ª7Ô¶4q k}Û7kçÉW~ˆ¿ñß?A‘+ò¼icfÍ[lȉaGÐ`ÙìÏ2MÈ1DËM~ìÇ~¬p@¾—+ äŒ 'Ë¿øÿ¢ÐÁBØÑ#ô–Ÿ¦P™SÔ íÃc6Ë¥H+:èµÊþþ)ôÝ¿ûwÿN--²¡h({3[ZòCiRÚe›Õ’ó‹“Tgè5ÐäŒìâíJ€Ç8XðØsèÈŒÚ[e÷ƒn}þÀ3'Ä¡Ž^íÜrRè­;äd¡™ˆ~ö³Ÿuœ+ô敽ð:f‚ö̰e÷,p®ì-Ê¢ž¾üå/gŽràÙ¯W® EØ7½õÖ[*…¢NhS]r˜¼úê«*ºåsŸûœxýõ×ÕÞKäòïI;ÿçÿüŸU ×—¯¬¬('Ђ³œ)åÎ¥åA´÷ŠÀ^åóŸÿ¼z׿ù7ÿFô7}W-³K=4ìS¾Q|ìé?öôÇä_í9ô|æ“b÷ ½<€þÿ€ü,Žì?Ͷ›%´g (©ÃLƒJ;¢…Þøb^Õ¼ ˜´7‰½ œ+û z+A.Eo ª-É!'-é¡(Úl–¢HèµËd¿ñ¿¡œ+áBvMo5ò÷"{¢}ˆÞxã ñ?ÿçÿT‘+t}J4 ݃ö-"gíÉÀ^‡êÉÏýÜÏ)Gã¿þ×ÿZô7}G¿érH6bùÜ9_þ?»ÿß;âÐß}JúЇÔ`u¯½…¾%@ƒYÚÄ”.ä ‚seò³?û³j”iÛm„{ûömõf#ÿíF©PØ/þâ/ª¿iyTUÄ ÓæçþçU´×OþäOfoÿ¢½Œ~æg~F9àÉ Y9äÉ Co³7JÝ À>„fôi©´áí‡?üa5àÝ‹›ªnnnª-¢ÉHÿ7Ý ¢  mnûÚk¯©hœ6ûÉ0MŒƒ…°/䌡å¤='(Ò‹>—9Xè™BËóìMտ뻾+pÜøi“ÜÚz;mræüʯüŠú›ø–oùG¾ƒ,€}GËÛo¿­–ÕÑf¸ô*s<äA8ا-#[6r’üÔOý”ZVgœ´ñy‘ƒÅ¤CËåÌ@Ú¯å—~é—Ô2$ú.5mŠ–¡hËO}êSêÚØuìÁxì ·‘SåôéÓp®€}ÍÚÚšøë¿þël+zõøÿñ‹þáÎää W£—ñéOZýÿýßÿýꂜ%ùBÑ^ä$)J›¢\ 䨡·~Ñýè7‚êØÓO?­ä4÷9ˆÀÁØwЦ·—/_n´<ˆÞ^D×þôOÿ4öûŠ¡¥:äÈ0Q)ò'¢–íøû$•½!Œ •~¿¯'ö›ŠÌR ¢(m{É'9XÈ‘âßê¥m6Ë>ˆÀÁì3Ì2бßêUäÀ åpe9“cd4)§ ½•ˆ"Wè-EvtJQÚ¶3å‹_ü¢r°<ŽÀÁì#h“Ù˜s…(Ú@œôæ¬"h©ñ¯þÕ¿RŽ•Ø‰ŠÒ¦È9rh?–Ç8X€}‚yƒm ë;WŠ&¡ÍdißÛ âCû«ÐÛÞhùÏW~åWò·:ª…6„6¥mG¶Ð2 r°ÐFÔ6ï½÷žJï ¿A``ö\1oç‰a6ýïÿý¿+‡A{ªÐÛ~Š Mhéõùt>9B ÕBÃìì¬zݲ6¥K2‘S…œ+tЛˆè;sOrà¼ñÆê{݃^Ó ìhÛáaCÑ'ö+M¤ AŽcÇŽ©·g½¦™°¯!Ì›‡ÈQb_gŸG÷ýøÇ?.~ó7Ó¹¿y}³œ>±eG 8XÐЬ¡eC?ù“?ùX¿öK„P E¥Ð[†ÌÒÂ,?¢W7?ÎÎ,H¼Ú¦lO˜Ç 8XZ‚%B-ƒ %p°´€–ÀÁÐ8XZ @Kà`h ,-ƒ %p°´€–ÀÁÐ8XZ @+„øÿ÷ÈDÔñ´8LIEND®B`‚param-2.1.1/doc/comparisons.md000066400000000000000000000167011463636336300162610ustar00rootroot00000000000000# Comparison to other approaches Param was first developed in 2003 for Python 2.1 as part of a long-running brain simulation [project](http://ioam.github.io/topographica), 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](https://github.com/enthought/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-2.1.1/doc/conf.py000066400000000000000000000035231463636336300146770ustar00rootroot00000000000000# -*- coding: utf-8 -*- import param param.parameterized.docstring_signature = False param.parameterized.docstring_describe_params = False from nbsite.shared_conf import * # noqa project = 'param' authors = 'HoloViz developers' copyright_years['start_year'] = '2003' # noqa copyright = copyright_fmt.format(**copyright_years) # noqa description = 'Declarative Python programming using Parameters' version = release = base_version(param.__version__) # noqa nbbuild_cell_timeout = 600 html_static_path += ['_static'] # noqa html_logo = "_static/logo_horizontal.png" html_favicon = "_static/favicon.ico" exclude_patterns = ['governance/**/*.*', 'Promo.ipynb'] html_theme_options = { "github_url": "https://github.com/holoviz/param", "icon_links": [ { 'name': 'Twitter', 'url': 'https://twitter.com/holoviz_org', 'icon': 'fa-brands fa-twitter-square', }, { "name": "Discourse", "url": "https://discourse.holoviz.org/", "icon": "fa-brands fa-discourse", }, { "name": "Discord", "url": "https://discord.gg/AXRHnJU6sP", "icon": "fa-brands fa-discord", }, ], "footer_start": [ "copyright", "last-updated", ], } extensions += [ # noqa 'sphinx_copybutton', 'sphinx.ext.napoleon', 'sphinx.ext.autosummary', 'sphinx_remove_toctrees', 'nbsite.analytics', ] remove_from_toctrees = ["reference/param/generated/*"] nbsite_analytics = { 'goatcounter_holoviz': True, } # 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' myst_heading_anchors = 3 myst_enable_extensions = ["colon_fence"] napoleon_numpy_docstring = True param-2.1.1/doc/developer_guide.md000066400000000000000000000017561463636336300170720ustar00rootroot00000000000000# Developer guide ## Setup The source code for `Param` is hosted on GitHub. To clone the source repository, issue the following command: ```bash git clone https://github.com/holoviz/param.git ``` This will create a `param` directory at your file system location. `Param` relies on `hatch` to manage the project. Follow the [instructions](https://hatch.pypa.io/latest/install/) to install it. Once installed, run the following command to create the *default* environment and activate it, it contains the dependencies required to develop `Param`: ```bash hatch shell ``` ## Testing The simplest way to run the unit tests is to run the following command: ```bash hatch run tests ``` You can also run the examples tests, i.e. check that the notebooks run without any error, with: ```bash hatch run examples ``` ## Documentation building Run the following command to build the documentation: ```bash hatch run docs:build ``` Once completed, the built site can be found in the `builtdocs` folder. param-2.1.1/doc/getting_started.md000066400000000000000000000205651463636336300171160ustar00rootroot00000000000000# 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 ([![defaults version](https://img.shields.io/conda/v/anaconda/param.svg?label=defaults&style=flat&colorB=4488ff)](https://anaconda.org/main/param) [![conda-forge version](https://img.shields.io/conda/v/conda-forge/param.svg?label=conda-forge&colorB=4488ff)](https://anaconda.org/conda-forge/param)) and PyPI ([![PyPI version](https://img.shields.io/pypi/v/param.svg?colorB=cc77dd)](https://pypi.org/project/param/)), and can be installed via: ``` conda install 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 and 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 >>> B(a="four") ValueError: Parameter 'a' must be an integer. >>> o2 = B() >>> 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 How-to guides](https://panel.holoviz.org/how_to/param/index.html) in Panel, and the rest of the [Panel](https://panel.holoviz.org) docs. Have fun making your life better with Param! param-2.1.1/doc/governance/000077500000000000000000000000001463636336300155245ustar00rootroot00000000000000param-2.1.1/doc/governance/project-docs/000077500000000000000000000000001463636336300201205ustar00rootroot00000000000000param-2.1.1/doc/governance/project-docs/CONTRIBUTING.md000066400000000000000000000005031463636336300223470ustar00rootroot00000000000000# Contributing For the contributing policy, see [HoloViz/HoloViz - CONTRIBUTING.md](https://github.com/holoviz/holoviz/blob/param-gov/doc/governance/project-docs/CONTRIBUTING.md). The Param Project’s equivalently named documents take precedence over any external materials referenced within this linked document above. param-2.1.1/doc/governance/project-docs/GOVERNANCE.md000066400000000000000000000010501463636336300220650ustar00rootroot00000000000000# Governance Policy The "Project" is herein defined as the activities related to this specific GitHub repository [`Param`](https://github.com/holoviz/param), within the `HoloViz` GitHub Organization. This Project adopts the governance specified by all of the numbered sections of [HoloViz/HoloViz - GOVERNANCE.md](https://github.com/holoviz/holoviz/blob/param-gov/doc/governance/project-docs/GOVERNANCE.md). The Param Project’s equivalently named documents take precedence over any external materials referenced within this linked document above. param-2.1.1/doc/governance/project-docs/LICENSE.md000066400000000000000000000001671463636336300215300ustar00rootroot00000000000000# License For the license, see [HoloViz/Param - LICENSE.txt](https://github.com/holoviz/param/blob/main/LICENSE.txt). param-2.1.1/doc/governance/project-docs/MEMBERS.md000066400000000000000000000011651463636336300215370ustar00rootroot00000000000000# Maintainers For member policy, see the description at the top of [HoloViz/HoloViz - MEMBERS.md](https://github.com/holoviz/holoviz/blob/param-gov/doc/governance/project-docs/MEMBERS.md) The Param Project’s equivalently named documents take precedence over any external materials referenced within this linked document above. | **NAME** | **Role** | **GitHub Handle** | | --- | --- | --- | | James Bednar | Project Director | [jbednar](https://github.com/jbednar) | | Jean-Luc Stevens | Maintainer | [jlstevens](https://github.com/jlstevens) | | Philipp Rudiger | Maintainer | [philippjfr](https://github.com/philippjfr) | param-2.1.1/doc/index.md000066400000000000000000000045331463636336300150330ustar00rootroot00000000000000# 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 --- Getting Started User Guide API Releases Upgrade Guide Comparisons Roadmap Developer Guide About ``` param-2.1.1/doc/reference/000077500000000000000000000000001463636336300153335ustar00rootroot00000000000000param-2.1.1/doc/reference/index.md000066400000000000000000000003541463636336300167660ustar00rootroot00000000000000# API reference Param effectively installs two packages `param` and `numbergen` importable under the same names. This section documents their public API. ```{toctree} --- maxdepth: 2 --- Param Numbergen ``` param-2.1.1/doc/reference/numbergen.md000066400000000000000000000001761463636336300176430ustar00rootroot00000000000000# Numbergen API reference ```{eval-rst} .. automodule:: numbergen :members: :inherited-members: :show-inheritance: ``` param-2.1.1/doc/reference/param/000077500000000000000000000000001463636336300164335ustar00rootroot00000000000000param-2.1.1/doc/reference/param/index.md000066400000000000000000000016041463636336300200650ustar00rootroot00000000000000# Param API reference ## Reactive API ```{include} reactive.md :start-line: 2 ``` ## Parameterized objects ```{include} parameterized_objects.md :start-line: 2 ``` ## Parameterized helpers ```{include} parameterized_helpers.md :start-line: 2 ``` ## Parameters ```{include} parameters.md :start-line: 2 ``` ## Parameter helpers ```{include} parameter_helpers.md :start-line: 2 ``` ## `.param` namespace ```{include} param_namespace.md :start-line: 2 ``` ## Logging ```{include} logging.md :start-line: 2 ``` ## Serialization ```{include} serialization.md :start-line: 2 ``` ```{toctree} --- maxdepth: 2 hidden: true --- Reactive API Parameterized objects Parameterized helpers Parameters Parameter helpers Param namespace Logging Serialization ``` param-2.1.1/doc/reference/param/logging.md000066400000000000000000000002131463636336300203770ustar00rootroot00000000000000# Logging ```{eval-rst} .. autosummary:: :toctree: generated/ param.parameterized.get_logger param.parameterized.logging_level ``` param-2.1.1/doc/reference/param/param_namespace.md000066400000000000000000000022161463636336300220720ustar00rootroot00000000000000# `.param` namespace These methods and properties are available under the `.param` namespace of {py:class}`Parameterized` classes and instances. ```{eval-rst} .. currentmodule:: param.parameterized ``` ```{eval-rst} .. autosummary:: :toctree: generated/ ~Parameters.add_parameter ~Parameters.debug ~Parameters.defaults ~Parameters.deserialize_parameters ~Parameters.deserialize_value ~Parameters.force_new_dynamic_value ~Parameters.get_param_values ~Parameters.get_value_generator ~Parameters.inspect_value ~Parameters.log ~Parameters.message ~Parameters.method_dependencies ~Parameters.objects ~Parameters.outputs ~Parameters.params ~Parameters.params_depended_on ~Parameters.pprint ~Parameters.print_param_defaults ~Parameters.print_param_values ~Parameters.schema ~Parameters.set_default ~Parameters.set_dynamic_time_fn ~Parameters.set_param ~Parameters.serialize_parameters ~Parameters.serialize_value ~Parameters.trigger ~Parameters.unwatch ~Parameters.update ~Parameters.values ~Parameters.verbose ~Parameters.warning ~Parameters.watch ~Parameters.watch_values ~Parameters.watchers ``` param-2.1.1/doc/reference/param/parameter_helpers.md000066400000000000000000000003201463636336300224520ustar00rootroot00000000000000# Parameter helpers ```{eval-rst} .. currentmodule:: param ``` ```{eval-rst} .. autosummary:: :toctree: generated/ get_soft_bounds guess_bounds guess_param_types param_union ParamOverrides ``` param-2.1.1/doc/reference/param/parameterized_helpers.md000066400000000000000000000007041463636336300233340ustar00rootroot00000000000000# Parameterized helpers ```{eval-rst} .. currentmodule:: param ``` ```{eval-rst} .. autosummary:: :toctree: generated/ param.parameterized.Event param.parameterized.Watcher batch_watch param.parameterized.batch_call_watchers concrete_descendents depends discard_events edit_constant output param.parameterized.register_reference_transform param.parameterized.resolve_ref param.parameterized.resolve_value script_repr ``` param-2.1.1/doc/reference/param/parameterized_objects.md000066400000000000000000000002711463636336300233220ustar00rootroot00000000000000# Parameterized objects ```{eval-rst} .. currentmodule:: param ``` ```{eval-rst} .. autosummary:: :toctree: generated/ :nosignatures: Parameterized ParameterizedFunction ``` param-2.1.1/doc/reference/param/parameters.md000066400000000000000000000010151463636336300211150ustar00rootroot00000000000000# Parameters ```{eval-rst} .. currentmodule:: param ``` ```{eval-rst} .. autosummary:: :toctree: generated/ :nosignatures: Parameter String Bytes Color Boolean Event Dynamic Number Integer Magnitude Tuple NumericTuple XYCoordinates Range Date DateRange CalendarDate CalendarDateRange List HookList Path Filename Foldername Selector FileSelector ListSelector MultiFileSelector ClassSelector Dict Array Series DataFrame Callable Action Composite ``` param-2.1.1/doc/reference/param/reactive.md000066400000000000000000000011231463636336300205540ustar00rootroot00000000000000## Reactive API ```{eval-rst} .. currentmodule:: param ``` ```{eval-rst} .. autosummary:: :toctree: generated/ bind rx ``` These methods and properties are available under the `.rx` namespace of reactive expressions ({py:class}`rx`). ```{eval-rst} .. currentmodule:: param.reactive ``` ```{eval-rst} .. autosummary:: :toctree: generated/ ~reactive_ops.bool ~reactive_ops.in_ ~reactive_ops.is_ ~reactive_ops.is_not ~reactive_ops.len ~reactive_ops.pipe ~reactive_ops.updating ~reactive_ops.when ~reactive_ops.where ~reactive_ops.value ~reactive_ops.watch ``` param-2.1.1/doc/reference/param/serialization.md000066400000000000000000000001431463636336300216300ustar00rootroot00000000000000# Serialization ```{eval-rst} .. automodule :: param.serializer :members: :undoc-members: ``` param-2.1.1/doc/releases.md000066400000000000000000001513331463636336300155300ustar00rootroot00000000000000# Releases ## Version 2.1.1 Date: 2024-06-25 This minor release contains bug fixes for reactive expressions and a few minor documentation improvements. Thanks to @jrycw for their first contribution! And to @ahuang11, @maximelt, and @philippjfr for their continued maintenance and development efforts. Bug fixes: - Ensure `rx._callback` resolves accessors ([#949](https://github.com/holoviz/param/pull/949)) - Ensure refs can be updated by watcher of the same parameter ([#929](https://github.com/holoviz/param/pull/929)) - Recursively resolve references on args and kwargs passed to a reactive operation ([#944](https://github.com/holoviz/param/pull/944)) - Only override the name of a Parameterized instance on Parameter instantiation when `instantiate=True` ([#938](https://github.com/holoviz/param/pull/938)) Documentation: - Various minor documentation improvements ([#933](https://github.com/holoviz/param/pull/933), [#935](https://github.com/holoviz/param/pull/935), [#941](https://github.com/holoviz/param/pull/941), [#947](https://github.com/holoviz/param/pull/947)) Maintenance: - Fix `test_reactive_logic_unary_ops` on Python 3.12 ([#946](https://github.com/holoviz/param/pull/946)) [*Full Changelog*](https://github.com/holoviz/param/compare/v2.1.0...v2.1.1) ## Version 2.1.0 Date: 2024-03-22 This minor release focuses on improving reactive expressions and support for asynchronous (and synchronous) generators. Many thanks to @maximelt, @Hoxbro and @philippjfr for their continued maintenance and development efforts. Enhancements: - Improvements for synchronous and asychronous generators ([#908](https://github.com/holoviz/param/pull/908)) - Additions to the .rx namespace including `and_`, `bool`, `map`, `not_`, `or_` and `updating` ([#906](https://github.com/holoviz/param/pull/906)) - Add support for adding asynchronous watcher to `rx` ([#917](https://github.com/holoviz/param/pull/917)) - Make it possible to resolve reactive expressions recursively with `.rx.resolve` ([#918](https://github.com/holoviz/param/pull/918)) - Add support for async and generator functions in `.rx.pipe` ([#924](https://github.com/holoviz/param/pull/924)) Bug fixes: - Ensure that `.param.update` context manager restore refs ([#915](https://github.com/holoviz/param/pull/915)) - Avoid overeager root invalidation on `rx` leading to unnecessary evaluation ([#919](https://github.com/holoviz/param/pull/919)) Deprecations: - Passing positional arguments to `Parameter` now raises a `ParamDeprecationWarning` ([#921](https://github.com/holoviz/param/pull/921)) [*Full Changelog*](https://github.com/holoviz/param/compare/v2.0.2...v2.1.0) ## Version 2.0.2 Date: 2024-01-17 This patch release fixes a few bugs and introduces a performance enhancement. Many thanks to @alfredocarella for their first contribution, and to the maintainers @maximlt and @philippjfr for contributing to this release. Optimization: - Minor optimizations in hot codepaths accessing class parameters ([#893](https://github.com/holoviz/param/pull/893)) Bug fixes: - Unpack partial callables in `iscoroutinefunction` ([#894](https://github.com/holoviz/param/pull/894)) - Fix building Param with `setuptools-scm<7` ([#903](https://github.com/holoviz/param/pull/903)) Documentation: - Replace *Google Analytics* with *GoatCounter* ([#895](https://github.com/holoviz/param/pull/895)) - Fix a typo in `Outputs.ipynb` ([#892](https://github.com/holoviz/param/pull/892)) [*Full Changelog*](https://github.com/holoviz/param/compare/v2.0.1...v2.0.2) ## Version 2.0.1 Date: 2023-11-08 This minor release fixes a number of bugs, including a regression introduced by the replacement of the build backend (`setuptools` for `hatchling`) which led to the `doc` folder being wrongly packaged. Many thanks to @SultanOrazbayev for their first contribution, to @musicinmybrain for spotting the regression and submitting fixes, and to the maintainers @Hoxbro, @jbednar and @maximlt for contributing to this release. Bug fixes: - Do not install `doc` folder in *site-packages* ([#878](https://github.com/holoviz/param/pull/878)) - Drop the `feather-format` test dependency ([#879](https://github.com/holoviz/param/pull/879)) - Add `tables` to the `tests-deser` extra ([#880](https://github.com/holoviz/param/pull/880)) - Fix `_state_push` and `_state_pop` ([#884](https://github.com/holoviz/param/pull/884)) - `version.py`: new process should not create a window on Windows ([#882](https://github.com/holoviz/param/pull/882), [#886](https://github.com/holoviz/param/pull/886)) - Don't import `setuptools_scm` if the `.git` folder doesn't exist ([#885](https://github.com/holoviz/param/pull/885)) Documentation: - Add migration guide to Param 2.0 ([#883](https://github.com/holoviz/param/pull/883)) - Update Parameter API reference ([#881](https://github.com/holoviz/param/pull/881)) [*Full Changelog*](https://github.com/holoviz/param/compare/v2.0.0...v2.0.1) ## Version 2.0.0 Date: 2023-10-24 20 years after its creation, Param has reached version 2.0! Can you guess when Param 3.0 will be released? Param 2.0 is a major new release available for Python 3.8 and above, significantly streamlining, simplifying, and improving the Param API. Many long-supported but also long-obsolete functions, methods, and usages will now warn loudly so that you can make sure your code is only using the fully supported and safe current approaches. Because upgrading to Param 2 is likely to reveal compatibility issues with older codebases, new releases in the 1.x series are expected to continue for some time, focused on compatibility with the ecosystem rather than adding new features. Thus you can keep using Param 1.x with your older code, but Param 2 is the future! We would like to thank @minimav for their first contribution, and @droumis, @Hoxbro, @jbednar, @maximlt, @philippjfr and @sdrobert for their contributions. We would also like to thank @ceball, who made the first plans for Param 2.0 quite a few years ago, and we are glad to be delivering on them at last! ### Major enhancements and features - Parameter slot values are now all inherited correctly across a hierarchy of Parameterized classes, making their behavior much clearer and more consistent. Let's say we have class `B` being a subclass of `A`, itself being a subclass of `param.Parameterized`. If `A` defines `x = Number(1, bounds=(0, 10))` and `B` defines `x = Number(2)`, `B.param['x'].bounds` is now going to be inherited from `A` and equal to `(0, 10)` as you would expect. Parameterized classes have always supported inheritance, but the previous mechanism was based on using `None` to indicate which values should be inherited, which was highly problematic because `None` was also a valid value for many slots. All Parameter slot signatures now default to the new `Undefined` sentinel, finally allowing `None` to be inherited where appropriate. ([#605](https://github.com/holoviz/param/pull/605), [#771](https://github.com/holoviz/param/pull/771), [#791](https://github.com/holoviz/param/pull/791), [#874](https://github.com/holoviz/param/pull/874)) - The `objects` slot of a Selector was previously highly confusing, because it accepted either a dictionary or a list for initialization but then was accessible only as a list, making it difficult to watch or update the objects. There is now a `ListProxy` wrapper around `Selector.objects` (with forward and backward compatibility) to easily update `objects` and watch `objects` updates ([#598](https://github.com/holoviz/param/pull/598), [#825](https://github.com/holoviz/param/pull/825)) - Parameterized classes and instances now have a rich HTML representation that is displayed automatically in a Jupyter/IPython notebook. For a class or instance `p`, just return `p.param` in a notebook cell to see a table of all the Parameters of the class/instance, their state, type, and range, plus the docstring on hover. It is likely we will improve the content and design of this repr based on feedback, so please let us know what you think! ([#425](https://github.com/holoviz/param/pull/425), [#781](https://github.com/holoviz/param/pull/781), [#821](https://github.com/holoviz/param/pull/821), [#831](https://github.com/holoviz/param/pull/831)) - Parameters have all gained the `allow_refs` and `nested_refs` attributes, bringing an exceptionally useful feature that was available in Panel since version 1.2 to Param. Declaring a Parameter with `allow_refs=True` (`False` by default) allows setting this Parameter value with a *reference* to automatically mirror the value of the reference. Supported references include class/instance Parameter objects, functions/methods decorated with `param.depends`, reactive functions and expressions, asynchronous generators and custom objects transformed into a valid reference with a hook registered with `param.parameterized.register_reference_transform`. `nested_refs` indicate whether references should be resolved even when they are nested inside a container ([#843](https://github.com/holoviz/param/pull/843), [#845](https://github.com/holoviz/param/pull/845), [#849](https://github.com/holoviz/param/pull/849), [#865](https://github.com/holoviz/param/pull/865), [#862](https://github.com/holoviz/param/pull/862), [#876](https://github.com/holoviz/param/pull/876)) - Experimental new `rx` reactive expressions: Param is widely used for building web apps in the HoloViz ecosystem, where packages have added various mechanisms for dynamic updates (e.g. `pn.bind` and `pn.depends` in Panel, and `.interactive` in hvPlot). These mechanisms were already built on Param and can be used far more widely than just in those packages, so that functionality has now been generalized, streamlined, and moved into Param. Nearly any Python expression can now be made reactive with `param.rx()`, at which point it will collect and be able to replay any operations (e.g. method calls) performed on them. This reactive programming approach lets you take just about any existing Python workflow and replace attributes with widgets or other reactive values, creating an app with fine-grained user control without having to design callbacks, event handlers, or any other complex logic! `rx` support is still experimental while we get feedback about the API, packaging, and documentation, but it's fully ready to try out and give us suggestions! ([#460](https://github.com/holoviz/param/pull/460), [#842](https://github.com/holoviz/param/pull/842), [#841](https://github.com/holoviz/param/pull/841), [#844](https://github.com/holoviz/param/pull/844), [#846](https://github.com/holoviz/param/pull/846), [#847](https://github.com/holoviz/param/pull/847), [#850](https://github.com/holoviz/param/pull/850), [#851](https://github.com/holoviz/param/pull/851), [#856](https://github.com/holoviz/param/pull/856), [#860](https://github.com/holoviz/param/pull/860), [#854](https://github.com/holoviz/param/pull/854), [#859](https://github.com/holoviz/param/pull/859), [#858](https://github.com/holoviz/param/pull/858), [#873](https://github.com/holoviz/param/pull/873)) ### Enhancements - Parameter slot values that are set to mutable containers (e.g. `Selector(objects=a_list)`) will now be shallow-copied on instantiation, so that the container is no longer confusingly shared between the class and its subclasses and instances ([#826](https://github.com/holoviz/param/pull/826)) - To further clean up the Parameterized namespace (first started in version 1.7.0), the remaining private attributes haven been collected under two private namespaces `_param__private` and `_param__parameters` ([#766](https://github.com/holoviz/param/pull/766), [#790](https://github.com/holoviz/param/pull/790)) - You can now use `.param.update` as a context manager for applying temporary updates ([#779](https://github.com/holoviz/param/pull/779)) - The `name` Parameter has always had special behavior dating to its use in labeling objects in a GUI context, but this behavior is now able to be overriden at the class and instance level ([#740](https://github.com/holoviz/param/pull/740)) - Improved Parameter signatures for static and dynamic code analysis ([#742](https://github.com/holoviz/param/pull/742)) - Removed inferred Parameterized docstring signature and add basic `__signature__` support ([#802](https://github.com/holoviz/param/pull/802)) - For speed, only generate the Parameter docstring in an IPython context ([#774](https://github.com/holoviz/param/pull/774)) - Improve Parameter validation error messages ([#808](https://github.com/holoviz/param/pull/808)) - Support for deserialization of file types into `Array` and `DataFrame` ([#482](https://github.com/holoviz/param/pull/482)) - `Integer` now accepts `numpy.integer` values ([#735](https://github.com/holoviz/param/pull/735)) - `Range` now does stricter validation of the slot values ([#725](https://github.com/holoviz/param/pull/725), [#824](https://github.com/holoviz/param/pull/824)) - `Path` now has `check_exists` attribute, leading it to raise an error if `path` is not found on parameter instantiation ([#800](https://github.com/holoviz/param/pull/800)) - Add top-level `__all__` and move Parameter classes to `parameters.py` ([#853](https://github.com/holoviz/param/pull/853)) ### Bug fixes - Allow type change for `DateRange` and `Date` ([#733](https://github.com/holoviz/param/pull/733)) - Ensure class watchers are not inherited by instance parameter ([#833](https://github.com/holoviz/param/pull/833)) - Fix multi-level indirection in Parameters access ([#840](https://github.com/holoviz/param/pull/840)) - Ensure non-function types are not resolved as empty function declarations ([#753](https://github.com/holoviz/param/pull/753)) - Fix watchers support when the Parameterized instance is falsy ([#769](https://github.com/holoviz/param/pull/769)) - Fix depending on the method of a sub-parameter object ([#765](https://github.com/holoviz/param/pull/765)) - Raise an error on bad non-watched references ([#777](https://github.com/holoviz/param/pull/777)) - Ensure that the root dependency can be resolved, and error otherwise ([#813](https://github.com/holoviz/param/pull/813)) - Fix basic pickling ([#783](https://github.com/holoviz/param/pull/783), [#792](https://github.com/holoviz/param/pull/792)) - Validate that `self` is present in the `__init__` signature of a Parameterized class ([#786](https://github.com/holoviz/param/pull/786)) - No longer force `instantiate` to True when `constant` is True ([#776](https://github.com/holoviz/param/pull/776)) - Instantiate default Parameter values based on all the Parameters available ([#798](https://github.com/holoviz/param/pull/798)) - `Array`: fix `param.pprint` ([#795](https://github.com/holoviz/param/pull/795)) - `Array`: don't hard-code `allow_None` to `True` ([#726](https://github.com/holoviz/param/pull/726)) - `Boolean`: validate the default type ([#722](https://github.com/holoviz/param/pull/722)) - `FileSelector`: made more consistent with `Selector` by defaulting to the first globbed path ([#801](https://github.com/holoviz/param/pull/801)) - `FileSelector`: ensure path separators are consistent on Windows ([#805](https://github.com/holoviz/param/pull/805)) - `Path`: raise a `ValueError` if set to `None` while not allowed ([#799](https://github.com/holoviz/param/pull/799)) - `Selector`: populate `objects` when `check_on_set` is False and `default` is not in `objects` ([#794](https://github.com/holoviz/param/pull/794), [#817](https://github.com/holoviz/param/pull/817)) - `File/MultiFileSelector`: updating `path` updates `objects` ([#814](https://github.com/holoviz/param/pull/814)) ### Documentation - Build the site with `sphinx` directly and refactor the API reference ([#810](https://github.com/holoviz/param/pull/810)) - Update to the latest version of `pydata-sphinx-theme` ([#752](https://github.com/holoviz/param/pull/752)) - Update to Google Analytics 4 ([#758](https://github.com/holoviz/param/pull/758)) - Fix minor errors in the Getting Started ([#787](https://github.com/holoviz/param/pull/787)) - Add OpenCollective sponsor link on the repository page ([#811](https://github.com/holoviz/param/pull/811)) ### Infrastructure - Increase the test suite coverage ([#716](https://github.com/holoviz/param/pull/716), [#717](https://github.com/holoviz/param/pull/717), [#719](https://github.com/holoviz/param/pull/719), [#720](https://github.com/holoviz/param/pull/720), [#739](https://github.com/holoviz/param/pull/739), [#775](https://github.com/holoviz/param/pull/775), [#778](https://github.com/holoviz/param/pull/778)) - Turn warnings into exceptions in the test suite ([#738](https://github.com/holoviz/param/pull/738)) - Add notebook smoke tests ([#750](https://github.com/holoviz/param/pull/750)) - Upgrades to leverage `hatch`, `pyproject.toml` and `pre-commit` ([#749](https://github.com/holoviz/param/pull/749), [#772](https://github.com/holoviz/param/pull/772)) - Add basic benchmark suite using `asv` ([#788](https://github.com/holoviz/param/pull/788)) - Reduce the number of tested Python versions ([#732](https://github.com/holoviz/param/pull/732)) - Run the tests with Python 3.12 ([#863](https://github.com/holoviz/param/pull/863)) ### Compatibility - Drop support for Python 2.7, 3.6, and 3.7 and upgrade the code base accordingly ([#741](https://github.com/holoviz/param/pull/741), [#784](https://github.com/holoviz/param/pull/784)) ### Breaking changes - While it's a major improvement to the definition of Parameters, properly inheriting Parameter slots can result in some Parameter slot values being different in Param 2, because Param 1 was sometimes silently not inheriting slot values. - User-defined `Parameter` classes should be updated to use `Undefined` as the formal default for any new slots, with the actual default defined on the new `_slot_defaults` dictionary. Otherwise, any new slot will fail to support inheritance, even if it was set to `None`, which would previously support inheritance. - `Parameterized` methods that were deprecated since Param 1.7.0 have finally been removed. These are now mostly available on the `.param` namespace ([#592](https://github.com/holoviz/param/pull/592)) - No longer supports setting non-Parameter class attributes during initialization, and no longer warns when setting non-Parameter class attributes directly ([#729](https://github.com/holoviz/param/pull/729)) - `instance.param.watchers` no longer returns the transient dict of watchers but instead returns the instance watchers, as the now deprecated `instance._param_watchers` ([#797](https://github.com/holoviz/param/pull/797)) - Removed deprecated `Parameterized.pprint`, `Parameterized._pprint`, `Parameterized.script_repr`, `ParameterizedFunction.script_repr` ([#767](https://github.com/holoviz/param/pull/767)) - Removed `Time.next` method needed only for Python 2, and moved `Parameterized.state_pop` and `Parameterized.state_push` to the `.param` namespace ([#767](https://github.com/holoviz/param/pull/767)) - Some removals were considered harmless and thus implemented immediately without a deprecation period: - Removed unused `bounds` slot from `Boolean` and `Event` ([#744](https://github.com/holoviz/param/pull/744), [#755](https://github.com/holoviz/param/pull/755)) - Removed private Parameter `_internal_name` slot ([#796](https://github.com/holoviz/param/pull/796)) ### Deprecations This section lists functionality that is expected to be removed sometime in the next couple of 2.x releases, so if you use Param 2, please take care of these warnings as soon as you encounter them, and certainly before you upgrade to the next release! Param 2.0 adds a validation step of the *default* value of a Parameter after the inheritance mechanism has completed if its type has changed (e.g. `x` in class `A` is a `Number` and in class `B(A)` is an `Integer`) or one of its slot values has changed ([#812](https://github.com/holoviz/param/pull/812), [#820](https://github.com/holoviz/param/pull/820), [#857](https://github.com/holoviz/param/pull/857)). We have decided to only emit a warning when this validation fails to make your life easier when upgrading your code from Param 1 to 2, as the validation is performed on class creation which means that any validation error breaks importing your code. You should definitely take care of these warnings, they indicate a Parameter is in an invalid state! We continue to clean up Param's API ([#734](https://github.com/holoviz/param/pull/734), [#751](https://github.com/holoviz/param/pull/751), [#768](https://github.com/holoviz/param/pull/768), [#797](https://github.com/holoviz/param/pull/797), [#834](https://github.com/holoviz/param/pull/834), [#838](https://github.com/holoviz/param/pull/838)) but have decided to do it in a gentle way, emitting deprecation warnings for a period of time before proceeding with removals. You will find below the complete list of deprecation warnings added in Param 2.0. - Parameter signature: - Instantiating most parameters with positional arguments beyond `default` is deprecated: - `String('prefix-test', '^prefix')`: deprecated! - `String('prefix-test', regex='^prefix')`: OK - `String(default='prefix-test', regex='^prefix')`: OK - For `Selector` parameters that accept `objects` as first positional argument, and `ClassSelector` parameters that accept `class_` as first positional argument, passing any argument by position is deprecated: - `Selector([1, 2])`: deprecated! - `Selector(objects=[1, 2])`: OK - `ClassSelector((str, int))`: deprecated! - `ClassSelector(class_=(str, int))`: OK - It's possible that in the future the signature of these two parameters will be aligned with the other parameters to accept `default` as first and only positional argument, but for now please use an explicit keyword so that your code will be compatible with all versions. - Parameter slots: - `List._class`: use instead `item_type`. - `Number.set_hook`: no replacement - `param.__init__` module: - `param.produce_value`: no replacement - `param.as_unicode`: no replacement - `param.is_ordered_dict`: no replacement - `param.is_ordered_dict`: no replacement - `param.hashable`: no replacement - `param.named_objs`: no replacement - `param.normalize_path`: no replacement - `param.abbreviate_paths`: no replacement - `param.parameterized` module: - `param.parameterized.all_equal`: no replacement - `param.parameterized.add_metaclass`: no replacement - `param.parameterized.batch_watch`: use instead `batch_call_watchers` - `param.parameterized.recursive_repr`: no replacement - `param.parameterized.overridable_property`: no replacement - Parameterized `.param` namespace; many of these methods have been deprecated since version 1.12.0, however, this was just announced in the release notes and we realised many users missed them, sometimes even us included! They now all emit deprecation warnings when executed and are clearly marked as deprecated in the API reference: - `.param.set_default`: use instead `for k,v in p.param.objects().items(): print(f"{p.__class__.name}.{k}={repr(v.default)}")` - `.param._add_parameter`: use instead `.param.add_parameter` - `.param.params`: use instead `.param.values()` or `.param['param']` - `.param.set_param`: use instead `.param.update` - `.param.get_param_values`: use instead `.param.values().items()` (or `.param.values()` for the common case of `dict(....param.get_param_values())`) - `.param.params_depended_on`: use instead `.param.method_dependencies` - `.param.defaults`: use instead `{k:v.default for k,v in p.param.objects().items()}` - `.param.print_param_defaults`: use instead `for k,v in p.param.objects().items(): print(f"{p.__class__.name}.{k}={repr(v.default)}")` - `.param.print_param_values`: use instead `for k,v in p.param.objects().items(): print(f"{p.__class__.name}.{k}={repr(v.default)}")` - `.param.message`: use instead `.param.log(param.MESSAGE, ...)` - `.param.verbose`: use instead `.param.log(param.VERBOSE, ...)` - `.param.debug`: use instead `.param.log(param.DEBUG, ...)` - Running unsafe operations **during Parameterized instance initialization**, instead run these operations after having called `super().__init__(**params)`: - `instance.param.objects(instance=True)` - `instance.param.trigger("")` - `instance.param.watch(callback, "")` - Parameterized namespace: - `instance._param_watchers` (getter and setter): use instead the property `inst.param.watchers` [*Full Changelog*](https://github.com/holoviz/param/compare/v1.13.0...v2.0.0) ## Version 1.13.0 Date: 2023-03-14 The `1.13.0` is the last release of Param before the 2.0 release. However, Param 1.13 is meant to receive long-term support; security patches and fixes to critical bugs are planned to be backported to the 1.13.x series. This release includes a new `Bytes` *Parameter* and a few important bug fixes. This release is also marked by the adoption of a formal project governance, ensuring Param's future as a healthy open-source project. Many thanks to @ovidner and @droumis for their first contributions! And to @maximlt, @Hoxbro, @jlstevens, @philippjfr and @jbednar for their continuing support to fixing and improving Param. Bug fixes: * Fix copying when having watchers on e.g. bounds on inherited Parameter types ([#675](https://github.com/holoviz/param/pull/675)) * Allow JSON serialization to work with `json.dumps` ([#655](https://github.com/holoviz/param/pull/655)) * `ListSelector` restricted to `list` type objects ([#531](https://github.com/holoviz/param/pull/531)) * Fix `depends` async wrapper ([#684](https://github.com/holoviz/param/pull/684)) * Allow named colors to be any case ([#711](https://github.com/holoviz/param/pull/711)) New features: * Add Bytes parameter ([#542](https://github.com/holoviz/param/pull/542)) Documentation: * Fix param module link ([#682](https://github.com/holoviz/param/pull/682)) Project governance: * Create initial project governance docs ([#674](https://github.com/holoviz/param/pull/674)) Maintenance: * Rename `master` branch to `main` ([#672](https://github.com/holoviz/param/pull/672)) * Add more tests ([#710](https://github.com/holoviz/param/pull/710)) * Various CI related fixes ([#680](https://github.com/holoviz/param/pull/680), [#683](https://github.com/holoviz/param/pull/683) and [#709](https://github.com/holoviz/param/pull/709)) ## 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/holoviz-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/holoviz-dev/autover/pull/39) * Allow develop install to work when repository is dirty (https://github.com/holoviz-dev/autover/pull/41) * Fixed failure to report dirty when commit count is 0 (https://github.com/holoviz-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-dev/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-2.1.1/doc/roadmap.md000066400000000000000000000062251463636336300153470ustar00rootroot00000000000000# 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-2.1.1/doc/upgrade_guide.md000066400000000000000000000416121463636336300165270ustar00rootroot00000000000000# Upgrade guide Welcome to the Upgrade Guide for Param! When we make backward-incompatible changes, we will provide a detailed guide here on how you can update your code to be compatible with the latest changes. ## Version 2.0 ### Breaking changes #### Parameter attributes inheritance `Parameter`s have attributes like `default`, `doc`, `bounds`, etc. When redefining a `Parameter` in a subclass, attributes not specified in the subclass are meant to be inherited from the superclass. Inheritance is not a new feature; it's a core design of Param. You can see in the example below that the `doc` attribute of the `x` Parameter on the class `B` has been inherited from `A`: ```python class A(param.Parameterized): x = param.Number(default=5, doc='The value of x') class B(A): x = param.Number(default=10) print(B().param['x'].doc) # The value of x ``` However, throughout the Param 1.x series, inheritance was broken in some subtle ways! The issues traced back to using `None` as the specific sentinel value for allowing inheritance, when `None` is also often a valid `Parameter` value. Let's have a look at two examples that have been fixed in Param 2.0. In this first example we want to re-set the default value of `x` to `None` in a subclass. As you can see, the explicit `None` value we provided in `B` was discarded entirely in versions before Param 2.0, while the new version now correctly re-assigns the value to `None`: ```python class A(param.Parameterized): x = param.Number(default=5.0) class B(A): x = param.Number(default=None) print(B().x) # Param < 2: 5.0 :( # Param 2.0: None :) ``` In this second example we want to narrow the bounds `x` in the subclass `B`. Because no default value is specified for `x` in `B`, you might reasonably have expected the default value of `5.0` to be inherited from `A`. However, prior to Param 2.0, instead the default value of the `Number` `Parameter` (`0.0`) is used for `B.x` instead of what was declared in `A`: ```python class A(param.Parameterized): x = param.Number(default=5.0, bounds=(-10, 10)) class B(A): x = param.Number(bounds=(0, 10)) print(B().x) # Param < 2: 0.0 :( # Param 2.0: 5.0 :) ``` These are just two of the most common cases where inheritance changes can affect your code, but there are many other cases, affecting any attribute where `None` is a legal value. The new behavior should be much more predictable and intuitive, avoiding subtle cases where your code would previously have behaved inappropriately without necessarily having any obvious error. Fixing this was already enough to be worth the major bump to Param 2.0! Because of all our work on making sure that Parameter attributes are properly inherited, we've realized that you can now more easily end up with a Parameter whose state, i.e. the combination of all of its attribute values, is invalid according to the Parameter declarations. Therefore, once class inheritance has completed as your classes get defined, we have added validation that the Parameter's *default* value is an allowed value. For now Param 2.0 is only emitting a warning when this validation fails, to avoid breaking your code on imports! If you do see these warnings, you should address them immediately by adding a legal default value or by relaxing one of the declared constraints on the value, because the warnings indicate a Parameter is in an invalid state. In the following example, the `bounds` of `x` are narrowed down in `B` without updating the `default` value that is inherited from `A.x` of `-1`, making the Parameter `x` on `B` in an invalid state: ```python class A(param.Parameterized): x = param.Number(default=-1, bounds=(-5, 5)) class B(A): x = param.Number(bounds=(0, 5)) # Param < 2: No warning and B().x == 0.0 # Param 2.0: Warning and B().x == -1 / ParamFutureWarning: Number parameter 'B.x' failed to validate its default value on class creation, this is going to raise an error in the future. The Parameter is defined with attributes which when combined with attributes inherited from its parent classes (A) make it invalid. Please fix the Parameter attributes. ``` Param doesn't do any more validation that what was just explained when creating subclasses that override `Parameter`s. For instance, it doesn't programmatically enforce that a subclass should define a `Parameter` that is of the same type or a subtype of the `Parameter` defined in a super class. However, the best approach for you to follow is clearly to make sure that `Parameter`s defined in subclasses satisfy the ["is-a"](https://en.wikipedia.org/wiki/Is-a) relationship. I.e., for an instance `b` of subclass `B` and an instance `a` of parent class `A`, `b` should always be usable where `a` is. That's the general rule for inheritance, and for `Parameter`s it's specifically that any parameter value accepted by `B` should be a valid setting for `A`. That way any code written for `A`, including code that handles the various values of any attribute of `A`, can be trusted to work properly with any instance of `B`. In more concrete terms, it means a `Parameter` of a subclass should not increase the range of accepted values, it should instead narrow it down, i.e. by specifying a subtype (e.g. `param.Number` to `param.Integer`) or restricting the range of accepted values (e.g. smaller `bounds`). #### Defining custom `Parameter`s To implement `Parameter` attribute inheritance correctly as described above, we have had to make some changes to how new `Parameter` classes are declared. While the custom `Parameter`s you previously wrote for Param before 2.0 should continue to work as they always have, unless you update their definitions, they could continue to suffer from the attribute inheritance issues that have been fixed within Param itself. We now recommend custom `Parameter`s to be written following the pattern we have adopted internally. In particular, we have: - introduced the `param.parameterized.Undefined` sentinel object that is used as the default value of the `Parameter` parameters in its `__init__()` method - introduced the `_slot_defaults` class attribute to declare the actual default values of the `Parameter` parameters, e.g. in the example below the default value of `some_attribute` is declared to be `10` - leveraged `@typing.overload` to expose the real default values to static type checkers and IDEs like VSCode All put together, this is now how `Parameter`s are structured internally and how we recommend writing custom `Parameter` classes: ```python class CustomParameter(Parameter): __slots__ = ['some_attribute'] _slot_defaults = _dict_update(Parameter._slot_defaults, default=None, some_attribute=10 ) @typing.overload def __init__( self, default=None, *, some_attribute=10, doc=None, label=None, precedence=None, instantiate=False, constant=False, readonly=False, pickle_default_value=True, allow_None=False, per_instance=True, allow_refs=False, nested_refs=False ): ... def __init__(self, default=Undefined, *, some_attribute=Undefined, **params): super().__init__(default=default, **params) self.some_attribute = some_attribute ... ``` To make Parameter attributes work as expected, we have overridden `Parameter.__getattribute__` to detect whether the attribute value has been set to something that is not `Undefined`, and if not, to fall back to returning the default value declared in `_slot_defaults`: ```python print(CustomParameter(some_attribute=0).some_attribute) # 0 print(CustomParameter().some_attribute) # Fallback case # 10 ``` #### Default value of `name` accounted for Every `Parameterized` class has always been equipped with a `name` `Parameter`, which was originally to support using a Parameterized object in a GUI context that needs a text label, but is not always needed in other contexts. At the class level the `name` value is the class name, equivalent to `.__name__`, and at the instance level the `name` value is automatically generated as a unique string, unless you set it in the constructor: ```python class A(param.Parameterized): x = param.Number() print(A.name) # A print(A().name) # A00002 print(A(name='some name').name) # some name ``` However, sometimes you want to define your own default value for the `name` parameter, as in the following example. Before Param 2.0, any such default value was discarded at both the class and instance levels, but since 2.0 the provided `name` is now respected: ```python class Person(param.Parameterized): name = param.String(default='Eva') print(Person.name) # Param < 2: Person :( # Param 2.0: Eva :) print(Person().name) # Param < 2: Person0000 :( # Param 2.0: Eva :) ``` #### Setting a non-Parameter attribute via the constructor Before Param 2.0, you could set an instance attribute that was not a `Parameter` via the constructor, though you would get a warning if you did so. This behavior was prone to let typos slip through your code, setting the wrong instance attribute but continuing to function. Starting from Param 2.0, providing an attribute value that is not a Parameter now raises a `TypeError`, similarly to when you call a Python callable with an incorrect parameter name: ```python class A(param.Parameterized): number = param.Number() A(numbre=10) # oops typo! # Param < 2: WARNING:param.A00002: Setting non-parameter attribute numbre=10 using a mechanism intended only for parameters # Param 2.0: TypeError: A.__init__() got an unexpected keyword argument 'numbre' ``` #### `instance.param.watchers` value changed `instance.param.watchers` no longer returns the transient watchers state, which was never very useful, and now instead returns what you would expect, i.e. a dictionary of the watchers set up on this instance. To access that dictionary previously, you would have had to access the private attribute `instance._param_watchers`, which is now deprecated and was never intended to be public API: ```python class A(param.Parameterized): x = param.Number() @param.depends('x', watch=True) def cb(self): pass print(A().param.watchers) # Param < 2: [] # Param 2.0: {'x': {'value': [Watcher(...)]}} ``` #### Clean-up of the `Parameterized` namespace To avoid having your Parameterized object namespaces polluted with internal Param details, the methods listed below have been removed from the `Parameterized` namespace (members available to any class that inherits from `param.Parameterized`). Most of the time, a replacement method is now available from the `.param` namespace instead: - `_add_parameter`: use instead `param.add_parameter` - `params`: use instead `.param.values()` or `.param['param']` - `set_default`: use instead `for k,v in p.param.objects().items(): print(f"{p.__class__.name}.{k}={repr(v.default)}")` - `print_param_defaults`: `for k,v in p.param.objects().items(): print(f"{p.__class__.name}.{k}={repr(v.default)}")` - `set_param`: use instead `.param.update` - `set_dynamic_time_fn`: use instead `.param.set_dynamic_time_fn` - `get_param_values`: use instead `.param.values().items()` (or `.param.values()` for the common case of `dict(....param.get_param_values())`) - `force_new_dynamic_value`: use instead `.param.force_new_dynamic_value` - `get_value_generator`: use instead `.param.get_value_generator` - `inspect_value`: use instead `.param.inspect_value` - `_set_name`: no replacement - `__db_print`: no replacement - `warning`: use instead `.param.warning` - `message`: use instead `.param.log(param.MESSAGE, ...)` - `verbose`: use instead `.param.log(param.VERBOSE, ...)` - `debug`: use instead `.param.log(param.DEBUG, ...)` - `print_param_values`: use instead `for k,v in p.param.objects().items(): print(f"{p.__class__.name}.{k}={repr(v.default)}")` - `defaults`: use instead `{k:v.default for k,v in p.param.objects().items()}` - `pprint` and `_pprint`: use instead `.param.pprint` - `script_repr`: use instead `param.parameterized.script_repr` - `state_pop`: moved to `.param._state_pop`, it might be removed in a future version, let us know if you need it! - `state_push`: moved to `.param._state_push`, it might be removed in a future version, let us know if you need it! #### `Parameter` clean-up - Removed unused `bounds` slot from `Boolean` and `Event` - Removed private Parameter `_internal_name` slot #### Additional removals - Removed `Time.next` method that was only needed for Python 2 ### Deprecations Param 2.0 was originally meant to clean up even more Param's API, except that we noticed that deprecating API in the release notes of previous versions was not enough to warn Param's users (including ourselves!). Thus for 2.0 we decided to postpone removing some APIs, and this time properly warn Param's users that the code is about to be deleted. We've also spent a lot of time looking at the code base while working on this major release, and noticed additional clean up that could be made in the future. Therefore when you upgrade to 2.0, you are likely to encounter quite a large number of deprecation warnings. Make sure you're adapting your code as soon as you can whenever you notice one of the newly emitted warnings, because that code will not continue to be supported in later releases in the 2.x series. (Or you can pin your code to Param 1.x if you want to avoid the warnings and do not need any of the features or bugfixes from 2.0.) Here is the complete list of deprecation warnings added in Param 2.0: - Parameter signature: - Instantiating parameters other than `default` with positional arguments is deprecated: - `String('prefix-test', '^prefix')`: deprecated! - `String('prefix-test', regex='^prefix')`: OK - `String(default='prefix-test', regex='^prefix')`: OK - For `Selector` parameters that accept `objects` as first positional argument, and `ClassSelector` parameters that accept `class_` as first positional argument, passing *any* argument by position is deprecated: - `Selector([1, 2])`: deprecated! - `Selector(objects=[1, 2])`: OK - `ClassSelector((str, int))`: deprecated! - `ClassSelector(class_=(str, int))`: OK - It's possible that in the future the signature of these two parameters will be aligned with the other parameters to accept `default` as first and only positional argument, but for now please use an explicit keyword so that your code will be compatible with all versions. - Parameter slots: - `List._class`: use instead `item_type`. - `Number.set_hook`: no replacement - `param.__init__` module: - `param.produce_value`: no replacement - `param.as_unicode`: no replacement - `param.is_ordered_dict`: no replacement - `param.is_ordered_dict`: no replacement - `param.hashable`: no replacement - `param.named_objs`: no replacement - `param.normalize_path`: no replacement - `param.abbreviate_paths`: no replacement - `param.parameterized` module: - `param.parameterized.all_equal`: no replacement - `param.parameterized.add_metaclass`: no replacement - `param.parameterized.batch_watch`: use instead `batch_call_watchers` - `param.parameterized.recursive_repr`: no replacement - `param.parameterized.overridable_property`: no replacement - Parameterized `.param` namespace; many of these methods have been deprecated since version 1.12.0, but because the deprecation was announced only in the release notes and not with runtime warnings, meany users have failed to realize the methods were deprecated, sometimes even us included! All these methods now emit deprecation warnings when executed and are now clearly marked as deprecated in the API reference: - `.param.set_default`: use instead `for k,v in p.param.objects().items(): print(f"{p.__class__.name}.{k}={repr(v.default)}")` - `.param._add_parameter`: use instead `.param.add_parameter` - `.param.params`: use instead `.param.values()` or `.param['param']` - `.param.set_param`: use instead `.param.update` - `.param.get_param_values`: use instead `.param.values().items()` (or `.param.values()` for the common case of `dict(....param.get_param_values())`) - `.param.params_depended_on`: use instead `.param.method_dependencies` - `.param.defaults`: use instead `{k:v.default for k,v in p.param.objects().items()}` - `.param.print_param_defaults`: use instead `for k,v in p.param.objects().items(): print(f"{p.__class__.name}.{k}={repr(v.default)}")` - `.param.print_param_values`: use instead `for k,v in p.param.objects().items(): print(f"{p.__class__.name}.{k}={repr(v.default)}")` - `.param.message`: use instead `.param.log(param.MESSAGE, ...)` - `.param.verbose`: use instead `.param.log(param.VERBOSE, ...)` - `.param.debug`: use instead `.param.log(param.DEBUG, ...)` - Some methods now (correctly) warn that they are not safe to run in a `Parameterized` subclass constructor until you have called `super().__init__(**params)`: - `instance.param.objects(instance=True)` - `instance.param.trigger("")` - `instance.param.watch(callback, "")` - Parameterized namespace: - `instance._param_watchers` (getter and setter): use instead the property `inst.param.watchers` param-2.1.1/doc/user_guide/000077500000000000000000000000001463636336300155305ustar00rootroot00000000000000param-2.1.1/doc/user_guide/Dependencies_and_Watchers.ipynb000066400000000000000000001125771463636336300236600ustar00rootroot00000000000000{ "cells": [ { "cell_type": "markdown", "id": "33cd4c67", "metadata": {}, "source": [ "# Dependencies and Watchers\n", "\n", "As outlined in the [Dynamic Parameters](Dynamic_Parameters.ipynb) guide, Param can be used in multiple ways, including as a static set of typed attributes, dynamic attributes that are computed when they are read (`param.Dynamic` parameters, a \"pull\" or \"get\" model), and using explicitly expressed chains of actions driven by events at the Parameter level (a \"push\" or \"set\" model described in this notebook). \n", "\n", "Unlike Dynamic Parameters, which calculate values when parameters are _accessed_, the dependency and watcher interface allows events to be triggered when parameters are _set_. With this interface, parameters and methods can declare that they should be updated or invoked when a given parameter is modified, spawning a cascading series of events that update settings to be consistent, adapt values as appropriate for a change, or invoke computations such as updating a displayed object when a value is modified. This approach is well suited to a GUI interface, where a user interacts with a single widget at a time but other widgets or displays need to be updated in response. The\n", "[Dynamic Parameters](Dynamic_Parameters.ipynb) approach, in contrast, is well suited when Parameters update either on read or in response to a global clock or counter, such as in a simulation or machine-learning iteration.\n", "\n", "This user guide is structured as three main sections:\n", "\n", "- [Dependencies](#dependencies): High-level dependency declaration via the `@param.depends()` decorator\n", "- [Watchers](#watchers): Low-level watching mechanism via `.param.watch()`.\n", "- [Using dependencies and watchers](#using-dependencies-and-watchers): Utilities and tools for working with events created using either dependencies or watchers." ] }, { "cell_type": "markdown", "id": "12e54858", "metadata": {}, "source": [ "## Dependencies\n", "\n", "Param's `depends` decorator allows a programmer to express that a given computation \"depends\" on a certain set of parameters. For instance, if you have parameters whose values are interlinked, it's easy to express that relationship with `depends`:" ] }, { "cell_type": "code", "execution_count": null, "id": "3001f2d9", "metadata": {}, "outputs": [], "source": [ "import param\n", "\n", "class C(param.Parameterized):\n", " _countries = {'Africa': ['Ghana', 'Togo', 'South Africa'],\n", " 'Asia' : ['China', 'Thailand', 'Japan', 'Singapore'],\n", " 'Europe': ['Austria', 'Bulgaria', 'Greece', 'Switzerland']}\n", " \n", " continent = param.Selector(default='Asia', objects=list(_countries.keys()))\n", " country = param.Selector(objects=_countries['Asia'])\n", " \n", " @param.depends('continent', watch=True)\n", " def _update_countries(self):\n", " countries = self._countries[self.continent]\n", " self.param['country'].objects = countries\n", " if self.country not in countries:\n", " self.country = countries[0]\n", "\n", "c = C()\n", "c.country, c.param.country.objects" ] }, { "cell_type": "code", "execution_count": null, "id": "93e1c6e6", "metadata": {}, "outputs": [], "source": [ "c.continent='Africa'\n", "c.country, c.param.country.objects" ] }, { "cell_type": "code", "execution_count": null, "id": "8258470a", "metadata": {}, "outputs": [], "source": [ "c" ] }, { "cell_type": "markdown", "id": "e2f371c2", "metadata": {}, "source": [ "As you can see, here Param updates the allowed and current values for `country` whenever someone changes the `continent` parameter. This code relies on the dependency mechanism to make sure these parameters are kept appropriately synchronized:\n", "\n", "1. First, we set up the default continent but do not declare the `default` for the `country` parameter. This is because this parameter is dependent on the `continent` and therefore it is easy to set up values that are inconsistent and makes it difficult to override the default continent since changes to both parameters need to be coordinated. \n", "2. Next, if someone chooses a different continent, the list of countries allowed needs to be updated, so the method `_update_countries()` that (a) looks up the countries allowed for the current continent, (b) sets that list as the allowed objects for the `country` parameter, and (c) selects the first such country as the default country.\n", "3. Finally, we expressed that the `_update_countries()` method depends on the `continent` parameter. We specified `watch=True` to direct Param to invoke this method immediately, whenever the value of `continent` changes. We'll see [examples of watch=False](#watch-false-dependencies) later. Importantly we also set `on_init=True`, which means that when instance is created the `self._update_countries()` method is automatically called setting up the `country` parameter appropriately. This avoids having to declare a `__init__` method to manually call the method ourselves and the potentially brittle process of setting up consistent defaults." ] }, { "cell_type": "markdown", "id": "b14b37b6", "metadata": {}, "source": [ "### Dependency specs\n", "\n", "The example above expressed a dependency of `_update_countries` on this object's `continent` parameter. A wide range of such dependency relationships can be specified:\n", "\n", "1. **Multiple dependencies**: Here we had only one parameter in the dependency list, but you can supply any number of dependencies (`@param.depends('continent', 'country', watch=True)`).\n", "2. **Dependencies on nested parameters**: Parameters specified can either be on this class, or on nested Parameterized objects of this class. Parameters on this class are specified as the attribute name as a simple string (like `'continent'`). Nested parameters are specified as a dot-separated string (like `'handler.strategy.i'`, if this object has a parameter `handler`, whose value is an object `strategy`, which itself has a parameter `i`). If you want to depend on some arbitrary parameter elsewhere in Python, just create an `instantiate=False` (and typically read-only) parameter on this class to hold it, then here you can specify the path to it on _this_ object.\n", "3. **Dependencies on metadata**: By default, dependencies are tied to a parameter's current value, but dependencies can also be on any of the declared metadata about the parameter (e.g. a method could depend on `country:constant`, triggering when someone changes whether that parameter is constant, or on `country:objects` (triggering when the objects list is replaced (not just changed in place as in appending). The available metadata is listed in the `__slots__` attribute of a Parameter object (e.g. \n", "`p.param.continent.__slots__`). \n", "4. **Dependencies on any nested param**: If you want to depend on _all_ the parameters of a nested object `n`, your method can depend on `'n.param'` (where parameter `n` has been set to a Parameterized object).\n", "5. **Dependencies on a method name**: Often you will want to break up computation into multiple chunks, some of which are useful on their own and some which require other computations to have been done as prerequisites. In this case, your method can declare a dependency on another method (as a string name), which means that it will now watch everything that method watches, and will then get invoked after that method is invoked.\n", "\n", "We can see examples of all these dependency specifications in class `D` below:" ] }, { "cell_type": "code", "execution_count": null, "id": "6df85b16", "metadata": {}, "outputs": [], "source": [ "class D(param.Parameterized):\n", " x = param.Number(7)\n", " s = param.String(\"never\")\n", " i = param.Integer(-5)\n", " o = param.Selector(objects=['red', 'green', 'blue'])\n", " n = param.ClassSelector(default=c, class_=param.Parameterized, instantiate=False) \n", " \n", " @param.depends('x', 's', 'n.country', 's:constant', watch=True)\n", " def cb1(self):\n", " print(f\"cb1 x={self.x} s={self.s} \"\n", " f\"param.s.constant={self.param.s.constant} n.country={self.n.country}\")\n", "\n", " @param.depends('n.param', watch=True)\n", " def cb2(self):\n", " print(f\"cb2 n={self.n}\")\n", "\n", " @param.depends('x', 'i', watch=True)\n", " def cb3(self):\n", " print(f\"cb3 x={self.x} i={self.i}\")\n", "\n", " @param.depends('cb3', watch=True)\n", " def cb4(self):\n", " print(f\"cb4 x={self.x} i={self.i}\")\n", "\n", "d = D()\n", "d" ] }, { "cell_type": "markdown", "id": "80365d7f", "metadata": {}, "source": [ "Here we have created an object `d` of type `D` with a unique ID like `D00003`. `d` has various parameters, including one nested Parameterized object in its parameter `n`. In this class, the nested parameter is set to our earlier object `c`, using `instantiate=False` to ensure that the value is precisely the same earlier object, not a copy of it. You can verify that it is the same object by comparing e.g. `name='C00002'` in the repr for the subobject in `d` to the name in the repr for `c` in the previous section; both should be e.g. `C00002`.\n", "\n", "Dependencies are stored declaratively so that they are accessible for other libraries to inspect and use. E.g. we can now examine the dependencies for the decorated callback method `cb1`:" ] }, { "cell_type": "code", "execution_count": null, "id": "c13bd79d", "metadata": {}, "outputs": [], "source": [ "dependencies = d.param.method_dependencies('cb1')\n", "[f\"{o.inst.name}.{o.pobj.name}:{o.what}\" for o in dependencies]" ] }, { "cell_type": "markdown", "id": "7548280e", "metadata": {}, "source": [ "Here we can see that method `cb1` will be invoked for any value changes in `d`'s parameters `x` or `s`, for any value changes in `c`'s parameter `country`, and a change in the `constant` slot of `s`. These dependency relationships correspond to the specification `@param.depends('x', 's', 'n.country', 's:constant', watch=True)` above.\n", "\n", "Now, if we change `x`, we can see that Param invokes `cb1`:" ] }, { "cell_type": "code", "execution_count": null, "id": "77caf9c1", "metadata": {}, "outputs": [], "source": [ "d.x = 5" ] }, { "cell_type": "markdown", "id": "066453bf", "metadata": {}, "source": [ "`cb3` and `cb4` are also invoked, because `cb3` depends on `x` as well, plus `cb4` depends on `cb3`, inheriting all of `cb3`'s dependencies.\n", "\n", "If we now change `c.country`, `cb1` will be invoked since `cb1` depends on `n.country`, and `n` is currently set to `c`:" ] }, { "cell_type": "code", "execution_count": null, "id": "17394110", "metadata": {}, "outputs": [], "source": [ "c.country = 'Togo'" ] }, { "cell_type": "markdown", "id": "477ab1d1", "metadata": {}, "source": [ "As you can see, `cb2` is also invoked, because `cb2` depends on _all_ parameters of the subobject in `n`. \n", "\n", "`continent` is also a parameter on `c`, so `cb2` will also be invoked if you change `c.continent`. Note that changing `c.continent` itself invokes `c._update_countries()`, so in that case `cb2` actually gets invoked _twice_ (once for each parameter changed on `c`), along with `cb1` (watching `n.country`):" ] }, { "cell_type": "code", "execution_count": null, "id": "acc4c4cb", "metadata": {}, "outputs": [], "source": [ "c.continent = 'Europe'" ] }, { "cell_type": "markdown", "id": "a973b325", "metadata": {}, "source": [ "Changing metadata works just the same as changing values. Because `cb1` depends on the `constant` slot of `s`, it is invoked when that slot changes:" ] }, { "cell_type": "code", "execution_count": null, "id": "a6403a93", "metadata": {}, "outputs": [], "source": [ "d.param.s.constant = True" ] }, { "cell_type": "markdown", "id": "68c82e25", "metadata": {}, "source": [ "Importantly, if we replace a sub-object on which we have declared dependencies, Param automatically rebinds the dependencies to the new object:" ] }, { "cell_type": "code", "execution_count": null, "id": "f6195bdd", "metadata": {}, "outputs": [], "source": [ "d.n = C()" ] }, { "cell_type": "markdown", "id": "af05eb1f", "metadata": {}, "source": [ "Note that if the values of the dependencies on the old and new object are the same, no event is fired. \n", "\n", "Additionally the previously bound sub-object is now no longer connected:" ] }, { "cell_type": "code", "execution_count": null, "id": "9d52086e", "metadata": {}, "outputs": [], "source": [ "c.continent = 'Europe'" ] }, { "cell_type": "markdown", "id": "6e4d5b80", "metadata": {}, "source": [ "### `watch=False` dependencies\n", "\n", "The previous examples all supplied `watch=True`, indicating that Param itself should watch for changes in the dependency and invoke that method when a dependent parameter is set. If `watch=False` (the default), `@param.depends` declares that such a dependency exists, but does not automatically invoke it. `watch=False` is useful for setting up code for a separate library like [Panel](https://panel.holoviz.org) or [HoloViews](https://holoviews.org) to use, indicating which parameters the external library should watch so that it knows when to invoke the decorated method. Typically, you'll want to use `watch=False` when that external library needs to do something with the return value of the method (a functional approach), and use `watch=True` when the function is [side-effecty](https://en.wikipedia.org/wiki/Side_effect_(computer_science)), i.e. having an effect just from running it, and not normally returning a value.\n", "\n", "For instance, consider this Param class with methods that return values to display:" ] }, { "cell_type": "code", "execution_count": null, "id": "7f1ebe63", "metadata": {}, "outputs": [], "source": [ "class Mul(param.Parameterized):\n", " a = param.Number(5, bounds=(-100, 100))\n", " b = param.Number(-2, bounds=(-100, 100))\n", "\n", " @param.depends('a', 'b')\n", " def view(self):\n", " return str(self.a*self.b)\n", "\n", " def view2(self):\n", " return str(self.a*self.b)\n", "\n", "prod = Mul(name='Multiplier')" ] }, { "cell_type": "markdown", "id": "00ff51ea", "metadata": {}, "source": [ "You could run this code manually:" ] }, { "cell_type": "code", "execution_count": null, "id": "fc51ed2b", "metadata": {}, "outputs": [], "source": [ "prod.a = 7\n", "prod.b = 10\n", "prod.view()" ] }, { "cell_type": "markdown", "id": "50ab0942", "metadata": {}, "source": [ "Or you could pass the parameters and the `view` method to Panel, and let Panel invoke it as needed by following the dependency chain:" ] }, { "cell_type": "code", "execution_count": null, "id": "05f1bb0d", "metadata": {}, "outputs": [], "source": [ "import panel as pn\n", "pn.extension()" ] }, { "cell_type": "code", "execution_count": null, "id": "050b268a", "metadata": {}, "outputs": [], "source": [ "pn.Row(prod.param, prod.view)" ] }, { "cell_type": "markdown", "id": "acda7cc7", "metadata": {}, "source": [ "Panel creates widgets for the parameters, runs the `view` method with the default values of those parameters, and displays the result. As long as you have a live Python process running (not just a static HTML export of this page as on param.holoviz.org), Panel will then watch for changes in those parameters due to the widgets and will re-execute the `view` method to update the output whenever one of those parameters changes. Using the dependency declarations, Panel is able to do all this without ever having to be told separately which parameters there are or what dependency relationships there are. \n", "\n", "How does that work? A library like Panel can simply ask Param what dependency relationships have been declared for the method passed to it:" ] }, { "cell_type": "code", "execution_count": null, "id": "3536291d", "metadata": {}, "outputs": [], "source": [ "[o.name for o in prod.param.method_dependencies('view')]" ] }, { "cell_type": "markdown", "id": "899a41e8", "metadata": {}, "source": [ "Note that in this particular case the `depends` decorator could have been omitted, because Param conservatively assumes that any method _could_ read the value of any parameter, and thus if it has no other declaration from the user, the dependencies are assumed to include _all_ parameters (including `name`, even though it is constant):" ] }, { "cell_type": "code", "execution_count": null, "id": "a8bef58c", "metadata": {}, "outputs": [], "source": [ "[o.name for o in prod.param.method_dependencies('view2')]" ] }, { "cell_type": "markdown", "id": "f8383036", "metadata": {}, "source": [ "Conversely, if you want to declare that a given method does not depend on any parameters at all, you can use `@param.depends()`. \n", "\n", "Be sure not to set `watch=True` for dependencies for any method you pass to an external library like Panel to handle, or else that method will get invoked _twice_, once by Param itself (discarding the output) and once by the external library (using the output). Typically you will want `watch=True` for a side-effecty function or method (typically not returning a value), and `watch=False` (the default) for a function or method with a return value, and you'll need an external library to do something with that return value." ] }, { "cell_type": "markdown", "id": "1169f3b6", "metadata": {}, "source": [ "### `@param.depends` with function objects\n", "\n", "The `depends` decorator can also be used with bare functions, in which case the specification should be an actual Parameter object, not a string. The function will be called with the parameter(s)'s value(s) as positional arguments:" ] }, { "cell_type": "code", "execution_count": null, "id": "60a085b7", "metadata": {}, "outputs": [], "source": [ "@param.depends(c.param.country, d.param.i, watch=True)\n", "def g(country, i):\n", " print(f\"g country={country} i={i}\")\n", "\n", "c.country = 'Greece'" ] }, { "cell_type": "code", "execution_count": null, "id": "a7105a92", "metadata": {}, "outputs": [], "source": [ "d.i = 6" ] }, { "cell_type": "markdown", "id": "cd198a2e", "metadata": {}, "source": [ "Here you can see that in addition to the classmethods starting with `cb` previously set up to depend on the country, setting `c`'s `country` parameter or `d`'s `i` parameter now also invokes function `g`, passing in the current values of the parameters it depends on whenever the function gets invoked. `g` can then make a side effect happen such as updating any other data structure it can access that needs to be changed when `country` or `i` changes. \n", "\n", "Using `@param.depends(..., watch=False)` with a function allows providing bound standalone functions to an external library for display, just as in the `.view` method above.\n", "\n", "Of course, you can still invoke `g` with your own explicit arguments, which does not invoke any watching mechanisms:" ] }, { "cell_type": "code", "execution_count": null, "id": "c4e160c1", "metadata": {}, "outputs": [], "source": [ "g('USA', 7)" ] }, { "cell_type": "markdown", "id": "00370f00", "metadata": {}, "source": [ "## Watchers\n", "\n", "The `depends` decorator is built on Param's lower-level `.param.watch` interface, registering the decorated method or function as a `Watcher` object associated with those parameter(s). If you're building or using a complex library like Panel, you can use the low-level Parameter watching interface to set up arbitrary chains of watchers to respond to parameter value or metadata setting:\n", "\n", "- `obj.param.watch(fn, parameter_names, what='value', onlychanged=True, queued=False, precedence=0)`:
Create 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, "id": "9f076d1b", "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", "id": "6cdbd8e5", "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, "id": "cef96eae", "metadata": {}, "outputs": [], "source": [ "#help(param.parameterized.Event)\n", "#help(param.parameterized.Watcher)\n", "#help(param.parameterized.PInfo)" ] }, { "cell_type": "markdown", "id": "c06d1a13", "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", "- [`.param.watchers`](#param-watchers): writable property to access the instance watchers\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", "id": "9356f733", "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, "id": "905456c3", "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", "id": "9c25515a", "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, "id": "6c7fe2df", "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", "id": "1deb1709", "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, "id": "e511a92a", "metadata": {}, "outputs": [], "source": [ "p.param.update(a=9,b=2)" ] }, { "cell_type": "markdown", "id": "acc97f01", "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, "id": "cf129b7c", "metadata": {}, "outputs": [], "source": [ "with param.parameterized.discard_events(p):\n", " p.a = 2\n", " p.b = 9" ] }, { "cell_type": "markdown", "id": "ffc9a462", "metadata": {}, "source": [ "(Notice that none of the callbacks is invoked, despite all the Watchers on `p.a` and `p.b`.)" ] }, { "cell_type": "markdown", "id": "67437fe6", "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, "id": "c2b06db2", "metadata": {}, "outputs": [], "source": [ "p.b = p.b" ] }, { "cell_type": "markdown", "id": "9fb9a5a8", "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, "id": "b086d010", "metadata": {}, "outputs": [], "source": [ "p.param.trigger('b')" ] }, { "cell_type": "markdown", "id": "621e183e", "metadata": {}, "source": [ "If you trigger `a`, the usual series of chained events will be triggered, including changing `b`:" ] }, { "cell_type": "code", "execution_count": null, "id": "827ef750", "metadata": {}, "outputs": [], "source": [ "p.param.trigger('a')" ] }, { "cell_type": "markdown", "id": "fc3cb5ba", "metadata": {}, "source": [ "### `.param.watchers`\n", "\n", "For more advanced purposes it can be useful to inspect all the watchers set up on an instance, in which case you can use `inst.param.watchers` to obtain a dictionary with the following structure: `{parameter_name: {what: [Watcher(), ...], ...}, ...}`" ] }, { "cell_type": "code", "execution_count": null, "id": "0b2ae598", "metadata": {}, "outputs": [], "source": [ "p.param.watchers" ] }, { "cell_type": "markdown", "id": "42ffa0a0", "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, "id": "8498624d", "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, "id": "317b053e", "metadata": {}, "outputs": [], "source": [ "q.e" ] }, { "cell_type": "code", "execution_count": null, "id": "b423aa8b", "metadata": {}, "outputs": [], "source": [ "q.param.trigger('e')" ] }, { "cell_type": "code", "execution_count": null, "id": "bb5b83ae", "metadata": {}, "outputs": [], "source": [ "q.e" ] }, { "cell_type": "markdown", "id": "8f17a138", "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. The asynchronous executor can be defined on `param.parameterized.async_executor` by default it will start an [asyncio](https://docs.python.org/3/library/asyncio.html) event loop or reuse the one that is running. This allows you to use coroutines and other asynchronous functions as `.param.watch` callbacks.\n", "\n", "As an example we can watch results accumulate:" ] }, { "cell_type": "code", "execution_count": null, "id": "03ece21a", "metadata": {}, "outputs": [], "source": [ "import param, asyncio, aiohttp\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", "id": "4744d8c7", "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 7 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, "id": "38da7bb2", "metadata": {}, "outputs": [], "source": [ "print(\"Waiting: \", end=\"\")\n", "while len(f.results) 10).rx.where('Risk of flooding', 'Everything is normal')" ] }, { "cell_type": "markdown", "id": "e1e80a15-e715-4cb7-bde8-17005e64c78d", "metadata": {}, "source": [ "The same thing can also easily be achieved by using parameter binding:" ] }, { "cell_type": "code", "execution_count": null, "id": "13f3bc65-be27-451e-b119-3faf0df6c559", "metadata": {}, "outputs": [], "source": [ "param.bind(lambda value: 'Risk of flooding' if value > 10 else 'Everything is normal', corrected).rx()" ] }, { "cell_type": "markdown", "id": "d1099127-bc12-4068-a123-737fbc7aae66", "metadata": {}, "source": [ "## Classes" ] }, { "cell_type": "markdown", "id": "c5cff863-cbf3-4e44-a1da-df915a9deab7", "metadata": {}, "source": [ "In the context of a class we can depend on a parameter that is driven by a generator as normal:" ] }, { "cell_type": "code", "execution_count": null, "id": "137954a7-9c9d-474c-92b8-fdf2b9a29c03", "metadata": {}, "outputs": [], "source": [ "class TidalWarning(TidalGauge):\n", "\n", " message = param.String()\n", " \n", " @param.depends('height', watch=True)\n", " def _update_message(self):\n", " self.message = 'Risk of flooding' if self.height > 10 else 'Everything is normal'\n", "\n", "warn = TidalWarning(height=poll_sensor)\n", "\n", "warn.param['message'].rx()" ] }, { "cell_type": "markdown", "id": "154d60b8-6460-4f83-9d12-9dc7bab45625", "metadata": {}, "source": [ "## Asynchronous Generators\n", "\n", "Generators are powerful and internally Param will execute the generators on a separate thread to ensure that it does not block the main event loop. However, often an asynchronous generator is more appropriate since many operations that rely on generators are waiting on some external event, i.e. by polling a file or network resource, which can be done asynchronously.\n", "\n", "Regular generators and asynchronous generators can be used interchangeably:" ] }, { "cell_type": "code", "execution_count": null, "id": "6fe71ab8-c87e-4e30-914e-d043f7922766", "metadata": {}, "outputs": [], "source": [ "import asyncio\n", "\n", "async def poll_sensor_async(timeout=0.5):\n", " while True:\n", " yield tidal_gauge()\n", " await asyncio.sleep(timeout)\n", "\n", "async_gauge = TidalGauge(height=poll_sensor_async)\n", "\n", "async_gauge.param.height.rx()" ] }, { "cell_type": "markdown", "id": "fde666d8-38a8-4829-ad25-81a039f09055", "metadata": {}, "source": [ "## Dependencies\n", "\n", "Now let us say that our generator itself has some dependency, e.g. we want to be able to control the sampling frequency of our gauge. To achieve this we can bind a parameter to our generator:" ] }, { "cell_type": "code", "execution_count": null, "id": "f50d6bc7-7ec6-40fc-a4fa-71db0ae8e413", "metadata": {}, "outputs": [], "source": [ "class VariableRateTidalGauge(TidalGauge):\n", "\n", " frequency = param.Integer(default=2, doc=\"\"\"\n", " Frequency in Hz to sample the tidal gauge sensor at.\"\"\")\n", "\n", "variable_gauge = VariableRateTidalGauge(frequency=5)\n", "\n", "variable_gauge.height = param.bind(poll_sensor_async, timeout=1/variable_gauge.param.frequency.rx())\n", "\n", "variable_gauge.param.height.rx()" ] }, { "cell_type": "markdown", "id": "eb210a56-2b9b-4804-8080-f856eb38234f", "metadata": {}, "source": [ "Now we can vary the frequency at which our generator samples the sensor:" ] }, { "cell_type": "code", "execution_count": null, "id": "14e0cfea-ad5a-4399-89ff-4bdfad0b25ed", "metadata": {}, "outputs": [], "source": [ "variable_gauge.frequency = 1" ] } ], "metadata": { "language_info": { "name": "python", "pygments_lexer": "ipython3" } }, "nbformat": 4, "nbformat_minor": 5 } param-2.1.1/doc/user_guide/How_Param_Works.ipynb000066400000000000000000000370331463636336300216430ustar00rootroot00000000000000{ "cells": [ { "cell_type": "markdown", "id": "8c73a0b0", "metadata": {}, "source": [ "# How Param Works\n", "\n", "Param seamlessly makes Python attributes have a much richer set of behaviors than they would otherwise, at once both more powerful (with automatic dynamic behavior) and more tightly controlled by the class author. It is natural to wonder how Param achieves this, especially given that it is a normal pure-Python library, not an alternative implementation of Python or a pre-processor. The answer is that Param makes extensive use of Python language features that allow tailoring the behavior of attribute getting and setting in sophisticated ways. You don't need to read any of the material on this page to use Param successfully, but it might help you understand what's going on \"under the hood\" for debugging or optimizing complex situations or for extending Param.\n", "\n", "## Descriptors\n", "\n", "A `Parameter` object is a type of Python \"descriptor\", i.e., an object that implements custom `__get__` and/or `__set__` behavior. When a descriptor is an attribute of a class, Python will invoke those custom methods instead of simply getting and setting the actual value of the attribute (i.e., the `Parameter` object). The [Python descriptor docs](https://docs.python.org/3/howto/descriptor.html) explain this process in detail, but briefly, let's consider a simple descriptor that returns how many times it has been accessed:" ] }, { "cell_type": "code", "execution_count": null, "id": "af0cddab", "metadata": {}, "outputs": [], "source": [ "class Count:\n", " def __init__(self, start=0):\n", " self._count = start\n", " \n", " def __get__(self, obj, objtype=None):\n", " self._count += 1\n", " return self._count" ] }, { "cell_type": "code", "execution_count": null, "id": "1488fb8c", "metadata": {}, "outputs": [], "source": [ "class C:\n", " x = 5\n", " y = Count(0)\n", "\n", "c = C()\n", "\n", "c.x, c.x, c.x, c.y, c.y, c.y" ] }, { "cell_type": "markdown", "id": "4876a7f6", "metadata": {}, "source": [ "As you can see, class attributes `x` and `y` here can both be used the same way, but `x` is a normal Python attribute, returning the fixed value `5` that was set on the class, while `y` is a descriptor and returns a computed value when accessed (rather than returning itself as you might think from the syntax), and thus gives a different value each time. Parameters are much more complex than the above example, but this descriptor support provides the underlying mechanism for having full-featured attribute behavior like dynamic values, bounds checking, and so on." ] }, { "cell_type": "markdown", "id": "8d07cbda", "metadata": {}, "source": [ "## Slots\n", "\n", "As described in the [Parameters docs](Parameters.ipynb), Parameters can store a rich collection of metadata about each parameter. Storing a full object and associated dictionary of metadata for each class and instance attribute could get expensive (i.e., slow and using a lot of memory), so Parameters are implemented using [slots](https://docs.python.org/3/reference/datamodel.html#slots). A slot is like a normal Python attribute, but instead of being stored in the convenient and flexible but slow `__dict__` attribute of the object, slots are stored in a fixed-size data structure `__slots__` that works like a C `struct`. `__slots__` reserves just enough space to store these attributes, which can be accessed instantaneously rather than requiring a dictionary lookup (hash table search).\n", "\n", "Using `__slots__` requires special support for operations to copy and restore Parameters (e.g. for Python persistent storage pickling); see `__getstate__` and `__setstate__`. A Parameter defines at least these slots, with additional slots added for each subclass:\n", "\n", "```\n", "__slots__ = ['name', 'default', 'doc',\n", " 'precedence', 'instantiate', 'constant', 'readonly',\n", " 'pickle_default_value', 'allow_None', 'per_instance',\n", " 'watchers', 'owner', 'allow_refs', 'nested_refs', '_label']\n", "```\n", "\n", "In most cases, you can just treat a Parameter's existing slots like attributes of the Parameter class; they work just the same as regular attributes except for speed and storage space. However, if you add a _new_ attribute to a Parameter class, you have to make sure that you also add it to the `__slots__` defined for that Parameter class, or you'll either get an error or else the Parameter will get an unnecessary full `__dict__` object just to hold the one new attribute. " ] }, { "cell_type": "markdown", "id": "cac0fda5", "metadata": {}, "source": [ "## Metaclasses\n", "\n", "Another way `Parameter` and `Parameterized` differ from ordinary Python classes is that they specify a special [metaclass](https://docs.python.org/3/reference/datamodel.html#metaclasses) that determines how they behave. Just like you instantiate a Python class to get a Python object, you instantiate a Python metaclass to get a Python class. Most classes are instances of the default metaclass named `type`, but with a custom metaclass, you can define how every Python class with that metaclass will behave, at a fundamental level. \n", "\n", "The `ParameterMetaclass` is fairly simple, mainly overriding docstrings so that `help(someparam)` gives the declared documentation for the Parameter instance, rather than the less-useful docstring for the underlying class that it would otherwise display. This behavior is convenient, but not essential to the operation of Param. \n", "\n", "`ParameterizedMetaclass`, on the other hand, defines a lot of the behavior behind Param's features. In particular, the metaclass implements the behavior for getting and setting parameter values at the class level, similar to how a descriptor controls such behavior at the instance level. Without the metaclass, setting the value of a class attribute to a scalar like `5` would wipe out the `Parameter` object rather than updating the default value. The metaclass thus performs the same role at the class level as descriptors do at the instance level. Descriptors allow setting the value of an instance attribute without overriding the `Parameter` object on that instance, and the metaclass allows setting the value of a class attribute without overridding the `Parameter` object on the class. All told, the ParameterizedMetaclass handles: \n", "\n", "- allowing Parameter default values to be set at the class level (as just described),\n", "- supporting inheriting Parameter objects from superclasses, \n", "- instantiating parameter default values (if needed)\n", "- populating the `name` slot of each Parameter by its attribute name in the class,\n", "- reporting whether a class has been declared to be abstract (useful for ignoring it in selectors),\n", "- various bookkeeping about dependencies and watchers, \n", "- generating docstrings at the class level for each Parameter in the class so that `help(parameterizedclass)` displays not just the class docstring but also information about the Parameters in it (or in superclasses)\n", "\n", "Thus much of how Param works depends on ParameterizedMetaclass. " ] }, { "cell_type": "markdown", "id": "60341a22", "metadata": {}, "source": [ "## Custom attribute access\n", "\n", "The above mechanisms let Param [customize attribute access](https://docs.python.org/3/reference/datamodel.html#customizing-attribute-access) for dynamic behavior and control over user settings. As an example of how this all fits together, consider the following code:" ] }, { "cell_type": "code", "execution_count": null, "id": "4d6e587e", "metadata": {}, "outputs": [], "source": [ "from param import Parameterized, Parameter\n", "\n", "class A(Parameterized):\n", " p = Parameter(default=1, per_instance=False, instantiate=False)\n", "\n", "a1 = A()\n", "a2 = A()" ] }, { "cell_type": "markdown", "id": "c03ebf19", "metadata": {}, "source": [ "Here, a1 and a2 share one Parameter object (`A.__dict__['p']`), because `per_instance` is `False`:" ] }, { "cell_type": "code", "execution_count": null, "id": "6eb529a8", "metadata": {}, "outputs": [], "source": [ "A.__dict__['p'] is a1.param.p is a2.param.p" ] }, { "cell_type": "markdown", "id": "03b20d6f", "metadata": {}, "source": [ "The default (class-attribute) value of p is stored in this Parameter object (`A.__dict__['p'].default`), but is accessible as `A.p` due to the Parameter being a descriptor:" ] }, { "cell_type": "code", "execution_count": null, "id": "0fe38b09", "metadata": {}, "outputs": [], "source": [ "A.__dict__['p'].default" ] }, { "cell_type": "code", "execution_count": null, "id": "886bc3d5", "metadata": {}, "outputs": [], "source": [ "A.p" ] }, { "cell_type": "markdown", "id": "36ef8c10", "metadata": {}, "source": [ "If the value of `p` is set on `a1`, `a1`'s value of `p` is stored in the `a1` instance itself, in a dictionary named `values` under the private namespace `_param__private`:" ] }, { "cell_type": "code", "execution_count": null, "id": "90061161", "metadata": {}, "outputs": [], "source": [ "a1.p = 2\n", "a1._param__private.values['p']" ] }, { "cell_type": "markdown", "id": "9b5c6a79", "metadata": {}, "source": [ "When `a1.p` is requested, `a1._param__private.values['p']` is returned. When `a2.p` is requested, `p` is not found in `a2._param__private.values`, so `A.__dict__['p'].default` (i.e. `A.p`) is returned instead:" ] }, { "cell_type": "code", "execution_count": null, "id": "ec1ac4fc", "metadata": {}, "outputs": [], "source": [ "a2.p" ] }, { "cell_type": "markdown", "id": "82208f0d", "metadata": {}, "source": [ "Because the value for `a2.p` is returned from `A.p`, changing `A.p` will affect `a2.p`, but not `a1.p` since it has its own independent value:" ] }, { "cell_type": "code", "execution_count": null, "id": "3f1d08b5", "metadata": {}, "outputs": [], "source": [ "A.p = 3\n", "a2.p, a1.p" ] }, { "cell_type": "markdown", "id": "88a854b5", "metadata": {}, "source": [ "If `p` was not defined in `A` but was defined in a superclass, the value found in that superclass would be returned instead. \n", "\n", "You can re-execute the above code changing to `per_instance=True` and/or `instantiate=True` on Parameter `p` and see how the behavior differs. With `per_instance=True` (which would normally be the default), `a1` and `a2` would each have independent copies of the `Parameter` object, and with `instantiate=True`, each instance would get its own copy of the class's default value, making it immune to later changes at the class level." ] }, { "cell_type": "markdown", "id": "b86f55e3-5ae5-4879-9b4c-273e859b6fec", "metadata": {}, "source": [ "## References\n", "\n", "Beyond the custom attribute access mechanisms of a single `Parameter`, Param can link together multiple Parameters. With the `allow_refs` option, a `Parameter` can act as a dynamic reference to another `Parameter`. This enables the values of two or more Parameters to stay in sync. Any change to the referenced Parameter's value is automatically reflected in all Parameters that reference it." ] }, { "cell_type": "code", "execution_count": null, "id": "fba9fbdc-0776-4cd3-8250-e00487593d57", "metadata": {}, "outputs": [], "source": [ "class B(Parameterized):\n", " \n", " p = Parameter(default=1, allow_refs=True)" ] }, { "cell_type": "markdown", "id": "27beb80b-415f-41cb-b909-adb6822e7e9a", "metadata": {}, "source": [ "Having declared a `Parameter` that allows references we can now pass the parameter `p` of `b1` to parameter `p` of `b2`:" ] }, { "cell_type": "code", "execution_count": null, "id": "22a51c29-c286-4e0f-b1a6-1e02bc4149da", "metadata": {}, "outputs": [], "source": [ "b1 = B(p=14)\n", "b2 = B(p=b1.param.p)" ] }, { "cell_type": "markdown", "id": "5eab5e13-66df-4001-8d70-2cab70449641", "metadata": {}, "source": [ "Inspecting `b2.p` we will see that `p` of `b2` now reflects the value of `b1.p`:" ] }, { "cell_type": "code", "execution_count": null, "id": "fdc58565-f327-405f-958d-b49caa70adb3", "metadata": {}, "outputs": [], "source": [ "b2.p" ] }, { "cell_type": "markdown", "id": "7322e310-9105-48c9-a0b8-41cb80bcf4e6", "metadata": {}, "source": [ "Even when we update the value of `b1.p` the value of `b2.p` will reflect the change:" ] }, { "cell_type": "code", "execution_count": null, "id": "961aebc0-45b5-458d-919c-1ba6a3c286e7", "metadata": {}, "outputs": [], "source": [ "b1.p = 7\n", "\n", "b2.p" ] }, { "cell_type": "markdown", "id": "75c5eb72-3132-458c-9b30-b5cd1e64b63d", "metadata": {}, "source": [ "Internally Param will [watch](Dependencies_and_Watchers.ipynb#watchers) all Parameters associated with the reference by calling `param.parameterized.resolve_ref` and then use `param.parameterized.resolve_value` to resolve the current value of the reference.\n", "\n", "If we explicitly set `b2.p` however the two values will become unsynced, i.e. the Watcher created when setting the reference will be removed:" ] }, { "cell_type": "code", "execution_count": null, "id": "2e02cb21-a4ab-48fb-a516-da17feaf0da0", "metadata": {}, "outputs": [], "source": [ "b2.p = 3\n", "\n", "print(b1.p, b2.p)" ] }, { "cell_type": "markdown", "id": "701e26c3-6eb0-4c76-b453-cc37be06ac41", "metadata": {}, "source": [ "and any subsequent changes to `b1.p` will not be reflected by `b2`:" ] }, { "cell_type": "code", "execution_count": null, "id": "50aedbea-7902-4f1a-8e98-6866a965eda8", "metadata": {}, "outputs": [], "source": [ "b1.p = 27\n", "\n", "b2.p" ] }, { "cell_type": "markdown", "id": "07d982cd-c0f1-496f-8086-28a882266901", "metadata": {}, "source": [ "If a new reference is assigned then the old `Watcher`(s) will be removed and if the new value also represents a reference new `Watcher`(s) will be registered." ] }, { "cell_type": "markdown", "id": "c3e3d3aa", "metadata": {}, "source": [ "# Other notes\n", "\n", "Once we have Parameter descriptors and the metaclasses, there is relatively little left for the Parameterized class itself to do:\n", "\n", "- implementing the rest of [dependencies and watchers](Dependencies_and_Watchers.ipynb)\n", "- providing a constructor that lets you set instance parameters\n", "- instantiating and providing the `.param` accessor for invoking methods and accessing the Parameter objects\n", "- handling state saving and restoring (pickling)\n", "\n", "And that's it for the core of Param! There are other behaviors implemented at the level of specific Parameters, but those are typically localized and can be understood by reading the class docstring for that Parameter type." ] } ], "metadata": { "language_info": { "name": "python", "pygments_lexer": "ipython3" } }, "nbformat": 4, "nbformat_minor": 5 } param-2.1.1/doc/user_guide/Logging_and_Warnings.ipynb000066400000000000000000000224401463636336300226550ustar00rootroot00000000000000{ "cells": [ { "cell_type": "markdown", "id": "7627a6fe", "metadata": {}, "source": [ "# Logging and Warnings\n", "\n", "`Parameterized` objects provide methods for displaying logging messages and warnings in a way that can be controlled and redirected globally using the standard Python [logging](https://docs.python.org/3/library/logging.html) module (see the [logging cookbook](https://docs.python.org/3/howto/logging.html)). Compared to using a Python logger directly, using these methods inside your Parameterized class helps users by making the messages consistent, each prepending with information about the Parameterized object where the call was made so that your users can understand what the message indicates.\n", "\n", "By default, a Python logger named `param` will be instantiated to do the logging, but another logger can be supplied by setting `param.parameterized.logger` to it after importing `param.parameterized`.\n", "\n", "\n", "## Writing log messages\n", "\n", "Each logging message has an associated logging `level` that indicates how severe a condition is being described (DEBUG, VERBOSE, INFO (aka \"message\"), WARNING, ERROR, or CRITICAL). These levels are as defined by the logging module, except for the addition of VERBOSE as a level intermediate between DEBUG (internal debugging information) and INFO (user-relevant non-error messages).\n", "\n", "The typical way to print a message is to call `.param.log()` with one of the following logging levels:\n", "\n", "- `.param.log(param.DEBUG, ...)`: Detailed debugging information, not displayed onscreen by default.\n", "- `.param.log(param.VERBOSE, ...)`: Additional sometimes-useful information, not displayed onscreen by default.\n", "- `.param.log(param.INFO, ...)`: Informative message, displayed onscreen by default.\n", "- `.param.log(param.WARNING, ...)`: Warning of an unexpected or inappropriate but non-fatal condition, displayed onscreen by default.\n", "\n", "For convenience and to allow eventual integration with the Python [warnings](https://docs.python.org/3/library/warnings.html) module, `.param.warning(` is available as an alias for `.param.log(param.WARNING,`:\n", "\n", "- `.param.warning(...)`: Warning of an unexpected or inappropriate but non-fatal condition, displayed onscreen by default.\n", "\n", "The arguments accepted in each case are the same as those of [logging.debug()](https://docs.python.org/3/library/logging.html#logging.debug). Specifically, each call is like `.param.debug(msg, *args, **kw)`, where `msg` is an [old-style ('%') format string](https://wiki.python.org/moin/StringFormatting) and the `args` and `kwargs` will be merged with that format string. E.g.:" ] }, { "cell_type": "code", "execution_count": null, "id": "1daef0ad", "metadata": {}, "outputs": [], "source": [ "import param\n", "\n", "desired = 1\n", "actual = 5\n", "\n", "param.main.param.log(param.INFO, \"Welcome!\")\n", "param.main.param.log(param.VERBOSE, \"Local variables: %s\", locals())\n", "param.main.param.log(param.WARNING, \"Value %02d is not %d\", actual, desired)\n", "param.main.param.warning(\"Value %02d is not %d\", actual, desired)" ] }, { "cell_type": "markdown", "id": "ff2462c4", "metadata": {}, "source": [ "Here we've used the default global Parameterized object `param.main`, useful for generic module-level messages, but more often you would make a call like `self.param.log()` _inside_ a Parameterized class instead. You can see that the messages are each prefixed by the logging level, `param` (the name of the default logger), and the name of this object (`main` in this case). You can also see that, by default, verbose messages are not actually printed.\n", "\n", "You may wonder (a) why the formatting string is \"old style\" , and (b) why the formatting values \"actual, desired\" are not combined directly with the formatting string. I.e., why not just use a Python3 f-string, like:" ] }, { "cell_type": "code", "execution_count": null, "id": "9f3b2a86", "metadata": {}, "outputs": [], "source": [ "param.main.param.log(param.WARNING, f\"Value {actual:02} is not {desired}\") # Discouraged!" ] }, { "cell_type": "markdown", "id": "4a96c56b", "metadata": {}, "source": [ "The answer is that particularly for `debug` and `verbose` messages that could occur inside frequently executed code, we want logging to be \"lazy\", in that we do not want to render a string representation for `actual`, `desired`, etc. unless we are actually printing the message. If we use an f-string or any other locally formatted string, the string formatting will be done _whether_ _or_ _not_ the message will be displayed, potentially causing drastic slowdowns in your code. For instance, in the code above, the entire `locals()` dictionary would be iterated over, printed to strings. Of course, since the message isn't being printed in that case, the entire format string would then be discarded, greatly slowing down the code without producing any output. To avoid that, the logging module has to defer the string substitution and handle that itself, and it was written before Python3, so it only supports old-style substitution. So, even though it is more awkward, it is highly recommended to use this old-style, lazy string formatting support." ] }, { "cell_type": "markdown", "id": "aeeda03b", "metadata": {}, "source": [ "## Controlling the logging level\n", "\n", "You can use the `param.parameterized.logging_level` context manager to temporarily reduce or elevate the logging level while you execute code:" ] }, { "cell_type": "code", "execution_count": null, "id": "f09f288a", "metadata": {}, "outputs": [], "source": [ "with param.logging_level('CRITICAL'):\n", " param.main.param.log(param.INFO, \"Message 1\")\n", " param.main.param.log(param.VERBOSE, \"Verbose 1\")" ] }, { "cell_type": "code", "execution_count": null, "id": "85dbe122", "metadata": {}, "outputs": [], "source": [ "with param.logging_level('DEBUG'):\n", " param.main.param.log(param.INFO, \"Message 2\")\n", " param.main.param.log(param.VERBOSE, \"Verbose 2\")" ] }, { "cell_type": "markdown", "id": "3b881898", "metadata": {}, "source": [ "You can also set the value more globally (and permanently) on the logger object:" ] }, { "cell_type": "code", "execution_count": null, "id": "a429ac57", "metadata": {}, "outputs": [], "source": [ "param.get_logger().setLevel(param.DEBUG)" ] }, { "cell_type": "code", "execution_count": null, "id": "7afeb934", "metadata": {}, "outputs": [], "source": [ "param.main.param.log(param.INFO, \"Message 2\")\n", "param.main.param.log(param.VERBOSE, \"Verbose 2\")" ] }, { "cell_type": "markdown", "id": "4ebe358c", "metadata": {}, "source": [ "For testing, continuous integration (CI), or other specific applications, you can also set `param.parameterized.warnings_as_exceptions = True`, which will cause your program to raise an exception the first time it encounters any warning." ] }, { "cell_type": "markdown", "id": "54f42ad0", "metadata": {}, "source": [ "# Controlling the formatting of log messages\n", "\n", "The Python logging module provides many options for configuring how the log messages are generated. For complete control, you can instantiate your own logger and set `param.parameterized.logger` to it after importing `param.parameterized`.\n", "\n", "A hook is provided for the relatively common case when you want to prefix each message with the time of day, a progress indication, a simulator time, or some other indication of a global state. Specifically, you can set `param.parameterized.dbprint_prefix` to a callable object returning a string. The object will be called when constructing each message:" ] }, { "cell_type": "code", "execution_count": null, "id": "be2a5f2c", "metadata": {}, "outputs": [], "source": [ "from datetime import datetime\n", "#param.parameterized.warnings_as_exceptions = True\n", "\n", "param.parameterized.dbprint_prefix=lambda: str(datetime.now())\n", "\n", "param.main.param.warning(\"Message 4\")\n", "param.main.param.warning(\"Message 5\")" ] }, { "cell_type": "markdown", "id": "e94c2fa9", "metadata": {}, "source": [ "## Counting warnings\n", "\n", "Typically, a program will abort if an error is encountered, making such a condition hard to miss, but warning messages might be lost in a sea of informational, verbose, or debug messages. Param keeps track of how many times `.param.warning(...)` (or its alias `.param.log(param.WARNING, ...)`) has been called, and it is often useful to print out that value at the end of a program run:" ] }, { "cell_type": "code", "execution_count": null, "id": "348f9d37", "metadata": {}, "outputs": [], "source": [ "print(f\"Run completed. {param.parameterized.warning_count} warnings were encountered.\")" ] } ], "metadata": { "language_info": { "name": "python", "pygments_lexer": "ipython3" } }, "nbformat": 4, "nbformat_minor": 5 } param-2.1.1/doc/user_guide/Outputs.ipynb000066400000000000000000000137241463636336300202650ustar00rootroot00000000000000{ "cells": [ { "cell_type": "markdown", "id": "fa87315d", "metadata": {}, "source": [ "# Outputs\n", "\n", "Parameters are typically used for the _inputs_ to a Parameterized objects, declaring as precisely as possible which inputs are allowed. You can also declare what _outputs_ are generated by a Parameterized, using `@param.output`. At present, Param itself does not use output declarations internally, but they can be queried by Param-based programs to allow safe chaining of Parameterized objects into pipelines, such as for the [boxflow](https://github.com/ioam/boxflow) dataflow programming system or the multi-page pipelines in [Panel](https://panel.holoviz.org/how_to/pipeline/index.html). \n", "\n", "`@param.output` annotates a method on a Parameterized class to declare that it returns one or more outputs of a specified type. As a simple example, this declaration indicates that the given function returns a number:" ] }, { "cell_type": "code", "execution_count": null, "id": "e4d4a11c", "metadata": {}, "outputs": [], "source": [ "import param\n", "\n", "class P(param.Parameterized):\n", " a = param.Number(default=5, bounds=(0, 10))\n", " b = param.Number(default=5, bounds=(0, 10))\n", "\n", " @param.output(param.Number)\n", " def product(self):\n", " return self.a * self.b\n", " \n", "p = P(a=2, b=3)\n", "p.product()" ] }, { "cell_type": "markdown", "id": "0dafc330", "metadata": {}, "source": [ "If a program wants to know if the output from object `p` is suitable for connecting to an input of some other object `q`, it can query this output type:" ] }, { "cell_type": "code", "execution_count": null, "id": "461604e5", "metadata": {}, "outputs": [], "source": [ "p.param.outputs()" ] }, { "cell_type": "markdown", "id": "787ac37c", "metadata": {}, "source": [ "This return value is of the form _name_: (_type_, _method_, _index_), which here indicates that:\n", "- object `p` generates one output called `product` \n", "- `product` is of type `param.Number`\n", "- `product` can be generated by invoking method `p.product()` \n", "- `product` will be returned directly as a single value by the method (indicated by an index of `None`; otherwise the index would indicate the position of this particular output in a tuple returned by the method)\n", "\n", "An automated program could use this information to decide whether the output from one Parameterized is suitable for connecting to the input of another.\n", "\n", "The above example is typical, but let's review the other output declarations accepted by `@param.output`. The simplest declaration declares the method returns an object of the same name as the method, without any type guarantees:\n", "\n", "```python\n", "@param.output()\n", "def product(self): return self.a * self.b\n", "```\n", "\n", "More commonly, a parameter type will be specified as above, indicating that this method returns a value of that type, again defaulting to the method name:\n", "\n", "```python\n", "@param.output(param.Number())\n", "def product2(self): return self.a * self.b\n", "```\n", "\n", "The output name can be declared explicitly as a keyword argument if desired, e.g. if the method name is not a suitable output name:\n", "\n", "```python\n", "@param.output(result=param.Number())\n", "def __call__(self): return self.a * self.b\n", "```\n", "\n", "Multiple outputs may be declared using keywords mapping from output name to the type:\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, "id": "e7863dc4", "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, "id": "8265c0b4", "metadata": {}, "outputs": [], "source": [ "q.param.outputs()" ] }, { "cell_type": "markdown", "id": "3a68b349", "metadata": {}, "source": [ "Here, you can see that there are _two_ outputs from `products()`, 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-2.1.1/doc/user_guide/Parameter_Types.ipynb000066400000000000000000001234241463636336300217050ustar00rootroot00000000000000{ "cells": [ { "cell_type": "markdown", "id": "7e085e53", "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", "id": "e9787a67", "metadata": {}, "source": [ "The full behavior of these types is covered in the [Reference Manual](https://param.holoviz.org/reference.html#param-module). 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", "id": "0edcb97b", "metadata": {}, "source": [ "## Strings\n", "\n", "- [`param.String`](param.String): String value, with optional regular expression (regex) constraints\n", "\n", "A [`param.String`](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, "id": "625ce570", "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, "id": "b72b6908", "metadata": {}, "outputs": [], "source": [ "with param.exceptions_summarized():\n", " s.s = 5" ] }, { "cell_type": "code", "execution_count": null, "id": "1dac26fc", "metadata": {}, "outputs": [], "source": [ "s.s = 'Forever after'" ] }, { "cell_type": "code", "execution_count": null, "id": "924db709", "metadata": {}, "outputs": [], "source": [ "with param.exceptions_summarized():\n", " s.s = 'four of spades'" ] }, { "cell_type": "code", "execution_count": null, "id": "d094459d", "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, "id": "79585706", "metadata": {}, "outputs": [], "source": [ "i.ip_address=\"10.0.0.2\"\n", "i.email = \"user@gmail.com\"" ] }, { "cell_type": "code", "execution_count": null, "id": "82efd99d", "metadata": {}, "outputs": [], "source": [ "with param.exceptions_summarized():\n", " i.ip_address='192.x.1.x'" ] }, { "cell_type": "markdown", "id": "65f03a46", "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, "id": "93ab3a83", "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, "id": "9db80a95", "metadata": {}, "outputs": [], "source": [ "c.c = 'lemonchiffon'\n", "c.c" ] }, { "cell_type": "code", "execution_count": null, "id": "e14d63f1", "metadata": {}, "outputs": [], "source": [ "with param.exceptions_summarized():\n", " c.c = 'puce'" ] }, { "cell_type": "code", "execution_count": null, "id": "b8d880a5", "metadata": {}, "outputs": [], "source": [ "with param.exceptions_summarized():\n", " c.c = '#abcdefg'" ] }, { "cell_type": "markdown", "id": "91e09506", "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, "id": "7a145fab", "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, "id": "cb91b352", "metadata": {}, "outputs": [], "source": [ "with param.exceptions_summarized():\n", " b.b=1" ] }, { "cell_type": "code", "execution_count": null, "id": "da7a9a2f", "metadata": {}, "outputs": [], "source": [ "with param.exceptions_summarized():\n", " b.b=None" ] }, { "cell_type": "code", "execution_count": null, "id": "e5f68ef1", "metadata": {}, "outputs": [], "source": [ "b.n = True\n", "b.n = None" ] }, { "cell_type": "markdown", "id": "f31c0f1e", "metadata": {}, "source": [ "## Numbers\n", "\n", "- `param.Number`: Python floats, int, and Numpy values.\n", "- `param.Integer`: Python and Numpy 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", "Using Number parameters:" ] }, { "cell_type": "code", "execution_count": null, "id": "2ff07754", "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, "id": "8e95f919", "metadata": {}, "outputs": [], "source": [ "N.param.n.set_in_bounds(a,-10)\n", "a.n" ] }, { "cell_type": "code", "execution_count": null, "id": "c8463d63", "metadata": {}, "outputs": [], "source": [ "a.param.n.set_in_bounds(a,-5)\n", "a.n" ] }, { "cell_type": "code", "execution_count": null, "id": "5db79041", "metadata": {}, "outputs": [], "source": [ "a.param.n.set_in_bounds(a,75)\n", "a.n" ] }, { "cell_type": "code", "execution_count": null, "id": "17a2faaf", "metadata": {}, "outputs": [], "source": [ "a.param.n.get_soft_bounds()" ] }, { "cell_type": "code", "execution_count": null, "id": "e887e3e4", "metadata": {}, "outputs": [], "source": [ "with param.exceptions_summarized():\n", " a.n = -5" ] }, { "cell_type": "code", "execution_count": null, "id": "3ae9ca52", "metadata": {}, "outputs": [], "source": [ "a.n = 500\n", "a.n" ] }, { "cell_type": "code", "execution_count": null, "id": "764dd765", "metadata": {}, "outputs": [], "source": [ "with param.exceptions_summarized():\n", " a.i=5.7" ] }, { "cell_type": "code", "execution_count": null, "id": "d3369af4", "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, "id": "caef921b", "metadata": {}, "outputs": [], "source": [ "with param.exceptions_summarized():\n", " d.d = 2022" ] }, { "cell_type": "code", "execution_count": null, "id": "995807bd", "metadata": {}, "outputs": [], "source": [ "d.t = datetime.date(1900, 1, 1)\n", "d.t" ] }, { "cell_type": "code", "execution_count": null, "id": "2c667efd", "metadata": {}, "outputs": [], "source": [ "with param.exceptions_summarized():\n", " d.d = datetime.datetime.fromisoformat('2002-12-25T00:00')" ] }, { "cell_type": "markdown", "id": "4847bc8b", "metadata": {}, "source": [ "## Tuples" ] }, { "cell_type": "markdown", "id": "0602f947", "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, "id": "aa7494a7", "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, "id": "89b12646", "metadata": {}, "outputs": [], "source": [ "b.t = (50.2,50.3)\n", "b.t" ] }, { "cell_type": "code", "execution_count": null, "id": "b1f1f4c9", "metadata": {}, "outputs": [], "source": [ "with param.exceptions_summarized():\n", " b.t = 5" ] }, { "cell_type": "code", "execution_count": null, "id": "2e481b37", "metadata": {}, "outputs": [], "source": [ "with param.exceptions_summarized():\n", " b.t = (5,5,5)" ] }, { "cell_type": "code", "execution_count": null, "id": "03fb63e9", "metadata": {}, "outputs": [], "source": [ "with param.exceptions_summarized():\n", " b.t = (5,\"5\")" ] }, { "cell_type": "code", "execution_count": null, "id": "cebd0b19", "metadata": {}, "outputs": [], "source": [ "with param.exceptions_summarized():\n", " b.t = (-500,500)" ] }, { "cell_type": "code", "execution_count": null, "id": "e01e474a", "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, "id": "b84a21ed", "metadata": {}, "outputs": [], "source": [ "with param.exceptions_summarized():\n", " c.d=(1905, 1907)" ] }, { "cell_type": "markdown", "id": "4befeab9", "metadata": {}, "source": [ "## Lists" ] }, { "cell_type": "markdown", "id": "76db67ce", "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, "id": "e8351f51", "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, "id": "6dbabdcb", "metadata": {}, "outputs": [], "source": [ "with param.exceptions_summarized():\n", " e.ls = [1,2]" ] }, { "cell_type": "code", "execution_count": null, "id": "5accd58c", "metadata": {}, "outputs": [], "source": [ "with param.exceptions_summarized():\n", " e.ls = [str(i) for i in range(20)]" ] }, { "cell_type": "code", "execution_count": null, "id": "811df0b8", "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, "id": "51c77d9e", "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, "id": "db1b102e", "metadata": {}, "outputs": [], "source": [ "from math import fabs\n", "\n", "ex.during=[abs]\n", "ex()" ] }, { "cell_type": "markdown", "id": "3571b46b", "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 in the current working directory (obtained with `os.getcwd()`). 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", "When `check_exists` is set to `False` (default is `True`) the provided path can optionally exist. This is for instance useful to declare an output file path that is meant to be created at a later stage in a process. In the default case the path must exist, on Parameter instantiation and setting.\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, "id": "cb449123", "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", " o = param.Filename('output.csv', check_exists=False)\n", " \n", "p = P()\n", "p.p" ] }, { "cell_type": "code", "execution_count": null, "id": "d3d08aa2", "metadata": {}, "outputs": [], "source": [ "p.p = '/usr/lib'\n", "p.p" ] }, { "cell_type": "code", "execution_count": null, "id": "f5a50ea9", "metadata": {}, "outputs": [], "source": [ "p.f" ] }, { "cell_type": "code", "execution_count": null, "id": "79112698", "metadata": {}, "outputs": [], "source": [ "with param.exceptions_summarized():\n", " p.f = '/usr/lib'" ] }, { "cell_type": "code", "execution_count": null, "id": "266a525c", "metadata": {}, "outputs": [], "source": [ "p.d" ] }, { "cell_type": "code", "execution_count": null, "id": "6da8779e", "metadata": {}, "outputs": [], "source": [ "with param.exceptions_summarized():\n", " p.d = 'Parameter_Types.ipynb'" ] }, { "cell_type": "code", "execution_count": null, "id": "53225144", "metadata": {}, "outputs": [], "source": [ "p.o # the output file doesn't exist yet" ] }, { "cell_type": "code", "execution_count": null, "id": "863b9817", "metadata": {}, "outputs": [], "source": [ "with open(p.o, 'w') as f:\n", " f.write('Param is awesome!')\n", "\n", "p.o # it now exists, getting its value resolve the full file path" ] }, { "cell_type": "markdown", "id": "573079ef", "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.\n", "\n", "To make it easier to modify the collection of `objects`, they are wrapped in a container that supports both list-style and dictionary-style methods. This approach ensures that there is a consistent API for updating the `objects` and that modifying the objects in place still triggers an event.\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, "id": "98852f74", "metadata": {}, "outputs": [], "source": [ "colors = [\"red\",\"green\",\"blue\"]\n", "\n", "class S(param.Parameterized):\n", " o = param.Selector(objects=colors)\n", " ls = param.ListSelector(colors[0:2], objects=colors)\n", " \n", "s = S()\n", "s.o" ] }, { "cell_type": "code", "execution_count": null, "id": "49fb6839", "metadata": {}, "outputs": [], "source": [ "s.o = \"green\"\n", "s.o" ] }, { "cell_type": "code", "execution_count": null, "id": "b1d118a2", "metadata": {}, "outputs": [], "source": [ "with param.exceptions_summarized():\n", " s.o = \"yellow\"" ] }, { "cell_type": "code", "execution_count": null, "id": "2f08db09", "metadata": {}, "outputs": [], "source": [ "with param.exceptions_summarized():\n", " s.o = 42" ] }, { "cell_type": "code", "execution_count": null, "id": "2beac492", "metadata": {}, "outputs": [], "source": [ "s.ls" ] }, { "cell_type": "code", "execution_count": null, "id": "9ddf7d53", "metadata": {}, "outputs": [], "source": [ "s.ls=['blue']\n", "s.ls" ] }, { "cell_type": "code", "execution_count": null, "id": "1c22dcd1", "metadata": {}, "outputs": [], "source": [ "with param.exceptions_summarized():\n", " s.ls=['red','yellow']\n", " s.ls" ] }, { "cell_type": "code", "execution_count": null, "id": "dc30268c", "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, "id": "f4abfeee", "metadata": {}, "outputs": [], "source": [ "f.param.f.objects[0:3]" ] }, { "cell_type": "code", "execution_count": null, "id": "b58509a6", "metadata": {}, "outputs": [], "source": [ "f.fs = f.param.fs.objects[0:2]\n", "f.fs" ] }, { "cell_type": "markdown", "id": "3084902c", "metadata": {}, "source": [ "## ClassSelectors\n", "\n", "- `param.ClassSelector`: An instance or subclass of a class or tuple of classes\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, "id": "70809e48", "metadata": {}, "outputs": [], "source": [ "class C(param.Parameterized):\n", " e_instance = param.ClassSelector(default=ZeroDivisionError(\"1/0\"), class_=ArithmeticError)\n", " e_class = param.ClassSelector(\n", " default=ZeroDivisionError, class_=(ArithmeticError, LookupError),\n", " is_instance=False\n", " )\n", " \n", "c = C(e_class=OverflowError)\n", "c.e_class, c.e_instance" ] }, { "cell_type": "code", "execution_count": null, "id": "b3be8c3d", "metadata": {}, "outputs": [], "source": [ "c.param.e_instance.get_range()" ] }, { "cell_type": "code", "execution_count": null, "id": "92c50c04", "metadata": {}, "outputs": [], "source": [ "c.param.e_class.get_range()" ] }, { "cell_type": "code", "execution_count": null, "id": "0b499411", "metadata": {}, "outputs": [], "source": [ "with param.exceptions_summarized():\n", " c.e_class = Exception" ] }, { "cell_type": "code", "execution_count": null, "id": "150cc654", "metadata": {}, "outputs": [], "source": [ "with param.exceptions_summarized():\n", " c.e_instance = ArithmeticError" ] }, { "cell_type": "code", "execution_count": null, "id": "0d3d9c56", "metadata": {}, "outputs": [], "source": [ "c.e_instance = ArithmeticError()\n", "c.e_instance" ] }, { "cell_type": "markdown", "id": "dce7b733", "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, "id": "067f20c1", "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, "id": "a321de5d", "metadata": {}, "outputs": [], "source": [ "with param.exceptions_summarized():\n", " d.d=[(\"a\",1)]" ] }, { "cell_type": "code", "execution_count": null, "id": "50f01ab8", "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, "id": "bb1097b7", "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, "id": "44a2a32d", "metadata": {}, "outputs": [], "source": [ "with param.exceptions_summarized():\n", " df = pd.DataFrame({'a':[-2], 'b':[-1]})\n", " d.f = df" ] }, { "cell_type": "markdown", "id": "dfbef370", "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, "id": "f4b40d09", "metadata": {}, "outputs": [], "source": [ "from param.parameterized import classlist\n", "\n", "classlist(D)" ] }, { "cell_type": "markdown", "id": "fbd3c5d3", "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, "id": "abf691ca", "metadata": {}, "outputs": [], "source": [ "param.descendents(param.SelectorBase)" ] }, { "cell_type": "markdown", "id": "7cfe3c7b", "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", "id": "bd887647", "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, "id": "1f3b7fc2", "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, "id": "70cd5a08", "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, "id": "9c9aad93", "metadata": {}, "outputs": [], "source": [ "d.a()" ] }, { "cell_type": "code", "execution_count": null, "id": "26927d79", "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-2.1.1/doc/user_guide/ParameterizedFunctions.ipynb000066400000000000000000000100321463636336300232540ustar00rootroot00000000000000{ "cells": [ { "cell_type": "markdown", "id": "2f8bed81", "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, "id": "4c52a416", "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, "id": "67e42bf5", "metadata": {}, "outputs": [], "source": [ "multiply(left=3, right=7)" ] }, { "cell_type": "code", "execution_count": null, "id": "16f1938f", "metadata": {}, "outputs": [], "source": [ "multiply.left = 7\n", "multiply(right = 10)" ] }, { "cell_type": "markdown", "id": "0e00795d", "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", "id": "99af9344", "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, "id": "db166711", "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-2.1.1/doc/user_guide/Parameters.ipynb000066400000000000000000001134431463636336300207040ustar00rootroot00000000000000{ "cells": [ { "cell_type": "markdown", "id": "edc2a19c", "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", "id": "49c75fc9", "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", "- **allow_refs**: Whether to allow the Parameter to accept references to other Parameters that will be dynamically resolved.\n", "- **nested_refs**: Whether references should be resolved even when they are nested inside a container.\n", "\n", "Most of these settings (apart from **name**) are accepted as keyword arguments to the Parameter's constructor, with `default` mostly also accepted as the only positional argument:" ] }, { "cell_type": "code", "execution_count": null, "id": "15441c20", "metadata": {}, "outputs": [], "source": [ "import param\n", "from param import Parameter, Parameterized\n", "\n", "p = Parameter(default=42, doc=\"The answer\", constant=True)\n", "p.default" ] }, { "cell_type": "code", "execution_count": null, "id": "8972998f", "metadata": {}, "outputs": [], "source": [ "p.allow_None" ] }, { "cell_type": "code", "execution_count": null, "id": "40523986", "metadata": {}, "outputs": [], "source": [ "p.doc" ] }, { "cell_type": "markdown", "id": "880c08d0", "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, "id": "4161f316", "metadata": {}, "outputs": [], "source": [ "class A(Parameterized):\n", " question = Parameter(\"What is it?\", doc=\"The question\")\n", " answer = Parameter(default=2, constant=True, doc=\"The answer\")\n", " ultimate_answer = Parameter(default=42, readonly=True, doc=\"The real answer\")\n", "\n", "a = A(question=\"How is it?\", answer=\"6\")" ] }, { "cell_type": "markdown", "id": "23141898", "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, "id": "016377f1", "metadata": {}, "outputs": [], "source": [ "with param.exceptions_summarized():\n", " A(ultimate_answer=\"no\")" ] }, { "cell_type": "markdown", "id": "876b69fa", "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, "id": "76e87cfd", "metadata": {}, "outputs": [], "source": [ "a.question" ] }, { "cell_type": "code", "execution_count": null, "id": "372f24e5", "metadata": {}, "outputs": [], "source": [ "a.answer" ] }, { "cell_type": "markdown", "id": "3206cfdb", "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, "id": "9954dc0e", "metadata": {}, "outputs": [], "source": [ "A.question" ] }, { "cell_type": "code", "execution_count": null, "id": "c855c562", "metadata": {}, "outputs": [], "source": [ "A.answer" ] }, { "cell_type": "code", "execution_count": null, "id": "8c59c6b5", "metadata": {}, "outputs": [], "source": [ "b = A()\n", "b.answer" ] }, { "cell_type": "markdown", "id": "b3589305", "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, via attribute or key:" ] }, { "cell_type": "code", "execution_count": null, "id": "7786fe6d", "metadata": {}, "outputs": [], "source": [ "a.param['question']" ] }, { "cell_type": "code", "execution_count": null, "id": "700999a6", "metadata": {}, "outputs": [], "source": [ "a.param.question" ] }, { "cell_type": "code", "execution_count": null, "id": "766e3a42", "metadata": {}, "outputs": [], "source": [ "a.param.question.name" ] }, { "cell_type": "code", "execution_count": null, "id": "e2ee5ef0", "metadata": {}, "outputs": [], "source": [ "a.param.question.default" ] }, { "cell_type": "code", "execution_count": null, "id": "f92224e9", "metadata": {}, "outputs": [], "source": [ "A.param.question.default" ] }, { "cell_type": "markdown", "id": "1399e0b3", "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, "id": "1622066a", "metadata": {}, "outputs": [], "source": [ "with param.exceptions_summarized():\n", " a.question = True\n", " a.answer = 5" ] }, { "cell_type": "code", "execution_count": null, "id": "e1232083", "metadata": {}, "outputs": [], "source": [ "a.question" ] }, { "cell_type": "markdown", "id": "f9ecc506", "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, "id": "25f5da3e", "metadata": {}, "outputs": [], "source": [ "with param.edit_constant(a):\n", " a.answer = 30\n", "a.answer" ] }, { "cell_type": "markdown", "id": "252aa858", "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", "id": "0ce2b60e", "metadata": {}, "source": [ "## Parameter inheritance\n", "\n", "`Parameter` objects and their metadata are inherited in a hierarchy of `Parameterized` objects. Let's see how that works:" ] }, { "cell_type": "code", "execution_count": null, "id": "3b743022", "metadata": {}, "outputs": [], "source": [ "class A(Parameterized):\n", " question = Parameter(\"What is it?\", doc=\"The question\")\n", " answer = Parameter(default=2, constant=True, doc=\"The answer\")\n", " ultimate_answer = Parameter(default=42, readonly=True, doc=\"The real answer\")\n", "\n", "class B(A):\n", " ultimate_answer = Parameter(default=84)\n", "\n", "b = B()\n", "b.question" ] }, { "cell_type": "code", "execution_count": null, "id": "e2171651", "metadata": {}, "outputs": [], "source": [ "A.question = \"How are you?\"" ] }, { "cell_type": "code", "execution_count": null, "id": "91292e0a", "metadata": {}, "outputs": [], "source": [ "b.question" ] }, { "cell_type": "markdown", "id": "17692a63", "metadata": {}, "source": [ "Here you can see that B inherits `question` 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, "id": "a57b907a", "metadata": {}, "outputs": [], "source": [ "b.question = \"Why?\"\n", "A.question = \"Who?\"\n", "b.question" ] }, { "cell_type": "markdown", "id": "fc265dfa", "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`.\n" ] }, { "cell_type": "code", "execution_count": null, "id": "0917eddf", "metadata": {}, "outputs": [], "source": [ "b.param.ultimate_answer.constant" ] }, { "cell_type": "code", "execution_count": null, "id": "4c8c7b11", "metadata": {}, "outputs": [], "source": [ "b.param.ultimate_answer.readonly" ] }, { "cell_type": "code", "execution_count": null, "id": "86039127", "metadata": {}, "outputs": [], "source": [ "b.ultimate_answer" ] }, { "cell_type": "code", "execution_count": null, "id": "84dc2df9", "metadata": {}, "outputs": [], "source": [ "b.param.ultimate_answer.default" ] }, { "cell_type": "code", "execution_count": null, "id": "4a549ab5", "metadata": {}, "outputs": [], "source": [ "b.param.ultimate_answer.doc" ] }, { "cell_type": "markdown", "id": "f6bb48df", "metadata": {}, "source": [ "Looking at the metadata values of `ultimate_answer` on `b` or `B` you can see that:\n", "\n", "- All the default metadata values like `constant`, `allow_none`, ..., were inherited from the base `Parameter` object provided by Param\n", "- The `read_only` and `doc` metadata values were inherited from `A`\n", "- The `default` metadata value of `ultimate_answer` in `B` overrode the value provided in `A`.\n", "\n", "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", "`help(b)` or `help(B)` will list all parameters. You can also prefix or suffix a Parameterized object with `?` in an IPython console/Notebook to display the help:" ] }, { "cell_type": "code", "execution_count": null, "id": "dd711290-b6f0-4e9e-bbce-c400da27a3c2", "metadata": {}, "outputs": [], "source": [ "B?" ] }, { "cell_type": "markdown", "id": "54ecbc24-14eb-4c07-8a00-0fb79b6241da", "metadata": {}, "source": [ "\"Param" ] }, { "cell_type": "markdown", "id": "3683dec7", "metadata": {}, "source": [ "## Parameter value instantiation\n", "\n", "So 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. While the default behavior shown above is appropriate for **immutable attributes**, what happens if the value (unlike Python strings) is mutable? Things get a lot more complex." ] }, { "cell_type": "code", "execution_count": null, "id": "b9fa3c69", "metadata": {}, "outputs": [], "source": [ "s = [1, 2, 3]\n", "\n", "class C(Parameterized):\n", " s1 = Parameter(s, doc=\"A sequence\")\n", " s2 = Parameter(s, doc=\"Another sequence\")\n", "\n", "c = C()" ] }, { "cell_type": "markdown", "id": "fe928bac", "metadata": {}, "source": [ "Here, both parameters `s1` and `s2` effectively point to the same underlying sequence `s`:" ] }, { "cell_type": "code", "execution_count": null, "id": "ba3863a5", "metadata": {}, "outputs": [], "source": [ "c.s1 is c.s2" ] }, { "cell_type": "code", "execution_count": null, "id": "0b9004be", "metadata": {}, "outputs": [], "source": [ "s[1] *= 5" ] }, { "cell_type": "code", "execution_count": null, "id": "81f40ddf", "metadata": {}, "outputs": [], "source": [ "s" ] }, { "cell_type": "code", "execution_count": null, "id": "e69157b7", "metadata": {}, "outputs": [], "source": [ "c.s1" ] }, { "cell_type": "code", "execution_count": null, "id": "344bae37", "metadata": {}, "outputs": [], "source": [ "c.s1[2] = 'a'" ] }, { "cell_type": "code", "execution_count": null, "id": "baf5b9f6", "metadata": {}, "outputs": [], "source": [ "c.s1" ] }, { "cell_type": "code", "execution_count": null, "id": "5c87a5d6", "metadata": {}, "outputs": [], "source": [ "c.s2" ] }, { "cell_type": "markdown", "id": "08324f49", "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` (default is `False`):" ] }, { "cell_type": "code", "execution_count": null, "id": "5c92d85f", "metadata": {}, "outputs": [], "source": [ "s = [1,2,3]\n", "\n", "class D(Parameterized):\n", " s1 = Parameter(default=s, doc=\"A sequence\", instantiate=True)\n", " s2 = Parameter(default=s, doc=\"Another sequence\", instantiate=True)\n", "\n", "d = D()" ] }, { "cell_type": "markdown", "id": "325b9bbe", "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, "id": "4c245a25", "metadata": {}, "outputs": [], "source": [ "d.s1 is d.s2" ] }, { "cell_type": "code", "execution_count": null, "id": "509c07ac", "metadata": {}, "outputs": [], "source": [ "s *= 2" ] }, { "cell_type": "code", "execution_count": null, "id": "311dbcba", "metadata": {}, "outputs": [], "source": [ "s" ] }, { "cell_type": "code", "execution_count": null, "id": "9e5c5336", "metadata": {}, "outputs": [], "source": [ "d.s1" ] }, { "cell_type": "code", "execution_count": null, "id": "f9d142d7", "metadata": {}, "outputs": [], "source": [ "d.s1[2] = 'a'" ] }, { "cell_type": "code", "execution_count": null, "id": "3d9a12f0", "metadata": {}, "outputs": [], "source": [ "d.s2" ] }, { "cell_type": "markdown", "id": "3792939f", "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", "id": "6cfed7ae", "metadata": {}, "source": [ "## Parameter object 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, "id": "2f5af5da", "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, "id": "9f224f3b", "metadata": {}, "outputs": [], "source": [ "d1.param.s1.label" ] }, { "cell_type": "markdown", "id": "5ce34483", "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, "id": "787e9813", "metadata": {}, "outputs": [], "source": [ "class E(Parameterized):\n", " a = Parameter(default=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", "id": "c2e3d4a7", "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, "id": "5013fe69", "metadata": {}, "outputs": [], "source": [ "class S(param.Parameterized):\n", " l = Parameter(default=[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", "id": "6c7195fd", "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, "id": "37e160d4", "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", "id": "ed4e296f", "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", "id": "678b7a0e", "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 string representations 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", "- `Parameterize.param._repr_html_()`: A rich HTML representation of the object with its parameters listed in a table together with their metadata.\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, "id": "0a1521ba", "metadata": {}, "outputs": [], "source": [ "import param\n", "\n", "class Q(param.Parameterized):\n", " a = param.Number(default=39, bounds=(0,50), doc='Number a')\n", " b = param.String(default=\"str\", doc='A string')\n", "\n", "class P(Q):\n", " c = param.ClassSelector(default=Q(), class_=Q, doc='An instance of Q')\n", " e = param.ClassSelector(default=param.Parameterized(), class_=param.Parameterized, doc='A Parameterized instance')\n", " f = param.Range(default=(0,1), doc='A range')\n", "\n", "p = P(f=(2,3), c=P(f=(42,43)), name=\"demo\")" ] }, { "cell_type": "code", "execution_count": null, "id": "0b0d5b84", "metadata": {}, "outputs": [], "source": [ "p.__str__()" ] }, { "cell_type": "code", "execution_count": null, "id": "e119e92a", "metadata": {}, "outputs": [], "source": [ "p.__repr__()" ] }, { "cell_type": "markdown", "id": "799c1eeb-71c2-40a3-ad06-fd3ed8eda501", "metadata": {}, "source": [ "The HTML representation of a `Parameterized` instance or class is displayed when you call `.param` in a Notebook." ] }, { "cell_type": "code", "execution_count": null, "id": "4c17a53b-3acc-4e6e-ab93-e5d09728e1c5", "metadata": {}, "outputs": [], "source": [ "p.param" ] }, { "cell_type": "code", "execution_count": null, "id": "64628d11-7f2c-4718-bd8a-7a381e282047", "metadata": {}, "outputs": [], "source": [ "P.param" ] }, { "cell_type": "code", "execution_count": null, "id": "2ed3a3f0", "metadata": {}, "outputs": [], "source": [ "p.param.pprint(separator=\"\\n\")" ] }, { "cell_type": "markdown", "id": "b0acf461", "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, "id": "22b09e5d", "metadata": {}, "outputs": [], "source": [ "p.c=P(c=p)\n", "p.param.pprint()" ] }, { "cell_type": "markdown", "id": "3830ace8", "metadata": {}, "source": [ "## Parameterized namespace\n", "\n", "Param allows you to create Parameterized objects by inheriting from the `Parameterized` base class. Param has evolved over time to reduce its footprint and reserve as few as possible attributes on this namespace, to reduce the risk of name clashes and allow you to freely define your attribute names. Param reserves a few names that are described below, make sure not to override, unless it is stated it is allowed:\n", "\n", "- Public attributes:\n", " - `name`: Parameterized classes and instances have a name `String` Parameter, that by default is set to the class name when accessed from the class and to the class name appended with a 5 digit when accessed from the instance. You can override this Parameter by your own `String` Parameter if you need to.\n", " - `param`: Property that helps keep the Parameterized namespace clean and disambiguate between Parameter objects and parameter values, it gives access to a namespace that offers various methods (see the section below) to update and inspect the Parameterized object at hand.\n", "- Private attributes:\n", " - `_param__parameters`: Store the object returned by `.param` on the class\n", " - `_param__private`: Store various internal data on Parameterized class and instances\n", " - `_param_watchers` (deprecated in Param 2.0 and to be removed soon): Store a dictionary of instance watchers" ] }, { "cell_type": "code", "execution_count": null, "id": "01759e1b", "metadata": {}, "outputs": [], "source": [ "class P(param.Parameterized):\n", " a = param.Number()\n", " b = param.String()\n", "\n", "p = P()\n", "print(f'{P.name=}, {p.name=}')" ] }, { "cell_type": "code", "execution_count": null, "id": "e802c7c7", "metadata": {}, "outputs": [], "source": [ "def namespace(obj):\n", " return [o for o in dir(obj) if not o.startswith('__')]" ] }, { "cell_type": "code", "execution_count": null, "id": "5c702205", "metadata": {}, "outputs": [], "source": [ "namespace(P)" ] }, { "cell_type": "code", "execution_count": null, "id": "b74fb157", "metadata": {}, "outputs": [], "source": [ "namespace(p)" ] }, { "cell_type": "markdown", "id": "23979d82", "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 the `.param` subobject:\n", "\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. `.param.update` can also be used as a context manager to temporarily set values, that are restored to their original values when the context manager exits." ] }, { "cell_type": "code", "execution_count": null, "id": "1779f544", "metadata": {}, "outputs": [], "source": [ "p.param.update(a=0, b='start');\n", "print(p.a, p.b)" ] }, { "cell_type": "code", "execution_count": null, "id": "f1508201", "metadata": {}, "outputs": [], "source": [ "with p.param.update(a=1, b='temp'):\n", " print(f'In the context manager: {p.a=}, {p.b=}')\n", "print(f'After the context manager exits: {p.a=}, {p.b=}')" ] }, { "cell_type": "markdown", "id": "0b9e1d85", "metadata": {}, "source": [ "- `.param.values(onlychanged=False)`: A dict of name,value pairs for all parameters of this object" ] }, { "cell_type": "code", "execution_count": null, "id": "fd7f0eca", "metadata": {}, "outputs": [], "source": [ "p.param.values()" ] }, { "cell_type": "markdown", "id": "244a17d0", "metadata": {}, "source": [ "- `.param.objects(instance=True)`: Parameter objects of this instance or class" ] }, { "cell_type": "code", "execution_count": null, "id": "1baf4823", "metadata": {}, "outputs": [], "source": [ "p.param.objects()" ] }, { "cell_type": "markdown", "id": "1c5085ea", "metadata": {}, "source": [ "\n", "- `.param.add_parameter(param_name,param_obj)`: Dynamically add a new Parameter to this object's 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", "id": "3e8ab618", "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, "id": "89c1d341", "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()._validate_value(val, allow_None)\n", " if not isinstance(val, numbers.Number):\n", " raise ValueError(\n", " f\"EvenInteger parameter {self.name!r} must be a number, not {val!r}.\"\n", " )\n", " \n", " if not (val % 2 == 0):\n", " raise ValueError(\n", " f\"EvenInteger parameter {self.name!r} must be even, not {val!r}.\"\n", " )\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, "id": "80fbb171", "metadata": {}, "outputs": [], "source": [ "with param.exceptions_summarized():\n", " P(n=5, b=\"four\")" ] }, { "cell_type": "code", "execution_count": null, "id": "980b932b", "metadata": {}, "outputs": [], "source": [ "with param.exceptions_summarized():\n", " P(n=5, b=5)" ] }, { "cell_type": "markdown", "id": "cc659490", "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-2.1.1/doc/user_guide/Reactive_Expressions.ipynb000066400000000000000000001107071463636336300227450ustar00rootroot00000000000000{ "cells": [ { "cell_type": "markdown", "id": "3ecf9ffc-96a0-4457-bc89-43cac0099e01", "metadata": {}, "source": [ "# Reactive Functions and Expressions" ] }, { "cell_type": "markdown", "id": "9e973ee7", "metadata": {}, "source": [ "In Param, multiple paradigms for dynamic behavior coexist. The [Dependencies and Watchers](Dependencies_and_Watchers.ipynb) guide delves into an imperative 'push' model, where explicit callbacks handle Parameter updates, often suited for GUI environments where user interactions drive state changes.\n", "\n", "Param 2.0 introduces a declarative, reactive model. Reactive expressions automatically update when their referenced Parameters change. This model encourages you to specify 'what' should happen, letting Param manage 'how,' thereby simplifying code logic and enhancing modularity. Unlike the 'push' model, which may require complex event orchestration, the reactive model emphasizes high-level relationships. This makes it versatile, scaling from simple to complex use cases. For instance, the reactive model is a good fit in data transformation pipelines and real-time dashboards, automatically updating dependent steps or visualizations when underlying Parameters change. The reactive approach allows you to focus on defining the transformation or relationship logic without worrying about the sequence of updates.\n", "\n", "This guide covers two main approaches to the reactive model: \n", "1. **Reactive Expressions:** With `.rx`, create reactive proxies for Parameters or objects, which recompute as inputs change.\n", "2. **Reactive Functions:** Using `.bind`, auto-invoked functions update when their inputs change, offering a more declarative alternative to `.watch()`." ] }, { "cell_type": "markdown", "id": "b551f8cd", "metadata": {}, "source": [ ":::{note} The code in this guide is designed to be run incrementally to observe the behavior of reactive expressions. If you're reading a rendered version online, keep in mind that the entire page will have been executed, affecting the output of earlier lines. To fully experience the live updates, download this page as a Jupyter Notebook and run through it line by line." ] }, { "cell_type": "markdown", "id": "7be71aa1", "metadata": {}, "source": [ "## Getting Started\n", "\n", "Param's `rx` feature allows you to create reactive values and expressions, enabling immediate updates to your results as you interactively modify values, avoiding the need for explicit callbacks or managing state manually.\n", "\n", "Before we dive in to discover how `rx` works behind the scenes, let's get started with a concrete example of loading some data into a [Pandas](https://pandas.pydata.org) DataFrame and then displaying it:" ] }, { "cell_type": "code", "execution_count": null, "id": "eaa1f75f", "metadata": {}, "outputs": [], "source": [ "import pandas as pd\n", "import param\n", "\n", "from param import rx" ] }, { "cell_type": "code", "execution_count": null, "id": "4833f209", "metadata": {}, "outputs": [], "source": [ "URL = 'https://datasets.holoviz.org/penguins/v1/penguins.csv'\n", "nrows = rx(2)\n", "df = rx(pd.read_csv(URL))\n", "df.head(nrows)" ] }, { "cell_type": "markdown", "id": "71463561", "metadata": {}, "source": [ "Here, this is just the same code you'd normally use to make a DataFrame, except for `rx()` being used to mark `nrows` and `df` as being reactive. As you can see, the reactive DataFrame works like any other DataFrame, using `.head()` and any other DataFrame methods as usual. But now, let's see what happens if we update the value of `nrows`:" ] }, { "cell_type": "code", "execution_count": null, "id": "e9ead350", "metadata": {}, "outputs": [], "source": [ "nrows.rx.value += 2" ] }, { "cell_type": "markdown", "id": "349ae1e6-d07f-4106-924f-21fbf6a4b0f7", "metadata": {}, "source": [ "Whoa! As long as you are running a Jupyter notebook with a live Python process, you should have seen the dataframe \"head\" output _in_ _the_ _previous_ _cell_ update to the new value of `nrows`. That's because the reactive `df` expression being displayed in that cell captures the full pipeline of operations, automatically re-running `head` because the `nrows` has now changed. \n", "\n", "We've done this without having to write any special callbacks or any new functions, instead using special Python objects that capture the operations you've invoked and replay them as needed when inputs change.\n", "\n", "These updates should happen immediately (not only when the code cell finishes executing):" ] }, { "cell_type": "code", "execution_count": null, "id": "49b40993", "metadata": {}, "outputs": [], "source": [ "import time\n", "\n", "for i in range(5,10):\n", " nrows.rx.value = i\n", " time.sleep(1)" ] }, { "cell_type": "markdown", "id": "374de653", "metadata": {}, "source": [ "You should see the previous `df.head` output react to each time `nrows` is changed, updating to reflect the current state.\n", "\n", "Next, let's explore a more intricate example. Although it involves a more complex pipeline, the code remains similar to what you'd write for a non-reactive Pandas DataFrame. To confirm, you can simply remove the `rx` calls:" ] }, { "cell_type": "code", "execution_count": null, "id": "298ad9dc-7099-4e4f-bf04-d296e0433109", "metadata": {}, "outputs": [], "source": [ "import numpy as np\n", "\n", "style = rx('color: white; background-color: {color}')\n", "color = rx('darkblue')\n", "\n", "def highlight_max(s, props=''):\n", " if s.dtype.kind not in 'f':\n", " return np.full_like(s, False)\n", " return np.where(s == np.nanmax(s.values), props, '')\n", "\n", "styled_df = df.head(nrows).style.apply(highlight_max, props=style.format(color=color), axis=0)\n", "\n", "styled_df" ] }, { "cell_type": "markdown", "id": "6f837bce-a2c2-4479-9adb-0a824fd29085", "metadata": {}, "source": [ "Here we've made two additional reactive values (`style` and `color`), and written a Pandas pipeline reacting to those values, using precisely the same syntax you would with a regular Pandas expression. Since `styled_df` is now a reactive Pandas expression, it will re-run whenever any of those changes. To see, try executing each of the following commands, one by one:" ] }, { "cell_type": "code", "execution_count": null, "id": "16d46876-0ded-4770-bb71-fd972d8d9046", "metadata": {}, "outputs": [], "source": [ "color.rx.value = 'red'" ] }, { "cell_type": "code", "execution_count": null, "id": "aa9f36b4", "metadata": {}, "outputs": [], "source": [ "nrows.rx.value += 2" ] }, { "cell_type": "code", "execution_count": null, "id": "b3502bc8", "metadata": {}, "outputs": [], "source": [ "color.rx.value = 'darkblue'" ] }, { "cell_type": "markdown", "id": "8082c9db", "metadata": {}, "source": [ "In the code above, we made reactive strings, numbers, and DataFrame expressions. You can also make functions reactive, which lets you make the URL reactive as well:" ] }, { "cell_type": "code", "execution_count": null, "id": "106363b4", "metadata": {}, "outputs": [], "source": [ "url = rx(URL)\n", "df = rx(pd.read_csv)(url)\n", "df.head(2)" ] }, { "cell_type": "code", "execution_count": null, "id": "30d7e585", "metadata": {}, "outputs": [], "source": [ "url.rx.value = 'https://datasets.holoviz.org/gapminders/v1/gapminders.csv'" ] }, { "cell_type": "code", "execution_count": null, "id": "742bd80f", "metadata": {}, "outputs": [], "source": [ "url.rx.value = URL" ] }, { "cell_type": "markdown", "id": "60db0a0a-a2be-4927-91a3-7cc8b10ea313", "metadata": {}, "source": [ "In this case, `df` wraps the `read_csv` call generating the DataFrame, rather than a specific DataFrame instance. This demonstrates the flexibility of reactive expressions: you can write code as you usually would, but gain control over its reactivity.\n", "\n", "While we've been updating `.rx.value` manually in this notebook, you could easily replace these literals with widgets from ipywidgets or [HoloViz Panel](https://panel.holoviz.org/tutorials/basic/pn_rx.html), enabling you to create user-interactive, reactive applications with minimal effort." ] }, { "cell_type": "markdown", "id": "1c566d1a", "metadata": {}, "source": [ "## How it works\n", "\n", "So, how does reactive programming in Param actually work? The underlying mechanism leverage's Python's [operator overloading](https://www.geeksforgeeks.org/operator-overloading-in-python/), which allows us to redefine common operators like '`+`' to perform additional tasks. When you use these operators in reactive expressions, Param not only carries out the operation but also records it, establishing a dependency from the reactive variables involved to the resulting expression. This means that when a reactive variable is updated, Param automatically updates any expressions dependent on it.\n", "\n", "For instance, if you set up a reactive expression `j = i + 1`, where `i` has been made reactive, any change to `i` will automatically trigger an update in `j`, eliminating the need for manual event handling:" ] }, { "cell_type": "code", "execution_count": null, "id": "6a830ba9", "metadata": {}, "outputs": [], "source": [ "i = rx(1)\n", "j = i + 1\n", "print(f'i = {i.rx.value}')\n", "print(f'j = {j.rx.value}')" ] }, { "cell_type": "code", "execution_count": null, "id": "bad87395", "metadata": {}, "outputs": [], "source": [ "i.rx.value = 7\n", "print(f'i = {i.rx.value}')\n", "print(f'j = {j.rx.value}')" ] }, { "cell_type": "markdown", "id": "e089f478", "metadata": {}, "source": [ "Without `rx()`, adding 1 to `i` would have immediately invoked integer addition in Python, assigning an integer 2 to `j`. However, because we made `i` a reactive expression, what happens is that `i` stores its input value on an internal attribute called `_obj`, while overloading `+` to not just calculate `i + 1`, but also return another reactive object that records the operation (`i + 1`). This stores the dependency so that whenever `i` changes, the reactive object knows that it needs to update itself by re-executing `i + 1`." ] }, { "cell_type": "code", "execution_count": null, "id": "c5925184", "metadata": {}, "outputs": [], "source": [ "type(i), i._obj, j._operation" ] }, { "cell_type": "markdown", "id": "51aed018", "metadata": {}, "source": [ "When you access the `.value` attribute of `j`, it retrieves the most recent result of the expression `i + 1`, automatically reapplying the operation if `i` has changed. For more complicated scenarios, reactive expressions can chain multiple operations and method calls together, executing them in sequence to obtain the final outcome.\n", "\n", "In essence, reactive expressions are specialized Python objects that wrap standard objects. They record the operations you apply, and when an underlying reactive value changes, they automatically re-execute these operations. This eliminates the need for manually tracking and updating dependent variables, making it easier to build dynamic, responsive applications." ] }, { "cell_type": "markdown", "id": "a48adb7e-c340-49e1-bec1-30ca6cc2eca2", "metadata": {}, "source": [ "## Limitations\n", "\n", "So does Python really allow _all_ operations to be overloaded so that a reactive expression works precisely like the underlying objects? \n", "\n", "Nearly, but not quite. For technical reasons, certain operations cannot be implemented in this way:\n", "\n", "- Python requires the `len` operation to return an integer, not a deferred reactive integer\n", "- The Python `is` statement always checks the immediate identity of its two operands, so it cannot be deferred reactively\n", "- Logical operators like `and`, `or`, `not`, and `in` are required to return Boolean types rather than deferred, reactive Boolean types\n", "- No overloading is available for control flow keywords like `if`, `elif`, and `else` or ternary conditional expressions (i.e. `a if condition else b`), and so those actions cannot be captured for later reactive execution\n", "- Iteration keywords like `for` or `while` can only be overloaded to some extent, specifically for fixed-length collections; other types of iteration cannot be captured for later reactive execution\n", "\n", "However, Param's reactive expressions offer workarounds for these limitations through special methods under the `.rx` namespace to avoid confusion with the underlying object's own methods. We'll cover these methods in the next section." ] }, { "cell_type": "markdown", "id": "227a230b-98b2-4097-8a6d-798ddb63b74a", "metadata": {}, "source": [ "## Special Methods on `.rx`\n", "\n", "To circumvent the limitations explained above, the `.rx` namespace provides reactive versions of the operations that can't be made reactive through overloading:\n", "\n", "- `.rx.and_`: Reactive version of `and`.\n", "- `.rx.bool`: Reactive version of `bool()`.\n", "- `.rx.in_`: Reactive version of `in`, testing if the value is in the provided collection.\n", "- `.rx.is_`: Reactive version of `is`, testing the object identity against another object.\n", "- `.rx.is_not`: Reactive version of `is not`, testing the absence of object identity with another object.\n", "- `.rx.len`: Reactive version of `len()`, returning the length of the expression\n", "- `.rx.map`: Applies a function to each item in a collection.\n", "- `.rx.not_`: Reactive version of `not`.\n", "- `.rx.or_`: Reactive version of `or`.\n", "- `.rx.pipe`: Applies the given function (with static or reactive arguments) to this object.\n", "- `.rx.updating`: Returns a boolean indicating whether the expression is currently updating.\n", "- `.rx.when`: Generates a new expression that only updates when the provided dependency updates.\n", "- `.rx.where`: Returns either the first or the second argument, depending on the current value of the expression.\n", "\n", "Unlike their corresponding standard Python equivalent, each of these returns a reactive expression that can thus be combined with other reactive expressions to make reactive pipelines." ] }, { "cell_type": "markdown", "id": "d9aaee66-05f2-44ee-b287-c5eaf4622c3b", "metadata": {}, "source": [ "#### `.rx.and_(arg)`\n", "\n", "Applies the `and` operator to the output of the reactive expression and the argument returning a new expression:" ] }, { "cell_type": "code", "execution_count": null, "id": "0dd7a685-1d2a-4f33-81d0-8d65feee2224", "metadata": {}, "outputs": [], "source": [ "rx(True).rx.and_(False)" ] }, { "cell_type": "markdown", "id": "6b568713-8fb0-46b7-877e-cfe4f43ebc8c", "metadata": {}, "source": [ "Unlike the bitwise `and` operator (`&`) this has the same semantics as the `and` keyword." ] }, { "cell_type": "markdown", "id": "dcbf07d6-e53b-4658-86c7-15caf8eb540d", "metadata": {}, "source": [ "#### `.rx.in_(arg)`\n", "\n", "Reactively checks if the current value is `.in_` the other collection" ] }, { "cell_type": "code", "execution_count": null, "id": "938583e7-c519-4c50-9f95-dc4099262a95", "metadata": {}, "outputs": [], "source": [ "rx(2).rx.in_([1, 2, 3])" ] }, { "cell_type": "markdown", "id": "fb3697e6-0413-44cd-916a-dae665eb263b", "metadata": {}, "source": [ "#### `.rx.is_(arg)`\n", "\n", "Reactively checks if the identity of the current value is the same as the argument to `.is_`" ] }, { "cell_type": "code", "execution_count": null, "id": "0db8bc66-bf5b-4214-b3de-063ec6d53523", "metadata": {}, "outputs": [], "source": [ "rx(None).rx.is_(None)" ] }, { "cell_type": "markdown", "id": "819249de-513d-4286-9a6f-ed14adba70bc", "metadata": {}, "source": [ "#### `.rx.is_not(arg)`\n", "\n", "Reactively checks if the identity of the current value is not the same as the argument to `.is_not`" ] }, { "cell_type": "code", "execution_count": null, "id": "93db5be8-2023-4a67-83f0-329d0dea69d1", "metadata": {}, "outputs": [], "source": [ "rx(None).rx.is_not(None)" ] }, { "cell_type": "markdown", "id": "9cba7f57-bc62-47bd-9cfd-b66c13d9f0e2", "metadata": {}, "source": [ "#### `.rx.len()`\n", "\n", "Returns the length of the object as a reactive expression" ] }, { "cell_type": "code", "execution_count": null, "id": "b9552875-905a-49d8-a1a3-6a824aeb1988", "metadata": {}, "outputs": [], "source": [ "obj = rx([1, 2, 3])\n", "obj.rx.len()" ] }, { "cell_type": "markdown", "id": "9531ef4c-28d9-48cd-8707-e0658b0ecc9e", "metadata": {}, "source": [ "#### `.rx.map(func, *args, **kwargs)`\n", "\n", "Maps the function to each item in a collection returned by the expression:" ] }, { "cell_type": "code", "execution_count": null, "id": "ee4cafb0-bb85-4bd3-b39f-4e705f369908", "metadata": {}, "outputs": [], "source": [ "rx([1, 2, 3]).rx.map(lambda v, mul: v*mul, mul=2)" ] }, { "cell_type": "markdown", "id": "2cbff9fe-e16d-427d-ab1c-35da603ad693", "metadata": {}, "source": [ "#### `.rx.or_(arg)`\n", "\n", "Applies `or` to the output of the reactive expression and the argument:" ] }, { "cell_type": "code", "execution_count": null, "id": "2e8bff49-1164-4747-a704-c96f64084c44", "metadata": {}, "outputs": [], "source": [ "rx(False).rx.or_('A value')" ] }, { "cell_type": "markdown", "id": "f50c3ea6-aa75-4bb5-91f3-1b2b5f320465", "metadata": {}, "source": [ "Unlike the bitwise `or` operator (`|`) this has the same semantics as the `or` keyword." ] }, { "cell_type": "markdown", "id": "2bd115e5-44bd-4d4d-b78a-a552b4072562", "metadata": {}, "source": [ "#### `.rx.pipe(func, *args, **kwargs)`\n", "\n", "Pipes the current value into a function as the first argument, passing in additional positional and keyword arguments if provided, and returning a reactive expression to replay that call as needed:" ] }, { "cell_type": "code", "execution_count": null, "id": "c393e1e5-e18a-4b50-b355-cb3d461c8de5", "metadata": {}, "outputs": [], "source": [ "def f(a, b): return a + b\n", " \n", "rx(1).rx.pipe(f, 2)" ] }, { "cell_type": "markdown", "id": "ac92db62", "metadata": {}, "source": [ "`.rx.pipe` can be used with any Python function. One common usage is for making type conversion functions reactive:" ] }, { "cell_type": "code", "execution_count": null, "id": "34e985c6", "metadata": {}, "outputs": [], "source": [ "rx(8.5).rx.pipe(int)" ] }, { "cell_type": "code", "execution_count": null, "id": "116b865a", "metadata": {}, "outputs": [], "source": [ "rx(8.5).rx.pipe(str)" ] }, { "cell_type": "markdown", "id": "376f5b3d-8bcd-4ad3-869d-cdee176e54d8", "metadata": {}, "source": [ "#### `.rx.updating()`" ] }, { "cell_type": "markdown", "id": "9334392f-61d0-4629-aae8-0c9a7935c6fa", "metadata": {}, "source": [ "Returns a new expression that is True while the original expression is updating. Useful for performing some action while an expression is running.\n", "\n", "Here we create a simple expression that calls a `calculate` function which emulates a long running computation:" ] }, { "cell_type": "code", "execution_count": null, "id": "e132793e-b5ad-49bb-88d1-378a24451055", "metadata": {}, "outputs": [], "source": [ "expr = rx(1)\n", "\n", "def calculate(value):\n", " time.sleep(1)\n", " return value\n", "\n", "updating = expr.rx.pipe(calculate).rx.updating()\n", "\n", "updating" ] }, { "cell_type": "markdown", "id": "7eac3901-b9e6-4020-8f74-5817561a187e", "metadata": {}, "source": [ "When we update the expression the `updating` expression will temporarily toggle to True and then reset:" ] }, { "cell_type": "code", "execution_count": null, "id": "1432d82f-4e8d-4ab0-b7f3-4a63a203b797", "metadata": {}, "outputs": [], "source": [ "expr.rx.value += 1" ] }, { "cell_type": "markdown", "id": "dfea7f35-f0fa-4bc0-954f-b01d5dcf9d6c", "metadata": {}, "source": [ "#### `.rx.when(*conditions)`\n", "\n", "Useful when creating UIs to declare that the expression should only update when some other parameter changes, e.g. when a user clicks a button or triggers an expensive operation through some other mechanism.\n", "\n", "For instance, let's say we have some expensive function (here simulated using `time.sleep`). First, we bind parameters `a` and `b` to this function and create a reactive expression from this function." ] }, { "cell_type": "code", "execution_count": null, "id": "b0b7e3cc-4c31-4208-8f0a-efa05cc6e13e", "metadata": {}, "outputs": [], "source": [ "import time\n", "\n", "def expensive_function(a, b):\n", " print(f'multiplying {a=} and {b=}')\n", " time.sleep(2)\n", " return a * b\n", "\n", "a = rx(1)\n", "b = rx(2)\n", "\n", "expensive_expr = rx(expensive_function)(a, b)" ] }, { "cell_type": "markdown", "id": "24ef7256-904b-4e74-859a-39d30d391317", "metadata": {}, "source": [ "The problem we face is that if we use this `expensive_expr` whenever `a` **or** `b` are changed, then the expensive computation gets triggered *twice* if we want to change both `a` _and_ `b`. To avoid unnecessary expense, we can gate the computation behind a third variable we'll name `gate`:" ] }, { "cell_type": "code", "execution_count": null, "id": "8396d27b-81be-4185-be83-16a7bcfe0e4f", "metadata": {}, "outputs": [], "source": [ "gate = rx(False)\n", "gated_expr = expensive_expr.rx.when(gate)\n", "\n", "gated_expr" ] }, { "cell_type": "markdown", "id": "0b9b464b-1582-499f-b4c0-e7fe8074d8eb", "metadata": {}, "source": [ "We can now safely change variables `a` and `b` separately without triggering the computation:" ] }, { "cell_type": "code", "execution_count": null, "id": "7f1dd442-0c57-49e2-97b5-2369adb35a7b", "metadata": {}, "outputs": [], "source": [ "a.rx.value = 2\n", "b.rx.value = 4\n", "\n", "gated_expr.rx.value" ] }, { "cell_type": "markdown", "id": "a1443865-288a-4320-841c-586fcfea04ac", "metadata": {}, "source": [ "But when we trigger the `run` parameter the expression will re-compute:" ] }, { "cell_type": "code", "execution_count": null, "id": "85c2a7d1-ff76-4137-b9ce-24363177e120", "metadata": {}, "outputs": [], "source": [ "gate.rx.value = True\n", "\n", "gated_expr.rx.value" ] }, { "cell_type": "markdown", "id": "d74f3645-5186-46b5-b85b-4e512cfae7bd", "metadata": {}, "source": [ "#### `.rx.where(a, b)`\n", "\n", "Reactive ternary conditional. In non-reactive Python code you can write:\n", "\n", "```python\n", "a if condition else b\n", "``` \n", "\n", "to return value `a` or value `b` depending on some `condition`. However, Python does not allow overriding `if` to have special behavior for a reactive `condition`, and thus such an expression will immediately evaluate and return `a` or `b` rather than capturing this logic for later reactivity.\n", "\n", "So if we want to have a reactive conditional, we have to rewrite the expression using `where`. First, we will declare a reactive `condition` expression to wrap a Boolean value that we can change later:" ] }, { "cell_type": "code", "execution_count": null, "id": "dd4f9569-b9a9-4d43-93cb-93ffba6ce927", "metadata": {}, "outputs": [], "source": [ "condition = rx(True)" ] }, { "cell_type": "markdown", "id": "d0dff104-a095-4a97-806b-059b4c9aa622", "metadata": {}, "source": [ "Now let's say we want to return either `a` or `b` depending on whether the `condition` is True or False. We can simply pass the values to `.where()`:" ] }, { "cell_type": "code", "execution_count": null, "id": "e9a36314-9a79-4e74-83e8-137f86655999", "metadata": {}, "outputs": [], "source": [ "a = rx(1)\n", "b = rx(2)\n", "\n", "ternary_expr = condition.rx.where(a, b)\n", "ternary_expr" ] }, { "cell_type": "markdown", "id": "88ef4e67-0fa4-45a7-8154-89f94183d3fe", "metadata": {}, "source": [ "Since the initial value is `True` it returns the current value of `a`, which is `1`. However when we set the value to `False` it will return the value of `b`:\n" ] }, { "cell_type": "code", "execution_count": null, "id": "2a1ce24c-e5b3-497d-83bc-5ba351ece9ef", "metadata": {}, "outputs": [], "source": [ "condition.rx.value = False\n", "\n", "ternary_expr.rx.value" ] }, { "cell_type": "markdown", "id": "ddb05099-e774-4f53-a345-527e44bdb5de", "metadata": {}, "source": [ "Importantly, if we now change `b` the result will be reflected by the expression, reactively unless we explicitly resolve the result:" ] }, { "cell_type": "code", "execution_count": null, "id": "5e275e74-1d17-40bf-9382-d02a1fed61ae", "metadata": {}, "outputs": [], "source": [ "b.rx.value = 5\n", "\n", "ternary_expr.rx.value" ] }, { "cell_type": "markdown", "id": "ce3ce7b3-8163-45b9-a465-17efdbca5c21", "metadata": {}, "source": [ "Here the expression value depends only on `b` thanks to the `where` condition, and thus changes to `a` will no longer trigger any downstream updates until the condition is reversed again." ] }, { "cell_type": "markdown", "id": "4151d9d4-49aa-42b8-83fd-eea081fb127d", "metadata": {}, "source": [ "#### Watching an expression\n", "\n", "In some cases you may want to trigger some side-effect based on the return value of an expression. The simplest way to achieve this is using the `.rx.watch` API, which mirrors the [`.param.watch` API](Dependencies_and_Watchers.ipynb#watchers). Using this API we can define a callback which runs whenever the expression outputs a new event:" ] }, { "cell_type": "code", "execution_count": null, "id": "b5f87127-4f6d-4806-a1cb-4e6444cc6bc8", "metadata": {}, "outputs": [], "source": [ "c = rx(1)\n", "\n", "c.rx.watch(lambda v: print(f'Output: {v}'))\n", "\n", "c" ] }, { "cell_type": "markdown", "id": "f9ddc079-3ed2-47f0-962f-0c1f6d58553d", "metadata": {}, "source": [ "Now if we update the expression we will see the output run:" ] }, { "cell_type": "code", "execution_count": null, "id": "cad29d4d-c893-4fb0-8f1a-11cc0c1b1c1b", "metadata": {}, "outputs": [], "source": [ "c.rx.value += 1" ] }, { "cell_type": "markdown", "id": "1c87f6a2-d108-435b-9305-0160eec628d6", "metadata": {}, "source": [ "It is also possible to call `.param.watch` without any arguments, which makes the expression evaluate eagerly." ] }, { "cell_type": "markdown", "id": "73810468", "metadata": {}, "source": [ "## Parameters and `param.bind`\n", "\n", "Reactive expressions are part of the [Param](https://param.holoviz.org) library, and behind the scenes, all the reactivity is implemented using Parameters and their [dependencies and watchers](https://param.holoviz.org/user_guide/Dependencies_and_Watchers.html) support. You can use reactive expressions without needing to learn about Parameters, but if you do use Parameters in your work, they interact seamlessly with reactive expressions, providing a powerful and convenient way to organize your code and your work. In this section we will show how to use Parameters and the `param.bind` function together with reactive expressions for a more structured approach to reactive programming.\n", "\n", "First, let's create a Parameterized class with a couple of Parameters:" ] }, { "cell_type": "code", "execution_count": null, "id": "f6bf6371-8dc8-4247-afaa-3ce872ed3371", "metadata": {}, "outputs": [], "source": [ "class Parameters(param.Parameterized): \n", " a = param.Number(1)\n", "\n", " b = param.Number(0)\n", "\n", " run = param.Event()\n", " \n", "p = Parameters()" ] }, { "cell_type": "markdown", "id": "cf4c375b", "metadata": {}, "source": [ "Any of the parameters can be used as reactive expressions by calling `.rx()` on their Parameter object:" ] }, { "cell_type": "code", "execution_count": null, "id": "f8346eea", "metadata": {}, "outputs": [], "source": [ "expr = p.param.a.rx() + p.param.b.rx() + 3\n", "expr" ] }, { "cell_type": "markdown", "id": "5f316cfc", "metadata": {}, "source": [ "Now if we update the Parameter, the result of the expression will update immediately:" ] }, { "cell_type": "code", "execution_count": null, "id": "2cc67793", "metadata": {}, "outputs": [], "source": [ "p.b = 5\n", "print(expr.rx.value)" ] }, { "cell_type": "markdown", "id": "7e5ff5e8", "metadata": {}, "source": [ "You can thus use any Parameter in your reactive expressions, including Parameters from [HoloViz Panel](https://panel.holoviz.org/tutorials/basic/pn_rx.html) widgets. In fact, reactive expressions are natively supported in Panel." ] }, { "cell_type": "markdown", "id": "874621cc", "metadata": {}, "source": [ "### Binding Parameters to Functions\n", "\n", "While reactive expressions with `rx` offer a flexible way to define dynamic relationships between Parameters, they operate at a fairly abstract level, encapsulating the underlying transformations. This can sometimes make it challenging to isolate specific parts of a pipeline for debugging or performance optimization.\n", "\n", "Enter `param.bind`, which allows you to define functions that are automatically invoked when their input Parameters change. This serves as a bridge between the reactive `rx` model and the lower-level 'push' model. Unlike the 'push' model, where you would explicitly set up watchers and callbacks, `param.bind` simplifies the process by letting Param manage the mechanics, but also making the dependencies more transparent than in a purely `rx` approach.\n", "\n", "In essence, `param.bind` offers the declarative nature of reactive expressions and the explicitness of the 'push' model. This makes it particularly useful for complex applications where you might want the clarity of explicit function calls for key parts of your pipeline, but also wish to retain the high-level, declarative relationships offered by reactive expressions.\n", "\n", "To demonstrate this concept, let's define a simple Python function for adding numbers. We'll also include print statements to make it evident when the function is invoked:" ] }, { "cell_type": "code", "execution_count": null, "id": "7a4bde61", "metadata": {}, "outputs": [], "source": [ "def add(a, b):\n", " print(f'add: {a}+{b}={a+b}')\n", " return a + b\n", "\n", "add(3, 7)" ] }, { "cell_type": "markdown", "id": "1c4434e7-9b5d-4a5b-b9de-c444dae42e36", "metadata": {}, "source": [ "Now we can use `param.bind` to \"bind\" parameters `a` and `b` to the `add` function's arguments to create a reactive function:" ] }, { "cell_type": "code", "execution_count": null, "id": "c0a4c64e-ee64-405d-bace-605551b34234", "metadata": {}, "outputs": [], "source": [ "reactive_add = param.bind(add, p.param.a, p.param.b)\n", "\n", "reactive_add" ] }, { "cell_type": "markdown", "id": "9094768c-5a39-448e-9721-e6253a2a55cb", "metadata": {}, "source": [ "As you can see, `reactive_add` works just like `add`, in that it adds two arguments, but in this case, it's taking the value of the `a` and `b` Parameters of `p`. Parameter `a` has been \"bound\" to the first argument and `b` to the second, and if either of them changes, the result changes. So if we change `p.a` to 5, the output above reacts immediately." ] }, { "cell_type": "code", "execution_count": null, "id": "f7cd00ac-02a2-4e0c-bc98-7ce9739ed55b", "metadata": {}, "outputs": [], "source": [ "p.a += 4" ] }, { "cell_type": "markdown", "id": "9a0e5412-b13f-42aa-bb21-4bf6f289510b", "metadata": {}, "source": [ "We can also call the reactive function explicitly to return the current result as a concrete, no longer reactive value:" ] }, { "cell_type": "code", "execution_count": null, "id": "1e026152-1772-4437-8165-dd9788edd1fb", "metadata": {}, "outputs": [], "source": [ "reactive_add()" ] }, { "cell_type": "markdown", "id": "16d5e9ec", "metadata": {}, "source": [ "The difference between `reactive_add` and `reactive_add()` is that the first one is a function, whose display will automatically update in IPython/Jupyter, while the second is a specific number (the result of calling that function a single time, never to be updated further):" ] }, { "cell_type": "code", "execution_count": null, "id": "9f1c1879", "metadata": {}, "outputs": [], "source": [ "print(type(reactive_add), type(reactive_add()))" ] }, { "cell_type": "markdown", "id": "03d62757-8b3f-4d67-b1a9-5759194408a2", "metadata": {}, "source": [ "`param.bind` follows the semantics of Python's `functools.partial`, and so if you only partially bind the required arguments, you'll get a function of the remaining arguments:" ] }, { "cell_type": "code", "execution_count": null, "id": "88870bf0-1d5e-42e9-9c59-b538070ad701", "metadata": {}, "outputs": [], "source": [ "add_b = param.bind(add, p.param.a)\n", "add_b" ] }, { "cell_type": "code", "execution_count": null, "id": "d516b3dc", "metadata": {}, "outputs": [], "source": [ "add_b(5)" ] }, { "cell_type": "markdown", "id": "1b0ead79", "metadata": {}, "source": [ "Note that you can bind any accepted type to make a reactive function, not just Parameters, but static values won't trigger reactive updates (here 38 will always be the same value, while the result will depend on the current value of `p.param.a`)." ] }, { "cell_type": "code", "execution_count": null, "id": "4badba81", "metadata": {}, "outputs": [], "source": [ "param.bind(add, p.param.a, b=38)" ] }, { "cell_type": "markdown", "id": "d1f2b719-b80e-4fca-b78e-534971eee92e", "metadata": {}, "source": [ "Bound functions update their outputs reactively when displayed, but what if you want to use one in a reactive expression? You can easily do that if you call `.rx()` on a fully bound function to get a reactive expression to work with:" ] }, { "cell_type": "code", "execution_count": null, "id": "6b4e1c1a-8d19-4e4f-beae-042f01d4e41a", "metadata": {}, "outputs": [], "source": [ "param.bind(add, p.param.a, p.param.b).rx() / 2" ] }, { "cell_type": "markdown", "id": "1272daed", "metadata": {}, "source": [ "As you can see, you can use bound functions to get reactivity if you prefer to write specific functions, or you can use reactive expressions to capture computations without writing function definitions, or you can combine the two as needed. Feel free to use the approach that best meets your needs!\n", "\n", "And overall, hopefully, you can see that Param's reactive support provides a natural and powerful way to capture your computations in a way that can be replayed automatically whenever inputs change, making it a convenient basis for building interactive applications and computations." ] } ], "metadata": { "language_info": { "name": "python", "pygments_lexer": "ipython3" } }, "nbformat": 4, "nbformat_minor": 5 } param-2.1.1/doc/user_guide/References.ipynb000066400000000000000000000330751463636336300206640ustar00rootroot00000000000000{ "cells": [ { "cell_type": "markdown", "id": "5d4a9beb-65c0-47dd-8afc-14b2c448f0b9", "metadata": {}, "source": [ "# Reactive References\n", "\n", "Building on previous discussion about the dual roles of a `Parameter` - as both a value holder and a metadata container - let's explore how parameters can go a step further by acting as dynamic references to other parameters (and other, more advanced, reactive references). What we mean by this is that parameters do not have to refer to a specific static value but can reference another object and update reactively when its value changes. \n", "\n", "## Parameter References\n", "\n", "In the simplest case, when a parameter is configured with `allow_refs=True`, it can be given another `Parameter` as its value and it will automatically mirror its current value . This capability enables more intricate relationships between parameters, allowing for automatic value synchronization and forming the basis for reactive programming:" ] }, { "cell_type": "code", "execution_count": null, "id": "e45694e1-9503-488f-8797-531b18d3502d", "metadata": {}, "outputs": [], "source": [ "import param\n", "\n", "class U(param.Parameterized):\n", " \n", " a = param.Number()\n", " \n", "class V(param.Parameterized):\n", " \n", " b = param.Number(default=None, allow_refs=True)\n", "\n", "u = U(a=3.14)\n", "v = V(b=u.param.a)\n", "\n", "v.b" ] }, { "cell_type": "markdown", "id": "5c5cbfee-a199-4ffb-8e35-f0b543916ff2", "metadata": {}, "source": [ "By declaring that `V.b` allows references we have made it possible to pass the Parameter `U.a`, which means `v.b` will reflect the value of `u.a`:" ] }, { "cell_type": "code", "execution_count": null, "id": "3926d917-7334-45b4-8a49-4b9baf558d50", "metadata": {}, "outputs": [], "source": [ "u.a = 1.57\n", "\n", "v.b" ] }, { "cell_type": "markdown", "id": "33744151-91c0-4d65-b84e-9a2678dc5eeb", "metadata": {}, "source": [ "This unidirectional link will be in effect until something else tries to set the value:" ] }, { "cell_type": "code", "execution_count": null, "id": "345e5b1e-a191-44b0-8c8f-48908b4332a2", "metadata": {}, "outputs": [], "source": [ "v.b = 14.1\n", "u.a = 13.2\n", "\n", "v.b" ] }, { "cell_type": "markdown", "id": "b8f1dd08-a91d-4e83-bb12-dd2447c8fad8", "metadata": {}, "source": [ "In other words, if the value is overridden from the outside the link will be automatically removed.\n", "\n", "Simple references are resolved when `allow_refs=True` but to allow nested references we separately have to set `nested_refs=True`." ] }, { "cell_type": "code", "execution_count": null, "id": "e3ad5e4c-e8f1-4a16-b800-a06e434f2f65", "metadata": {}, "outputs": [], "source": [ "class W(V):\n", " \n", " c = param.List(allow_refs=True, nested_refs=True)\n", " \n", "u1 = U(a=3)\n", "u2 = U(a=13)\n", "\n", "w = W(c=[u1.param.a, u2.param.a]) \n", "\n", "w.c" ] }, { "cell_type": "markdown", "id": "2f52c6d5-dad7-4d38-8b0f-1364123b8e17", "metadata": {}, "source": [ "When we modify either `u1.a` or `u2.a`, `w.c` will update:" ] }, { "cell_type": "code", "execution_count": null, "id": "e739d5f3-c762-4971-8386-48c5ed89e701", "metadata": {}, "outputs": [], "source": [ "u1.a = 7\n", "\n", "w.c" ] }, { "cell_type": "markdown", "id": "46039ee7-93a7-44d8-b1bb-a9c1ed7f87a9", "metadata": {}, "source": [ "## Other reference types" ] }, { "cell_type": "markdown", "id": "32f2429f-fbff-4906-b3b0-4db7c3a9aa2b", "metadata": {}, "source": [ "Note that `Parameter` types are not the only types of valid references. The full list of valid references include:\n", "\n", "- Class and instance `Parameter` objects\n", "- [Functions or methods with dependencies](Dependencies_and_Watchers.ipynb#dependencies) added using `param.depends`\n", "- [Reactive Functions](Reactive_Expressions.ipynb#parameters-and-param-bind) using `param.bind`\n", "- [Reactive expressions](Reactive_Expressions.ipynb) declared using `param.rx`\n", "- [(Asynchronous) generators](Generators.ipynb)\n", "- Custom objects transformed into a valid reference with a hook registered with `param.parameterized.register_reference_transform`.\n", "\n", "There are two utility functions which allow resolving all parameters a reference depends on and the current value of the reference:" ] }, { "cell_type": "code", "execution_count": null, "id": "da0cbf1c-e8b3-4d3a-9974-0c0e6ec1d2b0", "metadata": {}, "outputs": [], "source": [ "from param.parameterized import resolve_ref, resolve_value\n", "\n", "resolve_ref(u1.param.a), resolve_value(u1.param.a)" ] }, { "cell_type": "markdown", "id": "9856dd01-153f-4f87-9acc-3f951592a475", "metadata": {}, "source": [ "## Skipping Reference Updates\n", "\n", "Since references are resolved eagerly whenever one of the dependencies change we may run into situations where we want to control when a reference is updated. Specifically we may want to skip resolving a reference if one of the inputs does not meet some condition or only if a certain event is triggered.\n", "\n", "Let's see how we can configure this. Here we will create a class `W` with parameters `a` and `b` and a `run` event. We then define a function to `add` parameters `a` and `b` but only if the `run` event is active. To do this we can raise a `param.Skip` exception in the function." ] }, { "cell_type": "code", "execution_count": null, "id": "d04c62cc-8bab-46d6-9a4a-9c645d1d6510", "metadata": {}, "outputs": [], "source": [ "class W(param.Parameterized):\n", " \n", " a = param.Number()\n", " \n", " b = param.Number()\n", "\n", " run = param.Event()\n", "\n", "w = W(a=0, b=2)\n", "\n", "def add(a, b, run):\n", " if not run:\n", " raise param.Skip\n", " return a + b" ] }, { "cell_type": "markdown", "id": "a5ab6ca1-e220-405d-a8cf-89c276a6aa7e", "metadata": {}, "source": [ "We can now bind all three parameters to the function:" ] }, { "cell_type": "code", "execution_count": null, "id": "62af2167-e8cf-4099-8921-36d8a34b2a2b", "metadata": {}, "outputs": [], "source": [ "v = V(b=param.bind(add, w.param.a, w.param.b, w.param.run))\n", "\n", "v.b" ] }, { "cell_type": "markdown", "id": "f28c3c42-bd6f-46e1-b269-fa6d13d0bbbd", "metadata": {}, "source": [ "Even though we initialized `v.b` with a reference it will not resolve this reference until we trigger a `run` event:" ] }, { "cell_type": "code", "execution_count": null, "id": "28c13ab5-4616-4a42-8cff-f82ba9599e8f", "metadata": {}, "outputs": [], "source": [ "w.param.trigger('run')\n", "\n", "v.b" ] }, { "cell_type": "markdown", "id": "751e6786-7140-4a91-a430-6de35150f55c", "metadata": {}, "source": [ ":::{caution}\n", "`Skip` exceptions are a useful tool for handling control flow in an application, however they should only be raised when writing a reactive reference and never be used in place of a real exception in your business logic. For example, when writing a function that fetches some data, you should raise specific exceptions and then catch those in the function that you are using as a reference and raise the `Skip` from there, e.g.:\n", "\n", "```python\n", "def fetch_data(url):\n", " response = requests.get(url)\n", " json = response.json()\n", " if 'data' in json: \n", " raise ValueError(\"JSON response did not contain expected 'data' field.\")\n", " return json['data']\n", "\n", "def data_ref(url):\n", " try:\n", " data = fetch_data(url)\n", " except Exception:\n", " raise Skip\n", " return data\n", "```\n", ":::" ] }, { "cell_type": "markdown", "id": "6ffd34f8-211c-4945-95b8-e87ec712e028", "metadata": {}, "source": [ "## Composable classes through references\n", "\n", "One common problem that occurs when writing classes where one object depends on the parameters of some other object is that the objects end up having to reference each other. This introduces dependencies between the two classes and leads to less composable abstractions and it only gets worse as you add more classes to the mix.\n", "\n", "Let's illustrate this with an example, say we have an investment portfolio and want to determine how much it is worth in our local currency and how much capital gains taxes we owe on our profit. We obtain these values from some external objects called `Forex` and `TaxSchedule`:" ] }, { "cell_type": "code", "execution_count": null, "id": "11cf2cca-fcb3-4684-af9e-9b1ff476efe2", "metadata": {}, "outputs": [], "source": [ "class Forex(param.Parameterized):\n", "\n", " exchange_rate = param.Number(default=1.125)\n", "\n", " ...\n", "\n", "class TaxSchedule(param.Parameterized):\n", "\n", " capital_gains = param.Number(default=0.25)\n", "\n", " ...\n", "\n", "forex = Forex(exchange_rate=1.14)\n", "tax_schedule = TaxSchedule(capital_gains=0.25)" ] }, { "cell_type": "markdown", "id": "b44d1a47-c5e1-492c-9f48-027179ca180e", "metadata": {}, "source": [ "Without references we would now be forced to structure our `Portfolio` class to reference both the `forex` and `tax_schedule` objects because that's the only way we could cleanly depend on the values:" ] }, { "cell_type": "code", "execution_count": null, "id": "355b840f-8acc-43c6-b46e-d34e21799779", "metadata": {}, "outputs": [], "source": [ "class Portfolio(param.Parameterized):\n", "\n", " foreign_value = param.Number()\n", "\n", " local_value = param.Number()\n", "\n", " taxes_owed = param.Number()\n", "\n", " forex = param.ClassSelector(class_=Forex)\n", "\n", " tax_schedule = param.ClassSelector(class_=TaxSchedule)\n", "\n", " @param.depends('foreign_value', 'forex.exchange_rate', watch=True, on_init=True)\n", " def _update_local(self):\n", " self.local_value = self.foreign_value * self.forex.exchange_rate\n", "\n", " @param.depends('local_value', 'tax_schedule.capital_gains', watch=True, on_init=True)\n", " def _update_taxes(self):\n", " self.taxes_owed = self.local_value * self.tax_schedule.capital_gains\n", "\n", "portfolio = Portfolio(foreign_value=12000, forex=forex, tax_schedule=tax_schedule)\n", "\n", "portfolio" ] }, { "cell_type": "markdown", "id": "391e8070-4798-4104-90a6-fcfa15fce096", "metadata": {}, "source": [ "Not only does our `Portfolio` class now have to know about these other objects but it also has to make significant assumptions about it, e.g. the naming of the `exchange_rate` and `capital_gains` parameter names is now hard coded in the actual class definition.\n", "\n", "References allow us to declare only the subset of parameter values our class cares about and does so without having to make any reference to anything external through it, the class becomes agnostic to the exact provider of the value." ] }, { "cell_type": "code", "execution_count": null, "id": "3c538cfb-5101-47a1-864d-b7c252af0087", "metadata": {}, "outputs": [], "source": [ "class Portfolio(param.Parameterized):\n", "\n", " foreign_value = param.Number()\n", "\n", " local_value = param.Number()\n", "\n", " taxes_owed = param.Number()\n", "\n", " exchange_rate = param.Number(allow_refs=True)\n", "\n", " capital_gains = param.Number(allow_refs=True)\n", "\n", " @param.depends('foreign_value', 'exchange_rate', watch=True, on_init=True)\n", " def _update_local(self):\n", " self.local_value = self.foreign_value * self.exchange_rate\n", " \n", " @param.depends('local_value', 'capital_gains', watch=True, on_init=True)\n", " def _update_taxes(self):\n", " self.taxes_owed = self.local_value * self.capital_gains\n", "\n", "portfolio = Portfolio(\n", " foreign_value=12000,\n", " exchange_rate=forex.param.exchange_rate,\n", " capital_gains=tax_schedule.param.capital_gains\n", ")\n", "\n", "portfolio" ] }, { "cell_type": "markdown", "id": "ff72fa97-9f08-45ed-864b-503fb19b224f", "metadata": {}, "source": [ "As we have discovered reactive references are a powerful tool not only to link two parameters together but unlock the ability to express a whole host of dynamic behavior through a simple, declarative syntax. The various valid reference types make it possible to everything from [using generators](Generators.ipynb) to push new updates to a parameter periodically or merely in sequence to expressing complex dependencies between two or more parameters with a [simple reactive expression](ReactiveExpressions.ipynb). Lastly, they ensure that you can write composable components without incorporating complex dependencies between objects into your classes themselves." ] } ], "metadata": { "language_info": { "name": "python", "pygments_lexer": "ipython3" } }, "nbformat": 4, "nbformat_minor": 5 } param-2.1.1/doc/user_guide/Serialization_and_Persistence.ipynb000066400000000000000000000655071463636336300246130ustar00rootroot00000000000000{ "cells": [ { "cell_type": "markdown", "id": "61c98db5", "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", "id": "385b4ac1", "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, "id": "965dba56", "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", "id": "05c87f9b", "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, "id": "99e9fc90", "metadata": {}, "outputs": [], "source": [ "with open('data.pickle', 'wb') as f:\n", " pickle.dump(a, f)\n", " \n", "del a" ] }, { "cell_type": "markdown", "id": "b64088a5", "metadata": {}, "source": [ "To reload the state of `a` from disk, we do a pickle \"load\":" ] }, { "cell_type": "code", "execution_count": null, "id": "806560ea", "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", "id": "39042681", "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, "id": "67267e47", "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", "id": "93044cbb", "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", "id": "3d5b6ae6", "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", "id": "e01f6c6a", "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, "id": "420c6148", "metadata": {}, "outputs": [], "source": [ "import param, datetime, pandas as pd\n", "\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", "id": "acebf174", "metadata": {}, "source": [ "To serialize this Parameterized object to a JSON string, call `.serialize_parameters()` on it:" ] }, { "cell_type": "code", "execution_count": null, "id": "cf4ab02f", "metadata": {}, "outputs": [], "source": [ "s = p.param.serialize_parameters()\n", "s" ] }, { "cell_type": "markdown", "id": "ad0d7637", "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, "id": "041cd4f8", "metadata": {}, "outputs": [], "source": [ "p.param.serialize_parameters(subset=['a','m'])" ] }, { "cell_type": "markdown", "id": "d8470cef", "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, "id": "c2644101", "metadata": {}, "outputs": [], "source": [ "p2 = P(**P.param.deserialize_parameters(s))\n", "p2" ] }, { "cell_type": "markdown", "id": "d59d7765", "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, "id": "7ee520c9", "metadata": {}, "outputs": [], "source": [ "import json\n", "dj = json.loads(s)\n", "dj" ] }, { "cell_type": "markdown", "id": "c2e54200", "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, "id": "593c1d8b", "metadata": {}, "outputs": [], "source": [ "with param.exceptions_summarized():\n", " P(**dj)" ] }, { "cell_type": "markdown", "id": "216d6c7a", "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, "id": "febbc040", "metadata": {}, "outputs": [], "source": [ "print(dj['l'])\n", "print(p2.l)" ] }, { "cell_type": "markdown", "id": "3f410eda", "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", "id": "facde47d", "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, "id": "24afd3ee", "metadata": {}, "outputs": [], "source": [ "p.param.schema()" ] }, { "cell_type": "markdown", "id": "d8d8e7bb", "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, "id": "0fce2ea0", "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", "id": "4d751577", "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, "id": "612eaac6", "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", "id": "163223d1", "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, "id": "9cc85749", "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", "id": "ac593fcd", "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, "id": "ad5a4d3b", "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(default=Q(), class_=Q)\n", " d = param.ClassSelector(default=param.Parameterized(), class_=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", "id": "20e238cb", "metadata": {}, "source": [ "We can get a script representation for this object by calling `script_repr(p)`:" ] }, { "cell_type": "code", "execution_count": null, "id": "e08e3d7b", "metadata": {}, "outputs": [], "source": [ "print(param.script_repr(p))" ] }, { "cell_type": "markdown", "id": "ca0408d7", "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", "id": "ca55f9e2", "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", "id": "75cdd8f5", "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, "id": "57dede40", "metadata": {}, "outputs": [], "source": [ "import param.parameterized\n", "param.parameterized.script_repr_suppress_defaults=True" ] }, { "cell_type": "markdown", "id": "388f982f", "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-2.1.1/doc/user_guide/Simplifying_Codebases.ipynb000066400000000000000000000351461463636336300230460ustar00rootroot00000000000000{ "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-2.1.1/doc/user_guide/index.md000066400000000000000000000045211463636336300171630ustar00rootroot00000000000000# 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. - [References](./References): Allowing parameters to resolve references to other parameters, reactive expressions, functions and generators. - [Reactive Expressions](./Reactive_Expressions): How to write expressions and functions that automatically re-evaluate when their parameter inputs change. - [Generators](./Generators): Using (asynchronous) generators to drive events on parameters and expressions in a "push" based model. - [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/how_to/param/index.html): (external site) Using Param with Panel to make GUIs ```{toctree} --- hidden: true maxdepth: 2 --- Overview Simplifying Codebases Parameters Parameter Types Dependencies and Watchers References Reactive Expressions Generators Serialization and Persistence Outputs Logging and Warnings ParameterizedFunctions Dynamic Parameters How Param Works ``` param-2.1.1/numbergen/000077500000000000000000000000001463636336300146125ustar00rootroot00000000000000param-2.1.1/numbergen/__init__.py000066400000000000000000000664461463636336300167430ustar00rootroot00000000000000""" 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().__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()._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().__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().__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: """ 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 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().__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().__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().__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().__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().__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().__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().__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().__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().__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({_k for _k,_v in locals().items() if isinstance(_v,type) and issubclass(_v,NumberGenerator)}) __all__ = _public param-2.1.1/param/000077500000000000000000000000001463636336300137305ustar00rootroot00000000000000param-2.1.1/param/__init__.py000066400000000000000000000116651463636336300160520ustar00rootroot00000000000000import os from . import version # noqa: api import from .depends import depends # noqa: api import from .parameterized import ( # noqa: api import Parameterized, Parameter, Skip, String, ParameterizedFunction, ParamOverrides, Undefined, get_logger ) from .parameterized import (batch_watch, output, script_repr, # noqa: api import discard_events, edit_constant) 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 .parameters import ( # noqa: api import guess_param_types, param_union, parameterized_class, guess_bounds, get_soft_bounds, resolve_path, normalize_path, Time, Infinity, Dynamic, Bytes, Number, Integer, Magnitude, Boolean, Tuple, NumericTuple, XYCoordinates, Callable, Action, Composite, SelectorBase, ListProxy, Selector, ObjectSelector, ClassSelector, List, HookList, Dict, Array, DataFrame, Series, Path, Filename, Foldername, FileSelector, ListSelector, MultiFileSelector, Date, CalendarDate, Color, Range, DateRange, CalendarDateRange, Event, ) from .reactive import bind, rx # noqa: api import from ._utils import ( # noqa: api import produce_value, as_unicode, is_ordered_dict, hashable, named_objs, descendents, concrete_descendents, abbreviate_paths, exceptions_summarized, _is_number, ) # Define '__version__' try: # For performance reasons on imports, avoid importing setuptools_scm # if not in a .git folder if os.path.exists(os.path.join(os.path.dirname(__file__), "..", ".git")): # If setuptools_scm is installed (e.g. in a development environment with # an editable install), then use it to determine the version dynamically. from setuptools_scm import get_version # This will fail with LookupError if the package is not installed in # editable mode or if Git is not installed. __version__ = get_version(root="..", relative_to=__file__) else: raise FileNotFoundError except (ImportError, LookupError, FileNotFoundError): # As a fallback, use the version that is hard-coded in the file. try: # __version__ was added in _version in setuptools-scm 7.0.0, we rely on # the hopefully stable version variable. from ._version import version as __version__ except (ModuleNotFoundError, ImportError): # Either _version doesn't exist (ModuleNotFoundError) or version isn't # in _version (ImportError). ModuleNotFoundError is a subclass of # ImportError, let's be explicit anyway. # Try something else: from importlib.metadata import version as mversion, PackageNotFoundError try: __version__ = mversion("param") except PackageNotFoundError: # The user is probably trying to run this without having installed # the package. __version__ = "0.0.0+unknown" #: 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 __all__ = ( 'Action', 'Array', 'Boolean', 'Bytes', 'CRITICAL', 'CalendarDate', 'CalendarDateRange', 'Callable', 'ClassSelector', 'Color', 'Composite', 'DEBUG', 'DataFrame', 'Date', 'DateRange', 'Dict', 'Dynamic', 'ERROR', 'Event', 'FileSelector', 'Filename', 'Foldername', 'HookList', 'INFO', 'Infinity', 'Integer', 'List', 'ListProxy', 'ListSelector', 'Magnitude', 'MultiFileSelector', 'Number', 'NumericTuple', 'ObjectSelector', 'ParamOverrides', 'Parameter', 'Parameterized', 'ParameterizedFunction', 'Path', 'Range', 'Selector', 'SelectorBase', 'Series', 'Skip', 'String', 'Time', 'Tuple', 'Undefined', 'VERBOSE', 'WARNING', 'XYCoordinates', '__version__', '_is_number', 'abbreviate_paths', 'as_unicode', 'batch_watch', 'bind', 'concrete_descendents', 'depends', 'descendents', 'discard_events', 'edit_constant', 'exceptions_summarized', 'get_logger', 'get_soft_bounds', 'guess_bounds', 'guess_param_types', 'hashable', 'is_ordered_dict', 'logging_level', 'main', 'named_objs', 'normalize_path', 'output', 'param_union', 'parameterized_class', 'produce_value', 'random_seed', 'resolve_path', 'rx', 'script_repr', 'serializer', 'shared_parameters', 'version', ) param-2.1.1/param/_utils.py000066400000000000000000000441521463636336300156070ustar00rootroot00000000000000import asyncio import collections import contextvars import datetime as dt import inspect import functools import numbers import os import re import sys import traceback import warnings from collections import defaultdict, OrderedDict from contextlib import contextmanager from numbers import Real from textwrap import dedent from threading import get_ident from collections import abc DEFAULT_SIGNATURE = inspect.Signature([ inspect.Parameter('self', inspect.Parameter.POSITIONAL_OR_KEYWORD), inspect.Parameter('params', inspect.Parameter.VAR_KEYWORD), ]) MUTABLE_TYPES = (abc.MutableSequence, abc.MutableSet, abc.MutableMapping) class ParamWarning(Warning): """Base Param Warning""" class ParamPendingDeprecationWarning(ParamWarning, PendingDeprecationWarning): """Param PendingDeprecationWarning This warning type is useful when the warning is not meant to be displayed to REPL/notebooks users, as DeprecationWarning are displayed when triggered by code in __main__ (__name__ == '__main__' in a REPL). """ class ParamDeprecationWarning(ParamWarning, DeprecationWarning): """Param DeprecationWarning Ignored by default except when triggered by code in __main__ """ class ParamFutureWarning(ParamWarning, FutureWarning): """Param FutureWarning Always displayed. """ class Skip(Exception): """ Exception that allows skipping an update when resolving a reference. """ def _deprecated(extra_msg="", warning_cat=ParamDeprecationWarning): def decorator(func): """Internal decorator used to mark functions/methods as deprecated.""" @functools.wraps(func) def inner(*args, **kwargs): msg = f"{func.__name__!r} has been deprecated and will be removed in a future version." if extra_msg: em = dedent(extra_msg) em = em.strip().replace('\n', ' ') msg = msg + ' ' + em warnings.warn(msg, category=warning_cat, stacklevel=2) return func(*args, **kwargs) return inner return decorator def _deprecate_positional_args(func): """Internal decorator for methods that issues warnings for positional arguments Using the keyword-only argument syntax in pep 3102, arguments after the ``*`` will issue a warning when passed as a positional argument. Adapted from scikit-learn """ signature = inspect.signature(func) pos_or_kw_args = [] kwonly_args = [] for name, param in signature.parameters.items(): if param.kind in (inspect.Parameter.POSITIONAL_OR_KEYWORD, inspect.Parameter.POSITIONAL_ONLY): pos_or_kw_args.append(name) elif param.kind == inspect.Parameter.KEYWORD_ONLY: kwonly_args.append(name) @functools.wraps(func) def inner(*args, **kwargs): name = func.__qualname__.split('.')[0] n_extra_args = len(args) - len(pos_or_kw_args) if n_extra_args > 0: extra_args = ", ".join(kwonly_args[:n_extra_args]) warnings.warn( f"Passing '{extra_args}' as positional argument(s) to 'param.{name}' " "has been deprecated since Param 2.0.0 and will raise an error in a future version, " "please pass them as keyword arguments.", ParamDeprecationWarning, stacklevel=2, ) zip_args = zip(kwonly_args[:n_extra_args], args[-n_extra_args:]) kwargs.update({name: arg for name, arg in zip_args}) return func(*args[:-n_extra_args], **kwargs) return func(*args, **kwargs) return inner # Copy of Python 3.2 reprlib's recursive_repr but allowing extra arguments 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 def _is_auto_name(class_name, instance_name): return re.match('^'+class_name+'[0-9]{5}$', instance_name) def _find_pname(pclass): """ Go up the stack and attempt to find a Parameter declaration of the form `pname = param.Parameter(` or `pname = pm.Parameter(`. """ stack = traceback.extract_stack() for frame in stack: match = re.match(r"^(\S+)\s*=\s*(param|pm)\." + pclass + r"\(", frame.line) if match: return match.group(1) def _validate_error_prefix(parameter, attribute=None): """ Generate an error prefix suitable for Parameters when they raise a validation error. - unbound and name can't be found: "Number parameter" - unbound and name can be found: "Number parameter 'x'" - bound parameter: "Number parameter 'P.x'" """ from param.parameterized import ParameterizedMetaclass pclass = type(parameter).__name__ if parameter.owner is not None: if type(parameter.owner) is ParameterizedMetaclass: powner = parameter.owner.__name__ else: powner = type(parameter.owner).__name__ else: powner = None pname = parameter.name out = [] if attribute: out.append(f'Attribute {attribute!r} of') out.append(f'{pclass} parameter') if pname: if powner: desc = f'{powner}.{pname}' else: desc = pname out.append(f'{desc!r}') else: try: pname = _find_pname(pclass) if pname: out.append(f'{pname!r}') except Exception: pass return ' '.join(out) def _is_mutable_container(value): """True for mutable containers, which typically need special handling when being copied""" return issubclass(type(value), MUTABLE_TYPES) def _dict_update(dictionary, **kwargs): """ Small utility to update a copy of a dict with the provided keyword args. """ d = dictionary.copy() d.update(kwargs) return d def full_groupby(l, key=lambda x: x): """ Groupby implementation which does not require a prior sort """ d = defaultdict(list) for item in l: d[key(item)].append(item) return d.items() def iscoroutinefunction(function): """ Whether the function is an asynchronous generator or a coroutine. """ # Partial unwrapping not required starting from Python 3.11.0 # See https://github.com/holoviz/param/pull/894#issuecomment-1867084447 while isinstance(function, functools.partial): function = function.func return ( inspect.isasyncgenfunction(function) or inspect.iscoroutinefunction(function) ) async def _to_thread(func, /, *args, **kwargs): """ Polyfill for asyncio.to_thread in Python < 3.9 """ loop = asyncio.get_running_loop() ctx = contextvars.copy_context() func_call = functools.partial(ctx.run, func, *args, **kwargs) return await loop.run_in_executor(None, func_call) async def _to_async_gen(sync_gen): done = object() def safe_next(): # Converts StopIteration to a sentinel value to avoid: # TypeError: StopIteration interacts badly with generators and cannot be raised into a Future try: return next(sync_gen) except StopIteration: return done while True: if sys.version_info >= (3, 9): value = await asyncio.to_thread(safe_next) else: value = await _to_thread(safe_next) if value is done: break yield value def flatten(line): """ Flatten an arbitrarily nested sequence. Inspired by: pd.core.common.flatten Parameters ---------- line : sequence The sequence to flatten Notes ----- This only flattens list, tuple, and dict sequences. Returns ------- flattened : generator """ for element in line: if any(isinstance(element, tp) for tp in (list, tuple, dict)): yield from flatten(element) else: yield element def accept_arguments(f): """ Decorator for decorators that accept arguments """ @functools.wraps(f) def _f(*args, **kwargs): return lambda actual_f: f(actual_f, *args, **kwargs) return _f 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 # PARAM3_DEPRECATION @_deprecated() 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. .. deprecated:: 2.0.0 """ return _produce_value(value_obj) # PARAM3_DEPRECATION @_deprecated() def as_unicode(obj): """ Safely casts any object to unicode including regular string (i.e. bytes) types in python 2. .. deprecated:: 2.0.0 """ return str(obj) # PARAM3_DEPRECATION @_deprecated() def is_ordered_dict(d): """ Predicate checking for ordered dictionaries. OrderedDict is always ordered, and vanilla Python dictionaries are ordered for Python 3.6+ .. deprecated:: 2.0.0 """ 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 # PARAM3_DEPRECATION @_deprecated() 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. .. deprecated:: 2.0.0 """ return _hashable(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 = str(obj) objs[k] = obj return objs # PARAM3_DEPRECATION @_deprecated() 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. .. deprecated:: 2.0.0 """ return _named_objs(objlist, namesdict=namesdict) 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( f'unable to infer range, value from: ({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(f'value must be between min and max (min={min}, value={value}, max={max})') return min, max, value def _deserialize_from_path(ext_to_routine, path, type_name): """ Call deserialization routine with path according to extension. ext_to_routine should be a dictionary mapping each supported file extension to a corresponding loading function. """ if not os.path.isfile(path): raise FileNotFoundError( "Could not parse file '{}' as {}: does not exist or is not a file" "".format(path, type_name)) root, ext = os.path.splitext(path) if ext in {'.gz', '.bz2', '.xz', '.zip'}: # A compressed type. We'll assume the routines can handle such extensions # transparently (if not, we'll fail later) ext = os.path.splitext(root)[1] # FIXME(sdrobert): try...except block below with "raise from" might be a good idea # once py2.7 support is removed. Provides error + fact that failure occurred in # deserialization if ext in ext_to_routine: return ext_to_routine[ext](path) raise ValueError( "Could not parse file '{}' as {}: no deserialization method for files with " "'{}' extension. Supported extensions: {}" "".format(path, type_name, ext, ', '.join(sorted(ext_to_routine)))) 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 _is_abstract(class_): try: return class_.abstract except AttributeError: return False 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] # 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 {c.__name__:c for c in descendents(parentclass) if not _is_abstract(c)} 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()]) # PARAM3_DEPRECATION @_deprecated() 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. .. deprecated:: 2.0.0 """ return _abbreviate_paths(pathspec, named_paths) def _to_datetime(x): """ Internal function that will convert date objs to datetime objs, used for comparing date and datetime objects without error. """ if isinstance(x, dt.date) and not isinstance(x, dt.datetime): return dt.datetime(*x.timetuple()[:6]) return x @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(f"{etype.__name__}: {value}", file=sys.stderr) def _in_ipython(): try: get_ipython() return True except NameError: return False _running_tasks = set() def async_executor(func): try: event_loop = asyncio.get_running_loop() except RuntimeError: event_loop = asyncio.new_event_loop() if event_loop.is_running(): task = asyncio.ensure_future(func()) _running_tasks.add(task) task.add_done_callback(_running_tasks.discard) else: event_loop.run_until_complete(func()) param-2.1.1/param/depends.py000066400000000000000000000121511463636336300157240ustar00rootroot00000000000000import inspect from collections import defaultdict from functools import wraps from .parameterized import ( Parameter, Parameterized, ParameterizedMetaclass, transform_reference, ) from ._utils import accept_arguments, iscoroutinefunction @accept_arguments def depends(func, *dependencies, watch=False, on_init=False, **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. Parameters ---------- watch : bool, optional Whether to invoke the function/method when the dependency is updated, by default False on_init : bool, optional Whether to invoke the function/method when the instance is created, by default False """ dependencies, kw = ( tuple(transform_reference(arg) for arg in dependencies), {key: transform_reference(arg) for key, arg in kw.items()} ) if inspect.isgeneratorfunction(func): @wraps(func) def _depends(*args, **kw): for val in func(*args, **kw): yield val elif inspect.isasyncgenfunction(func): @wraps(func) async def _depends(*args, **kw): async for val in func(*args, **kw): yield val elif iscoroutinefunction(func): @wraps(func) async def _depends(*args, **kw): return await func(*args, **kw) 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, str): string_specs = True elif hasattr(dep, '_dinfo'): pass 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, str) 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 inspect.isgeneratorfunction(func): 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()} for val in func(*args, **dep_kwargs): yield val elif inspect.isasyncgenfunction(func): async 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()} async for val in func(*args, **dep_kwargs): yield val elif iscoroutinefunction(func): async 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()} await func(*args, **dep_kwargs) 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 param-2.1.1/param/display.py000066400000000000000000000014661463636336300157560ustar00rootroot00000000000000import weakref _display_accessors = {} _reactive_display_objs = weakref.WeakSet() def register_display_accessor(name, accessor, force=False): if name in _display_accessors and not force: if _display_accessors[name].__module__ != accessor.__module__: raise KeyError( f'Display accessor {name!r} already registered. Override it ' 'by setting force=True or unregister the existing accessor first.' ) _display_accessors[name] = accessor for fn in _reactive_display_objs: setattr(fn, name, accessor(fn)) def unregister_display_accessor(name): if name not in _display_accessors: raise KeyError('No such display accessor: {name!r}') del _display_accessors[name] for fn in _reactive_display_objs: delattr(fn, name) param-2.1.1/param/ipython.py000066400000000000000000000352621463636336300160040ustar00rootroot00000000000000""" 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. """ import re import itertools import textwrap import uuid import param from param.display import register_display_accessor from param._utils import async_executor # 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: """ 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 = {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 = {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 = [f"{' ' * 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, preserving the original ordering for parameters with the same precedence. """ 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) ordered_groups = [list(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 = {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 = f'{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'] = f'({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 = {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 = f"{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 = f'Parameters of {class_name!r} instance{obj_name}' if title is None: title = 'Parameters of %r' % param_obj.name heading_line = '=' * len(title) heading_text = f"{title}\n{heading_line}\n" param_info = self.get_param_info(param_obj, include_super=True) if not param_info[0]: top_heading = green % heading_text return f"{top_heading}\nObject 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 = f"{heading_text}\n{'=' * len(heading_text)}" docstring_heading = (green % heading_string) return f"{top_heading}\n\n{table}\n\n{docstring_heading}\n\n{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().__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) class IPythonDisplay: """ Reactive display handler that updates the output. """ enabled = True def __init__(self, reactive): self._reactive = reactive def __call__(self): from param.depends import depends from param.parameterized import Undefined, resolve_ref from param.reactive import rx handle = None if isinstance(self._reactive, rx): cb = self._reactive._callback @depends(*self._reactive._params, watch=True) def update_handle(*args, **kwargs): if handle is not None: handle.update(cb()) else: cb = self._reactive @depends(*resolve_ref(cb), watch=True) def update_handle(*args, **kwargs): if handle is not None: handle.update(cb()) try: obj = cb() if obj is Undefined: obj = None handle = display(obj, display_id=uuid.uuid4().hex) # noqa except TypeError: raise NotImplementedError def ipython_async_executor(func): event_loop = None try: ip = get_ipython() # noqa if ip.kernel: # We are in Jupyter and can piggyback the tornado IOLoop from tornado.ioloop import IOLoop ioloop = IOLoop.current() event_loop = ioloop.asyncio_loop # type: ignore if event_loop.is_running(): ioloop.add_callback(func) else: event_loop.run_until_complete(func()) return except (NameError, AttributeError): pass async_executor(func) register_display_accessor('_ipython_display_', IPythonDisplay) param-2.1.1/param/parameterized.py000066400000000000000000005360311463636336300171460ustar00rootroot00000000000000""" 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 asyncio import copy import datetime as dt import html import inspect import logging import numbers import operator import random import re import types import typing import warnings # 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 html import escape from itertools import chain from operator import itemgetter, attrgetter from types import FunctionType, MethodType from contextlib import contextmanager from logging import DEBUG, INFO, WARNING, ERROR, CRITICAL from ._utils import ( DEFAULT_SIGNATURE, ParamDeprecationWarning as _ParamDeprecationWarning, ParamFutureWarning as _ParamFutureWarning, Skip, _deprecated, _deprecate_positional_args, _dict_update, _in_ipython, _is_auto_name, _is_mutable_container, _recursive_repr, _to_async_gen, _validate_error_prefix, accept_arguments, iscoroutinefunction, descendents, ) # Ideally setting param_pager would be in __init__.py but param_pager is # needed on import to create the Parameterized class, so it'd need to precede # importing parameterized.py in __init__.py which would be a little weird. if _in_ipython(): # In case the optional ipython module is unavailable try: from .ipython import ParamPager, ipython_async_executor as async_executor param_pager = ParamPager(metaclass=True) # Generates param description except ImportError: from ._utils import async_executor else: from ._utils import async_executor param_pager = None from inspect import getfullargspec dt_types = (dt.datetime, dt.date) _int_types = (int,) try: import numpy as np dt_types = dt_types + (np.datetime64,) _int_types = _int_types + (np.integer,) except: pass 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 # Hook to apply to depends and bind arguments to turn them into valid parameters _reference_transforms = [] def register_reference_transform(transform): """ Appends a transform to extract potential parameter dependencies from an object. Arguments --------- transform: Callable[Any, Any] """ return _reference_transforms.append(transform) def transform_reference(arg): """ Applies transforms to turn objects which should be treated like a parameter reference into a valid reference that can be resolved by Param. This is useful for adding handling for depending on objects that are not simple Parameters or functions with dependency definitions. """ for transform in _reference_transforms: if isinstance(arg, Parameter) or hasattr(arg, '_dinfo'): break arg = transform(arg) return arg def eval_function_with_deps(function): """Evaluates a function after resolving its dependencies. Calls and returns a function after resolving any dependencies stored on the _dinfo attribute and passing the resolved values as arguments. """ args, kwargs = (), {} if hasattr(function, '_dinfo'): arg_deps = function._dinfo['dependencies'] kw_deps = function._dinfo.get('kw', {}) if kw_deps or any(isinstance(d, Parameter) for d in arg_deps): args = (getattr(dep.owner, dep.name) for dep in arg_deps) kwargs = {n: getattr(dep.owner, dep.name) for n, dep in kw_deps.items()} return function(*args, **kwargs) def resolve_value(value, recursive=True): """ Resolves the current value of a dynamic reference. """ if not recursive: pass elif isinstance(value, (list, tuple)): return type(value)(resolve_value(v) for v in value) elif isinstance(value, dict): return type(value)((resolve_value(k), resolve_value(v)) for k, v in value.items()) elif isinstance(value, slice): return slice( resolve_value(value.start), resolve_value(value.stop), resolve_value(value.step) ) value = transform_reference(value) is_gen = inspect.isgeneratorfunction(value) if hasattr(value, '_dinfo') or iscoroutinefunction(value) or is_gen: value = eval_function_with_deps(value) if is_gen: value = _to_async_gen(value) elif isinstance(value, Parameter): value = getattr(value.owner, value.name) return value def resolve_ref(reference, recursive=False): """ Resolves all parameters a dynamic reference depends on. """ if recursive: if isinstance(reference, (list, tuple, set)): return [r for v in reference for r in resolve_ref(v, recursive)] elif isinstance(reference, dict): return [r for kv in reference.items() for o in kv for r in resolve_ref(o, recursive)] elif isinstance(reference, slice): return [r for v in (reference.start, reference.stop, reference.step) for r in resolve_ref(v, recursive)] reference = transform_reference(reference) if hasattr(reference, '_dinfo'): dinfo = getattr(reference, '_dinfo', {}) args = list(dinfo.get('dependencies', [])) kwargs = list(dinfo.get('kw', {}).values()) refs = [] for arg in (args + kwargs): if isinstance(arg, str): owner = get_method_owner(reference) if arg in owner.param: arg = owner.param[arg] elif '.' in arg: path = arg.split('.') arg = owner for attr in path[:-1]: arg = getattr(arg, attr) arg = arg.param[path[-1]] else: arg = getattr(owner, arg) refs.extend(resolve_ref(arg)) return refs elif isinstance(reference, Parameter): return [reference] return [] def _identity_hook(obj, val): """To be removed when set_hook is removed""" return val class _Undefined: """ Dummy value to signal completely undefined values rather than simple None values. """ def __bool__(self): # Haven't defined whether Undefined is falsy or truthy, # so to avoid subtle bugs raise an error when it # is used in a comparison without `is`. raise RuntimeError('Use `is` to compare Undefined') def __repr__(self): return '' Undefined = _Undefined() @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(f"Level {level!r} not in {levels!r}") 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() # PARAM3_DEPRECATION @_deprecated(extra_msg="Use instead `batch_call_watchers`.") @contextmanager def batch_watch(parameterized, enable=True, run=True): with _batch_call_watchers(parameterized, enable, run): yield @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 _syncing(parameterized, parameters): old = parameterized._param__private.syncing parameterized._param__private.syncing = set(old) | set(parameters) try: yield finally: parameterized._param__private.syncing = old @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._state_watchers), list(parameterized.param._events)) try: yield except: raise finally: parameterized.param._BATCH_WATCH = batch_watch parameterized.param._state_watchers = watchers parameterized.param._events = events 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 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)] # PARAM3_DEPRECATION @_deprecated() 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 # PARAM3_DEPRECATION # 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). @_deprecated() def add_metaclass(metaclass): """Class decorator for creating a class with a metaclass. .. deprecated:: 2.0.0 """ 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: """ '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. """ def __init__(self, method): self.method = method def __get__(self, instance, owner): if instance is None: # Class call return self.method.__get__(owner) else: # Instance call return self.method.__get__(instance, owner) def _getattrr(obj, attr, *args): def _getattr(obj, attr): return getattr(obj, attr, *args) return reduce(_getattr, [obj] + attr.split('.')) def no_instance_params(cls): """ Disables instance parameters on the class """ cls._param__private.disable_instance_params = True return cls def _instantiate_param_obj(paramobj, owner=None): """Return a Parameter object suitable for instantiation given the class's Parameter object""" # Shallow-copy Parameter object without the watchers p = copy.copy(paramobj) p.owner = owner # Reset watchers since class parameter watcher should not execute # on instance parameters p.watchers = {} # shallow-copy any mutable slot values other than the actual default for s in p.__class__._all_slots_: v = getattr(p, s) if _is_mutable_container(v) and s != "default": setattr(p, s, copy.copy(v)) return p def _instantiated_parameter(parameterized, param): """ Given a Parameterized object and one of its class Parameter objects, return the appropriate Parameter object for this instance, instantiating it if need be. """ if (getattr(parameterized._param__private, 'initialized', False) and param.per_instance and not getattr(type(parameterized)._param__private, 'disable_instance_params', False)): key = param.name if key not in parameterized._param__private.params: parameterized._param__private.params[key] = _instantiate_param_obj(param, parameterized) param = parameterized._param__private.params[key] return param def instance_descriptor(f): # If parameter has an instance Parameter, delegate setting def _f(self, obj, val): # obj is None when the metaclass is setting if obj is not None: instance_param = obj._param__private.params.get(self.name) if instance_param is None: instance_param = _instantiated_parameter(obj, self) 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__ # PARAM3_DEPRECATION def recursive_repr(fillvalue='...'): """ Decorator to make a repr function return fillvalue for a recursive call .. deprecated:: 1.12.0 """ warnings.warn( 'recursive_repr has been deprecated and will be removed in a future version.', category=_ParamDeprecationWarning, stacklevel=2, ) return _recursive_repr(fillvalue=fillvalue) @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. 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 or using tuples of the same format, 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: # (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.cls if minfo.inst is None else minfo.inst).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 extract_dependencies(function): """ Extract references from a method or function that declares the references. """ subparameters = list(function._dinfo['dependencies'])+list(function._dinfo['kw'].values()) params = [] for p in subparameters: if isinstance(p, str): owner = get_method_owner(function) *subps, p = p.split('.') for subp in subps: owner = getattr(owner, subp, None) if owner is None: raise ValueError('Cannot depend on undefined sub-parameter {p!r}.') if p in owner.param: pobj = owner.param[p] if pobj not in params: params.append(pobj) else: for sp in extract_dependencies(getattr(owner, p)): if sp not in params: params.append(sp) elif p not in params: params.append(p) return params # Two callers at the module top level to support pickling. async def _async_caller(*events, what='value', changed=None, callback=None, function=None): if callback: callback(*events) if not _skip_event or not _skip_event(*events, what=what, changed=changed): await function() def _sync_caller(*events, what='value', changed=None, callback=None, function=None): if callback: callback(*events) if not _skip_event(*events, what=what, changed=changed): return function() 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) _caller = _async_caller if iscoroutinefunction(function) else _sync_caller caller = partial(_caller, what=what, changed=changed, callback=callback, function=function) caller._watcher_name = method_name return caller def _add_doc(obj, docstring): """Add a docstring to a namedtuple""" 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().__new__(cls_, **values) def __str__(self): cls = type(self) attrs = ', '.join([f'{f}={getattr(self, f)!r}' for f in cls._fields]) return f"{cls.__name__}({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')) # Compute all slots in order, using a dict later turned into a list # as it's the fastest way to get an ordered set in Python all_slots = {} for bcls in set(chain(*(base.__mro__[::-1] for base in bases))): all_slots.update(dict.fromkeys(getattr(bcls, '__slots__', []))) # 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__'] = [] else: all_slots.update(dict.fromkeys(classdict['__slots__'])) classdict['_all_slots_'] = list(all_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) class _ParameterBase(metaclass=ParameterMetaclass): """ Base Parameter class used to dynamically update the signature of all the Parameters. """ @classmethod def _modified_slots_defaults(cls): defaults = cls._slot_defaults.copy() defaults['label'] = defaults.pop('_label') return defaults @classmethod def __init_subclass__(cls): # _update_signature has been tested against the Parameters available # in Param, we don't want to break the Parameters created elsewhere # so wrapping this in a loose try/except. try: cls._update_signature() except Exception: # The super signature has been changed so we need to get the one # from the class constructor directly. cls.__signature__ = inspect.signature(cls.__init__) @classmethod def _update_signature(cls): defaults = cls._modified_slots_defaults() new_parameters = {} for i, kls in enumerate(cls.mro()): if kls.__name__.startswith('_'): continue sig = inspect.signature(kls.__init__) for pname, parameter in sig.parameters.items(): if pname == 'self': continue if i >= 1 and parameter.default == inspect.Signature.empty: continue if parameter.kind in (inspect.Parameter.VAR_KEYWORD, inspect.Parameter.VAR_POSITIONAL): continue if getattr(parameter, 'default', None) is Undefined: if pname not in defaults: raise LookupError( f'Argument {pname!r} of Parameter {cls.__name__!r} has no ' 'entry in _slot_defaults.' ) default = defaults[pname] if callable(default) and hasattr(default, 'sig'): default = default.sig new_parameter = parameter.replace(default=default) else: new_parameter = parameter if i >= 1: new_parameter = new_parameter.replace(kind=inspect.Parameter.KEYWORD_ONLY) new_parameters.setdefault(pname, new_parameter) def _sorter(p): if p.default == inspect.Signature.empty: return 0 else: return 1 new_parameters = sorted(new_parameters.values(), key=_sorter) new_sig = sig.replace(parameters=new_parameters) cls.__signature__ = new_sig class Parameter(_ParameterBase): """ 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._param__private.values['p']) # # * When a1.p is requested, a1._param__private.values['p'] is # returned. When a2.p is requested, 'p' is not found in # a1._param__private.values, 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 'name'. # # * When a Parameterized instance has its own local value for a # parameter, it is stored as 'p._param__private.values[X]' where X is the # name of the Parameter # 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', 'default', 'doc', 'precedence', 'instantiate', 'constant', 'readonly', 'pickle_default_value', 'allow_None', 'per_instance', 'watchers', 'owner', 'allow_refs', 'nested_refs', '_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, and name are # set. _serializers = {'json': serializer.JSONSerialization} _slot_defaults = dict( default=None, precedence=None, doc=None, _label=None, instantiate=False, constant=False, readonly=False, pickle_default_value=True, allow_None=False, per_instance=True, allow_refs=False, nested_refs=False ) # Parameters can be updated during Parameterized class creation when they # are defined multiple times in a class hierarchy. We have to record which # Parameter slots require the default value to be re-validated. Any slots # in this list do not have to trigger such re-validation. _non_validated_slots = ['_label', 'doc', 'name', 'precedence', 'constant', 'pickle_default_value', 'watchers', 'owner'] @typing.overload def __init__( self, default=None, *, doc=None, label=None, precedence=None, instantiate=False, constant=False, readonly=False, pickle_default_value=True, allow_None=False, per_instance=True, allow_refs=False, nested_refs=False ): ... @_deprecate_positional_args def __init__(self, default=Undefined, *, doc=Undefined, # pylint: disable-msg=R0913 label=Undefined, precedence=Undefined, instantiate=Undefined, constant=Undefined, readonly=Undefined, pickle_default_value=Undefined, allow_None=Undefined, per_instance=Undefined, allow_refs=Undefined, nested_refs=Undefined): """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. allow_refs: if True allows automatically linking parameter references to this Parameter, i.e. the parameter value will automatically reflect the current value of the reference that is passed in. nested_refs: if True and allow_refs=True then even nested objects such as dictionaries, lists, slices, tuples and sets will be inspected for references and will be automatically resolved. 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.allow_refs = allow_refs self.nested_refs = nested_refs self.precedence = precedence self.default = default self.doc = doc self.constant = constant is True or readonly is True # readonly => constant self.readonly = readonly self._label = label self._set_instantiate(instantiate) self.pickle_default_value = pickle_default_value self._set_allow_None(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(f'Mode {mode!r} not in available serialization formats {list(self._serializers.keys())!r}') return self._serializers[mode].param_schema(self.__class__.__name__, self, safe=safe, subset=subset) @property def rx(self): from .reactive import reactive_ops return reactive_ops(self) @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_allow_None(self, allow_None): # allow_None is set following these rules (last takes precedence): # 1. to False by default # 2. to the value provided in the constructor, if any # 3. to True if default is None if self.default is None: self.allow_None = True elif allow_None is not Undefined: self.allow_None = allow_None else: self.allow_None = self._slot_defaults['allow_None'] 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 elif instantiate is not Undefined: self.instantiate = instantiate else: # Default value self.instantiate = self._slot_defaults['instantiate'] def __setattr__(self, attribute, value): if attribute == 'name': name = getattr(self, 'name', None) if name is not None and value != name: raise AttributeError("Parameter name cannot be modified after " "it has been bound to a Parameterized.") is_slot = attribute in self.__class__._all_slots_ has_watcher = attribute != "default" and attribute in getattr(self, 'watchers', []) if not (is_slot or has_watcher): # Return early if attribute is not a slot return super().__setattr__(attribute, value) # Otherwise get the old value so we can call watcher/on_set old = getattr(self, attribute, NotImplemented) if is_slot: try: self._on_set(attribute, old, value) except AttributeError: pass super().__setattr__(attribute, value) if has_watcher and old is not NotImplemented: self._trigger_event(attribute, old, value) def _trigger_event(self, attribute, old, new): event = Event(what=attribute, name=self.name, obj=None, cls=self.owner, old=old, new=new, 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 __getattribute__(self, key): """ Allow slot values to be Undefined in an "unbound" parameter, i.e. one that is not (yet) owned by a Parameterized object, in which case their value will be retrieved from the _slot_defaults dictionary. """ v = object.__getattribute__(self, key) # Safely checks for name (avoiding recursion) to decide if this object is unbound if v is Undefined and key != "name" and getattr(self, "name", None) is None: try: v = self._slot_defaults[key] except KeyError as e: raise KeyError( f'Slot {key!r} on unbound parameter {self.__class__.__name__!r} ' 'has no default value defined in `_slot_defaults`' ) from e if callable(v): v = v(self) return v def _on_set(self, attribute, old, value): """ Can be overridden on subclasses to handle changes when parameter attribute is set. """ def _update_state(self): """ Can be overridden on subclasses to update a Parameter state, i.e. slot values, after the slot values have been set in the inheritance procedure. """ 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: # Attribute error when .values does not exist (_ClassPrivate) # and KeyError when there's no cached value for this parameter. try: result = obj._param__private.values[self.name] except (AttributeError, KeyError): result = 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 `values` dictionary located in the private namespace `_param__private`, under the parameter's 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). """ name = self.name if obj is not None and self.allow_refs and obj._param__private.initialized: syncing = name in obj._param__private.syncing ref, deps, val, is_async = obj.param._resolve_ref(self, val) refs = obj._param__private.refs if ref is not None: self.owner.param._update_ref(name, ref) elif name in refs and not syncing: del refs[name] if name in obj._param__private.async_refs: obj._param__private.async_refs.pop(name).cancel() if is_async or val is Undefined: return # Deprecated Number set_hook called here to avoid duplicating setter if hasattr(self, 'set_hook'): val = self.set_hook(obj, val) if self.set_hook is not _identity_hook: # PARAM3_DEPRECATION warnings.warn( 'Number.set_hook has been deprecated.', category=_ParamDeprecationWarning, stacklevel=6, ) 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" % name) elif obj is None: _old = self.default self.default = val elif not obj._param__private.initialized: _old = obj._param__private.values.get(self.name, self.default) obj._param__private.values[self.name] = val else: _old = obj._param__private.values.get(self.name, self.default) if val is not _old: raise TypeError("Constant parameter '%s' cannot be modified" % name) else: if obj is None: _old = self.default self.default = val else: # When setting a Parameter before calling super. if not isinstance(obj._param__private, _InstancePrivate): obj._param__private = _InstancePrivate( explicit_no_refs=type(obj)._param__private.explicit_no_refs ) _old = obj._param__private.values.get(name, self.default) obj._param__private.values[name] = val self._post_setter(obj, val) if obj is not None: if not hasattr(obj, '_param__private') or not getattr(obj._param__private, 'initialized', False): return obj.param._update_deps(name) if obj is None: watchers = self.watchers.get("value") elif name in obj._param__private.watchers: watchers = obj._param__private.watchers[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=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 {} parameter {!r} has already been ' 'assigned a name by the {} 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.'.format(type(self).__name__, self.name, self.owner.name, attrib_name)) self.name = attrib_name def __getstate__(self): """ All Parameters have slots, not a dict, so we have to support pickle and deepcopy ourselves. """ return {slot: getattr(self, slot) for slot in self.__class__._all_slots_} def __setstate__(self,state): # set values of __slots__ (instead of in non-existent __dict__) 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'] _slot_defaults = _dict_update(Parameter._slot_defaults, default="", regex=None) @typing.overload def __init__( self, default="", *, regex=None, doc=None, label=None, precedence=None, instantiate=False, constant=False, readonly=False, pickle_default_value=True, allow_None=False, per_instance=True, allow_refs=False, nested_refs=False ): ... @_deprecate_positional_args def __init__(self, default=Undefined, *, regex=Undefined, **kwargs): super().__init__(default=default, **kwargs) self.regex = regex self._validate(self.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( f'{_validate_error_prefix(self)} value {val!r} does not ' f'match regex {regex!r}.' ) def _validate_value(self, val, allow_None): if allow_None and val is None: return if not isinstance(val, str): raise ValueError( f'{_validate_error_prefix(self)} only takes a string value, ' f'not value of {type(val)}.' ) def _validate(self, val): self._validate_value(val, self.allow_None) self._validate_regex(val, self.regex) class shared_parameters: """ 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._param__private.initialized parameterized_instance._param__private.initialized = False ret = fn(self_, *args, **kw) parameterized_instance._param__private.initialized = original_initialized return ret return override_initialization class Comparator: """ 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, str: operator.eq, bytes: operator.eq, type(None): operator.eq, lambda o: hasattr(o, '_infinitely_iterable'): operator.eq, # Time } 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(): try: are_instances = isinstance(obj1, eq_type) and isinstance(obj2, eq_type) except TypeError: pass else: if are_instances: return eq(obj1, obj2) if isinstance(eq_type, FunctionType) and eq_type(obj1) and eq_type(obj2): 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 _ParametersRestorer: """ Context-manager to handle the reset of parameter values after an update. """ def __init__(self, *, parameters, restore, refs=None): self._parameters = parameters self._restore = restore self._refs = {} if refs is None else refs def __enter__(self): return self._restore def __exit__(self, exc_type, exc_value, exc_tb): try: self._parameters._update(dict(self._restore, **self._refs)) finally: self._restore = {} class Parameters: """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. """ 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._param__private.parameters_state['BATCH_WATCH'] @_BATCH_WATCH.setter def _BATCH_WATCH(self_, value): self_.self_or_cls._param__private.parameters_state['BATCH_WATCH'] = value @property def _TRIGGER(self_): return self_.self_or_cls._param__private.parameters_state['TRIGGER'] @_TRIGGER.setter def _TRIGGER(self_, value): self_.self_or_cls._param__private.parameters_state['TRIGGER'] = value @property def _events(self_): return self_.self_or_cls._param__private.parameters_state['events'] @_events.setter def _events(self_, value): self_.self_or_cls._param__private.parameters_state['events'] = value @property def _state_watchers(self_): return self_.self_or_cls._param__private.parameters_state['watchers'] @_state_watchers.setter def _state_watchers(self_, value): self_.self_or_cls._param__private.parameters_state['watchers'] = value @property def watchers(self_): """Dictionary of instance watchers.""" if self_.self is None: raise TypeError('Accessing `.param.watchers` is only supported on a Parameterized instance, not class.') return self_.self._param__private.watchers @watchers.setter def watchers(self_, value): if self_.self is None: raise TypeError('Setting `.param.watchers` is only supported on a Parameterized instance, not class.') self_.self._param__private.watchers = value @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_, cls = state.get('self'), state.get('cls') self_or_cls = self_ if self_ is not None else cls for k in self_or_cls._param__private.parameters_state: key = '_'+k if key in state: self_or_cls._param__private.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 if inst is None: return self_._cls_parameters[key] p = self_.objects(instance=False)[key] return _instantiated_parameter(inst, p) def __dir__(self_): """ Adds parameters to dir """ return super().__dir__() + list(self_._cls_parameters) def __iter__(self_): """ Iterates over the parameters on this object. """ yield from self_._cls_parameters def __contains__(self_, param): return param in self_._cls_parameters def __getattr__(self_, attr): """ Extends attribute access to parameter objects. """ cls = self_.__dict__.get('cls') if cls is None: # Class not initialized raise AttributeError if attr in self_._cls_parameters: return self_.__getitem__(attr) elif self_.self is None: raise AttributeError(f"type object '{self_.cls.__name__}.param' has no attribute {attr!r}") else: raise AttributeError(f"'{self_.cls.__name__}.param' object has no attribute {attr!r}") @as_uninitialized def _set_name(self_, name): self_.self.name = name @as_uninitialized def _generate_name(self_): self_._set_name('%s%05d' % (self_.cls.__name__, object_count)) @as_uninitialized def _setup_params(self_, **params): """ Initialize default and keyword parameter values. First, ensures that values for all Parameters with 'instantiate=True' (typically used for mutable Parameters) are copied directly into each object, to ensure that there is an independent copy of the value (to avoid surprising aliasing errors). Second, ensures that Parameters with 'constant=True' are referenced on the instance, to make sure that setting a constant Parameter on the class doesn't affect already created instances. Then sets each of the keyword arguments, raising when any of them are not defined as parameters. """ self = self_.self ## Deepcopy all 'instantiate=True' parameters params_to_deepcopy = {} params_to_ref = {} objects = self_._cls_parameters for pname, p in objects.items(): if p.instantiate and pname != "name": params_to_deepcopy[pname] = p elif p.constant and pname != 'name': params_to_ref[pname] = p for p in params_to_deepcopy.values(): self_._instantiate_param(p) for p in params_to_ref.values(): self_._instantiate_param(p, deepcopy=False) ## keyword arg setting deps, refs = {}, {} for name, val in params.items(): desc = self_.cls.get_param_descriptor(name)[0] # pylint: disable-msg=E1101 if not desc: raise TypeError( f"{self.__class__.__name__}.__init__() got an unexpected " f"keyword argument {name!r}" ) pobj = objects.get(name) if pobj is None or not pobj.allow_refs: # Until Parameter.allow_refs=True by default we have to # speculatively evaluate a values to check whether they # contain a reference and warn the user that the # behavior may change in future. if name not in self_.cls._param__private.explicit_no_refs: try: ref, _, resolved, _ = self_._resolve_ref(pobj, val) except Exception: ref = None if ref: warnings.warn( f"Parameter {name!r} on {pobj.owner} is being given a valid parameter " f"reference {val} but is implicitly allow_refs=False. " "In future allow_refs will be enabled by default and " f"the reference {val} will be resolved to its underlying " f"value {resolved}. Please explicitly set allow_ref on the " "Parameter definition to declare whether references " "should be resolved or not.", category=_ParamFutureWarning, stacklevel=4, ) setattr(self, name, val) continue # Resolve references ref, ref_deps, resolved, is_async = self_._resolve_ref(pobj, val) if ref is not None: refs[name] = ref deps[name] = ref_deps if not is_async and not (resolved is Undefined or resolved is Skip): setattr(self, name, resolved) return refs, deps def _setup_refs(self_, refs): groups = defaultdict(list) for pname, subrefs in refs.items(): for p in subrefs: if isinstance(p, Parameter): groups[p.owner].append((pname, p.name)) else: for sp in extract_dependencies(p): groups[sp.owner].append((pname, sp.name)) for owner, pnames in groups.items(): refnames, pnames = zip(*pnames) self_.self._param__private.ref_watchers.append(( refnames, owner.param._watch(self_._sync_refs, list(set(pnames)), precedence=-1) )) def _update_ref(self_, name, ref): param_private = self_.self._param__private if name in param_private.async_refs: param_private.async_refs.pop(name).cancel() for _, watcher in param_private.ref_watchers: dep_obj = watcher.cls if watcher.inst is None else watcher.inst dep_obj.param.unwatch(watcher) self_.self._param__private.ref_watchers = [] refs = dict(self_.self._param__private.refs, **{name: ref}) deps = {name: resolve_ref(ref) for name, ref in refs.items()} self_._setup_refs(deps) self_.self._param__private.refs = refs def _sync_refs(self_, *events): updates = {} for pname, ref in self_.self._param__private.refs.items(): # Skip updating value if dependency has not changed recursive = self_[pname].nested_refs deps = resolve_ref(ref, recursive) is_gen = inspect.isgeneratorfunction(ref) is_async = iscoroutinefunction(ref) or is_gen if not any((dep.owner is e.obj and dep.name == e.name) for dep in deps for e in events) and not is_async: continue try: new_val = resolve_value(ref, recursive) except Skip: new_val = Undefined if new_val is Skip or new_val is Undefined: continue elif is_async: async_executor(partial(self_._async_ref, pname, new_val)) continue updates[pname] = new_val with edit_constant(self_.self): with _syncing(self_.self, updates): self_.update(updates) def _resolve_ref(self_, pobj, value): is_gen = inspect.isgeneratorfunction(value) is_async = iscoroutinefunction(value) or is_gen deps = resolve_ref(value, recursive=pobj.nested_refs) if not (deps or is_async or is_gen): return None, None, value, False ref = value try: value = resolve_value(value, recursive=pobj.nested_refs) except Skip: value = Undefined if is_async: async_executor(partial(self_._async_ref, pobj.name, value)) value = None return ref, deps, value, is_async async def _async_ref(self_, pname, awaitable): if not self_.self._param__private.initialized: async_executor(partial(self_._async_ref, pname, awaitable)) return current_task = asyncio.current_task() running_task = self_.self._param__private.async_refs.get(pname) if running_task is None: self_.self._param__private.async_refs[pname] = current_task elif current_task is not running_task: self_.self._param__private.async_refs[pname].cancel() try: if isinstance(awaitable, types.AsyncGeneratorType): async for new_obj in awaitable: with _syncing(self_.self, (pname,)): self_.update({pname: new_obj}) else: with _syncing(self_.self, (pname,)): try: self_.update({pname: await awaitable}) except Skip: pass finally: # Ensure we clean up but only if the task matches the currrent task if self_.self._param__private.async_refs.get(pname) is current_task: del self_.self._param__private.async_refs[pname] @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=True): # deepcopy or store a reference to reference param_obj.default into # self._param__private.values (or dict_ if supplied) under the # parameter's name (or key if supplied) instantiator = copy.deepcopy if deepcopy else lambda o: o self = self_.self dict_ = dict_ or self._param__private.values key = key or param_obj.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 = instantiator(param_obj.default) shared_parameters._shared_cache[param_key] = new_object else: new_object = instantiator(param_obj.default) dict_[key] = new_object if isinstance(new_object, Parameterized) and deepcopy: 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._param__private.dynamic_watchers.pop(method, []): (w.cls if w.inst is None else w.inst).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._param__private.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.cls if param_dep.inst is None else param_dep.inst 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.cls if param_dep.inst is None else param_dep.inst 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) @_recursive_repr() def _repr_html_(self_, open=True): return _parameterized_repr_html(self_.self_or_cls, open) # Classmethods # PARAM3_DEPRECATION @_deprecated(extra_msg="""Use instead `for k,v in p.param.objects().items(): print(f"{p.__class__.name}.{k}={repr(v.default)}")`""") def print_param_defaults(self_): """Print the default values of all cls's Parameters. .. deprecated:: 1.12.0 Use instead `for k,v in p.param.objects().items(): print(f"{p.__class__.name}.{k}={repr(v.default)}")` """ cls = self_.cls for key,val in cls.__dict__.items(): if isinstance(val,Parameter): print(cls.__name__+'.'+key+ '='+ repr(val.default)) # PARAM3_DEPRECATION @_deprecated(extra_msg="Use instead `p.param.default =`") def set_default(self_,param_name,value): """ Set the default value of param_name. Equivalent to setting param_name on the class. .. deprecated:: 1.12.0 Use instead `p.param.default =` """ 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() cls._param__private.params.clear() # PARAM3_DEPRECATION @_deprecated(extra_msg="Use instead `.param.add_parameter`") def _add_parameter(self_,param_name, param_obj): """Add a new Parameter object into this object's class. .. deprecated :: 1.12.0 """ return self_.add_parameter(param_name, param_obj) # PARAM3_DEPRECATION @_deprecated(extra_msg="Use instead `.param.values()` or `.param['param']`") 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. .. deprecated:: 1.12.0 Use instead `.param.values()` or `.param['param']` """ pdict = self_.objects(instance='existing') if parameter_name is None: return pdict else: return pdict[parameter_name] # Bothmethods def update(self_, arg=Undefined, /, **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. May also be used as a context manager to temporarily set and then reset parameter values. """ refs = {} if self_.self is not None: private = self_.self._param__private params = list(kwargs if arg is Undefined else dict(arg, **kwargs)) for pname in params: if pname in refs: continue elif pname in private.refs: refs[pname] = private.refs[pname] elif pname in private.async_refs: refs[pname] = private.async_refs[pname] restore = dict(self_._update(arg, **kwargs)) return _ParametersRestorer(parameters=self_, restore=restore, refs=refs) def _update(self_, arg=Undefined, /, **kwargs): BATCH_WATCH = self_._BATCH_WATCH self_._BATCH_WATCH = True self_or_cls = self_.self_or_cls if arg is not Undefined: kwargs = dict(arg, **kwargs) trigger_params = [ k for k in kwargs if k in self_ and hasattr(self_[k], '_autotrigger_value') ] for tp in trigger_params: self_[tp]._mode = 'set' values = self_.values() restore = {k: values[k] for k, v in kwargs.items() if k in values} for (k, v) in kwargs.items(): if k not in self_: self_._BATCH_WATCH = False raise ValueError(f"{k!r} is not a parameter of {self_.cls.__name__}") try: setattr(self_or_cls, k, v) except: self_._BATCH_WATCH = False raise self_._BATCH_WATCH = BATCH_WATCH if not BATCH_WATCH: self_._batch_call_watchers() for tp in trigger_params: p = self_[tp] p._mode = 'reset' setattr(self_or_cls, tp, p._autotrigger_reset_value) p._mode = 'set-reset' return restore # PARAM3_DEPRECATION @_deprecated(extra_msg="Use instead `.param.update`") 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. .. deprecated:: 1.12.0 Use instead `.param.update` """ 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) @property def _cls_parameters(self_): """ Class parameters are cached because they are accessed often, and parameters are rarely added (and cannot be deleted) """ cls = self_.cls pdict = cls._param__private.params if pdict: return pdict 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). # cls._param__private.params[f'_{cls.__name__}__params'] = paramdict cls._param__private.params = paramdict return paramdict 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'. """ if self_.self is not None and not self_.self._param__private.initialized and instance is True: warnings.warn( 'Looking up instance Parameter objects (`.param.objects()`) until ' 'the Parameterized instance has been fully initialized is deprecated and will raise an error in a future version. ' 'Ensure you have called `super().__init__(**params)` in your Parameterized ' 'constructor before trying to access instance Parameter objects, or ' 'looking up the class Parameter objects with `.param.objects(instance=False)` ' 'may be enough for your use case.', category=_ParamFutureWarning, stacklevel=2, ) pdict = self_._cls_parameters if instance and self_.self is not None: if instance == 'existing': if getattr(self_.self._param__private, 'initialized', False) and self_.self._param__private.params: return dict(pdict, **self_.self._param__private.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. """ if self_.self is not None and not self_.self._param__private.initialized: warnings.warn( 'Triggering watchers on a partially initialized Parameterized instance ' 'is deprecated and will raise an error in a future version. ' 'Ensure you have called super().__init__(**params) in ' 'the Parameterized instance constructor before trying to set up a watcher.', category=_ParamFutureWarning, stacklevel=2, ) trigger_params = [p for p in self_ if hasattr(self_[p], '_autotrigger_value')] triggers = {p:self_[p]._autotrigger_value for p in trigger_params if p in param_names} events = self_._events watchers = self_._state_watchers self_._events = [] self_._state_watchers = [] param_values = self_.values() params = {name: param_values[name] for name in param_names} self_._TRIGGER = True self_.update(dict(params, **triggers)) self_._TRIGGER = False self_._events += events self_._state_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: try: watcher.fn(*args, **kwargs) except Skip: pass def _call_watcher(self_, watcher, event): """ Invoke the given watcher appropriately given an Event object. """ if self_._TRIGGER: pass elif watcher.onlychanged and (not self_._changed(event)): return if self_._BATCH_WATCH: self_._events.append(event) if not any(watcher is w for w in self_._state_watchers): self_._state_watchers.append(watcher) else: event = self_._update_event_type(watcher, event, self_._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_._events: event_dict = OrderedDict([((event.name, event.what), event) for event in self_._events]) watchers = self_._state_watchers[:] self_._events = [] self_._state_watchers = [] for watcher in sorted(watchers, key=lambda w: w.precedence): events = [self_._update_event_type(watcher, event_dict[(name, watcher.what)], self_._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(f'Mode {mode!r} not in available serialization formats {list(Parameter._serializers.keys())!r}') 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(f'Mode {mode!r} not in available serialization formats {list(Parameter._serializers.keys())!r}') 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(f'Mode {mode!r} not in available serialization formats {list(Parameter._serializers.keys())!r}') 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(f'Mode {mode!r} not in available serialization formats {list(Parameter._serializers.keys())!r}') serializer = Parameter._serializers[mode] return serializer.schema(self_or_cls, safe=safe, subset=subset) # PARAM3_DEPRECATION # same as values() but returns list, not dict @_deprecated(extra_msg=""" Use `.param.values().items()` instead (or `.param.values()` for the common case of `dict(....param.get_param_values())`) """) def get_param_values(self_, onlychanged=False): """ 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). .. deprecated:: 1.12.0 Use `.param.values().items()` instead (or `.param.values()` for the common case of `dict(....param.get_param_values())`) """ vals = self_.values(onlychanged) return [(k, v) for k, v in vals.items()] 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). """ 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 name == 'name' and onlychanged and _is_auto_name(self_.cls.__name__, value): continue if not onlychanged or not Comparator.is_equal(value, val.default): vals.append((name, value)) vals.sort(key=itemgetter(0)) return dict(vals) 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: # TODO: is this always an instance? if isinstance(cls_or_slf, Parameterized) and name in cls_or_slf._param__private.values: # dealing with object and it's been set on this object value = cls_or_slf._param__private.values[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) # PARAM3_DEPRECATION @_deprecated(extra_msg='Use instead `.param.method_dependencies`') def params_depended_on(self_, *args, **kwargs): """ 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. .. deprecated: 2.0.0 Use instead `.param.method_dependencies` """ return self_.method_dependencies(*args, **kwargs) 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): if name == '_param_watchers': continue 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: if not hasattr(self_.self_or_cls, obj.split('.')[1]): raise AttributeError( f'Dependency {obj[1:]!r} could not be resolved, {self_.self_or_cls} ' f'has no parameter or attribute {obj.split(".")[1]!r}. Ensure ' 'the object being depended on is declared before calling the ' 'Parameterized constructor.' ) 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): attr_obj = getattr(src, attr) if isinstance(attr_obj, Parameterized): return [], [] elif isinstance(attr_obj, (FunctionType, MethodType)): info = MInfo(inst=inst, cls=cls, name=attr, method=attr_obj) else: raise AttributeError(f"Attribute {attr!r} could not be resolved on {src}.") elif getattr(src, "abstract", None): return [], [] if intermediate == 'only' else [DInfo(spec=spec)] else: raise AttributeError(f"Attribute {attr!r} could not be resolved on {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'): if self_.self is not None and not self_.self._param__private.initialized: warnings.warn( '(Un)registering a watcher on a partially initialized Parameterized instance ' 'is deprecated and will raise an error in a future version. Ensure ' 'you have called super().__init__(**) in the Parameterized instance ' 'constructor before trying to set up a watcher.', category=_ParamFutureWarning, stacklevel=4, ) parameter_names = watcher.parameter_names for parameter_name in parameter_names: if parameter_name not in self_.cls.param: raise ValueError("{} parameter was not found in list of " "parameters of class {}".format(parameter_name, self_.cls.__name__)) if self_.self is not None and what == "value": watchers = self_.self._param__private.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(f'No such watcher {str(watcher)} to remove.') 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 # PARAM3_DEPRECATION @_deprecated(extra_msg="Use instead `{k:v.default for k,v in p.param.objects().items()}`") 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. .. deprecated:: 1.12.0 Use instead `{k:v.default for k,v in p.param.objects().items()}` """ 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) # PARAM3_DEPRECATION @_deprecated(extra_msg="""Use instead `for k,v in p.param.objects().items(): print(f"{p.__class__.name}.{k}={repr(v.default)}")`""") def print_param_values(self_): """Print the values of all this object's Parameters. .. deprecated:: 1.12.0 Use instead `for k,v in p.param.objects().items(): print(f"{p.__class__.name}.{k}={repr(v.default)}")` """ self = self_.self for name, val in self.param.values().items(): print(f'{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) # PARAM3_DEPRECATION @_deprecated(extra_msg="Use instead `.param.log(param.MESSAGE, ...)`") def message(self_,msg,*args,**kw): """ Print msg merged with args as a message. See Python's logging module for details of message formatting. .. deprecated:: 1.12.0 Use instead `.param.log(param.MESSAGE, ...)` """ self_.__db_print(INFO,msg,*args,**kw) # PARAM3_DEPRECATION @_deprecated(extra_msg="Use instead `.param.log(param.VERBOSE, ...)`") 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. .. deprecated:: 1.12.0 Use instead `.param.log(param.VERBOSE, ...)` """ self_.__db_print(VERBOSE,msg,*args,**kw) # PARAM3_DEPRECATION @_deprecated(extra_msg="Use instead `.param.log(param.DEBUG, ...)`") 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. .. deprecated:: 1.12.0 Use instead `.param.log(param.DEBUG, ...)` """ 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) # 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. """ self = self_.self_or_cls if not isinstance(self, Parameterized): raise NotImplementedError('_state_push is not implemented at the class level') 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. """ self = self_.self_or_cls if not isinstance(self, Parameterized): raise NotImplementedError('_state_pop is not implemented at the class level') 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() 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. """ self = self_.self_or_cls if not isinstance(self, Parameterized): raise NotImplementedError('pprint is not implemented at the class level') # Wrapping the staticmethod _pprint with partial to pass `self` as the `_recursive_repr` # decorator expects `self`` to be the pprinted object (not `self_`). return partial(self_._pprint, self, imports=imports, prefix=prefix, unknown_value=unknown_value, qualify=qualify, separator=separator)() @staticmethod @_recursive_repr() def _pprint(self, imports=None, prefix=" ", unknown_value='', qualify=False, separator=""): 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(type(self).__init__) if 'self' not in spec.args or spec.args[0] != 'self': raise KeyError(f"'{type(self).__name__}.__init__.__signature__' must contain 'self' as its first Parameter.") args = spec.args[1:] 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(f"{self.name}: unknown value of {k!r}") 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(f'{k}={value}') processed.append(k) qualifier = mod + '.' if qualify else '' arguments = arglist + keywords + (['**%s' % spec.varargs] if spec.varargs else []) return qualifier + '{}({})'.format(self.__class__.__name__, (','+separator+prefix).join(arguments)) 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_) # Compute which parameters explicitly do not support references # This can be removed when Parameter.allow_refs=True by default. explicit_no_refs = set() for base in bases: if issubclass(base, Parameterized): explicit_no_refs |= set(base._param__private.explicit_no_refs) _param__private = _ClassPrivate(explicit_no_refs=list(explicit_no_refs)) mcs._param__private = _param__private mcs.__set_name(name, dict_) mcs._param__parameters = 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)] 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) minfo = MInfo(cls=mcs, inst=None, name=name, method=method) deps, dynamic_deps = _params_depended_on(minfo, dynamic=False) if watch: _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__parameters'): 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() def __set_name(mcs, name, dict_): """ Give Parameterized classes a useful 'name' attribute that is by default the class name, unless a class in the hierarchy has defined a `name` String Parameter with a defined `default` value, in which case that value is used to set the class name. """ name_param = dict_.get("name", None) if name_param is not None: if not type(name_param) is String: raise TypeError( f"Parameterized class {name!r} cannot override " f"the 'name' Parameter with type {type(name_param)}. " "Overriding 'name' is only allowed with a 'String' Parameter." ) if name_param.default: mcs.name = name_param.default mcs._param__private.renamed = True else: mcs.name = name else: classes = classlist(mcs)[::-1] found_renamed = False for c in classes: if hasattr(c, '_param__private') and c._param__private.renamed: found_renamed = True break if not found_renamed: mcs.name = name def __class_docstring(mcs): """ Customize the class docstring with a Parameter table if `docstring_describe_params` and the `param_pager` is available. """ if not docstring_describe_params or not param_pager: return class_docstr = mcs.__doc__ if mcs.__doc__ else '' description = param_pager(mcs) mcs.__doc__ = 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 def __get_signature(mcs): """ For classes with a constructor signature that matches the default Parameterized.__init__ signature (i.e. ``__init__(self, **params)``) this method will generate a new signature that expands the parameters. If the signature differs from the default the custom signature is returned. """ if mcs._param__private.signature: return mcs._param__private.signature # allowed_signature must be the signature of Parameterized.__init__ # Inspecting `mcs.__init__` instead of `mcs` to avoid a recursion error if inspect.signature(mcs.__init__) != DEFAULT_SIGNATURE: return None 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 and not v.readonly: keyword_group.append(k) processed_kws.add(k) keyword_groups.append(keyword_group) keywords = [el for grp in reversed(keyword_groups) for el in grp] mcs._param__private.signature = signature = inspect.Signature([ inspect.Parameter(k, inspect.Parameter.KEYWORD_ONLY) for k in keywords ]) return signature __signature__ = property(__get_signature) abstract = property(__is_abstract) def _get_param(mcs): return mcs._param__parameters 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) 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 `Undefined`. In such a case, i.e. when no non-`Undefined` 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) p_type = type(param) slots = dict.fromkeys(p_type._all_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'] supers = classlist(mcs)[::-1] # Explicitly inherit instantiate from super class and # check if type has changed to a more specific or different # Parameter type, requiring extra validation type_change = False for superclass in supers: super_param = superclass.__dict__.get(param_name) if not isinstance(super_param, Parameter): continue if super_param.instantiate is True: param.instantiate = True super_type = type(super_param) if not issubclass(super_type, p_type): type_change = True del slots['instantiate'] callables, slot_values = {}, {} slot_overridden = False for slot in slots.keys(): # Search up the hierarchy until param.slot (which has to # be obtained using getattr(param,slot)) is not Undefined, # is a new value (using identity) or we run out of classes # to search. for scls in supers: # Class may not define parameter or slot might not be # there because could be a more general type of Parameter new_param = scls.__dict__.get(param_name) if new_param is None or not hasattr(new_param, slot): continue new_value = getattr(new_param, slot) old_value = slot_values.get(slot, Undefined) if new_value is Undefined: continue elif new_value is old_value: continue elif old_value is Undefined: slot_values[slot] = new_value # If we already know we have to re-validate abort # early to avoid costly lookups if slot_overridden or type_change: break else: if slot not in param._non_validated_slots: slot_overridden = True break if slot_values.get(slot, Undefined) is Undefined: try: default_val = param._slot_defaults[slot] except KeyError as e: raise KeyError( f'Slot {slot!r} of parameter {param_name!r} has no ' 'default value defined in `_slot_defaults`' ) from e if callable(default_val): callables[slot] = default_val else: slot_values[slot] = default_val elif slot == 'allow_refs': # Track Parameters that explicitly declared no refs explicit_no_refs = mcs._param__private.explicit_no_refs if param.allow_refs is False: explicit_no_refs.append(param.name) elif param.allow_refs is True and param.name in explicit_no_refs: explicit_no_refs.remove(param.name) # Now set the actual slot values for slot, value in slot_values.items(): setattr(param, slot, value) # Avoid crosstalk between mutable slot values in different Parameter objects if slot != "default": v = getattr(param, slot) if _is_mutable_container(v): setattr(param, slot, copy.copy(v)) # Once all the static slots have been filled in, fill in the dynamic ones # (which are only allowed to use static values or results are undefined) for slot, fn in callables.items(): setattr(param, slot, fn(param)) # Once all the slot values have been set, call _update_state for Parameters # that need updates to make sure they're set up correctly after inheritance. param._update_state() # If the type has changed to a more specific or different type # or a slot value has been changed validate the default again. # Hack: Had to disable re-validation of None values because the # automatic appending of an unknown value on Selector opens a whole # rabbit hole in regard to the validation. if type_change or slot_overridden and param.default is not None: try: param._validate(param.default) # Param has no base validation exception class. Param Parameters raise # ValueError, TypeError, OSError exceptions but external Parameters # might raise other types of error, so we catch them all. except Exception as e: msg = f'{_validate_error_prefix(param)} failed to validate its ' \ 'default value on class creation, this is going to raise ' \ 'an error in the future. ' parents = ', '.join(klass.__name__ for klass in mcs.__mro__[1:-2]) if not type_change and slot_overridden: msg += ( f'The Parameter is defined with attributes which when ' 'combined with attributes inherited from its parent ' f'classes ({parents}) make it invalid. ' 'Please fix the Parameter attributes.' ) elif type_change and not slot_overridden: msg += ( f'The Parameter type changed between class {mcs.__name__!r} ' f'and one of its parent classes ({parents}) which ' f'made it invalid. Please fix the Parameter type.' ) else: # type_change and slot_overriden is not possible as when # the type changes checking the slots is aborted for # performance reasons. pass msg += f'\nValidation failed with:\n{e}' warnings.warn( msg, category=_ParamFutureWarning, stacklevel=4, ) 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 isinstance(val, Parameterized) or (type(val) is type and issubclass(val, Parameterized)): rep=val.param.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 def truncate(str_, maxlen = 30): """Return HTML-safe truncated version of given string""" rep = (str_[:(maxlen-2)] + '..') if (len(str_) > (maxlen-2)) else str_ return html.escape(rep) def _get_param_repr(key, val, p, vallen=30, doclen=40): """HTML representation for a single Parameter object and its value""" if isinstance(val, Parameterized) or (type(val) is type and issubclass(val, Parameterized)): value = val.param._repr_html_(open=False) elif hasattr(val, "_repr_html_"): value = val._repr_html_() else: value = truncate(repr(val), vallen) if hasattr(p, 'bounds'): if p.bounds is None: range_ = '' elif hasattr(p,'inclusive_bounds'): # Numeric bounds use ( and [ to indicate exclusive and inclusive bl,bu = p.bounds il,iu = p.inclusive_bounds lb = '' if bl is None else ('>=' if il else '>') + str(bl) ub = '' if bu is None else ('<=' if iu else '<') + str(bu) range_ = lb + (', ' if lb and bu else '') + ub else: range_ = repr(p.bounds) elif hasattr(p, 'objects') and p.objects: range_ = ', '.join(list(map(repr, p.objects))) elif hasattr(p, 'class_'): if isinstance(p.class_, tuple): range_ = ' | '.join(kls.__name__ for kls in p.class_) else: range_ = p.class_.__name__ elif hasattr(p, 'regex') and p.regex is not None: range_ = f'regex({p.regex})' else: range_ = '' if p.readonly: range_ = ' '.join(s for s in ['read-only', range_] if s) elif p.constant: range_ = ' '.join(s for s in ['constant', range_] if s) if getattr(p, 'allow_None', False): range_ = ' '.join(s for s in ['nullable', range_] if s) tooltip = f' class="param-doc-tooltip" data-tooltip="{escape(p.doc.strip())}"' if p.doc else '' return ( f'' f'

{key}

' f' {value}' f' {p.__class__.__name__}' f' {range_}' f'\n' ) def _parameterized_repr_html(p, open): """HTML representation for a Parameterized object""" if isinstance(p, Parameterized): cls = p.__class__ title = cls.name + "()" value_field = 'Value' else: cls = p title = cls.name value_field = 'Default' tooltip_css = """ .param-doc-tooltip{ position: relative; cursor: help; } .param-doc-tooltip:hover:after{ content: attr(data-tooltip); background-color: black; color: #fff; border-radius: 3px; padding: 10px; position: absolute; z-index: 1; top: -5px; left: 100%; margin-left: 10px; min-width: 250px; } .param-doc-tooltip:hover:before { content: ""; position: absolute; top: 50%; left: 100%; margin-top: -5px; border-width: 5px; border-style: solid; border-color: transparent black transparent transparent; } """ openstr = " open" if open else "" param_values = p.param.values().items() contents = "".join(_get_param_repr(key, val, p.param[key]) for key, val in param_values) return ( f'\n' f'
\n' ' \n' f' {title}\n' ' \n' '
\n' ' \n' f' \n' f'{contents}\n' '
Name{value_field}TypeRange
\n
\n
\n' ) # _ClassPrivate and _InstancePrivate are the private namespaces of Parameterized # classes and instance respectively, stored on the `_param__private` attribute. # They are implemented with slots for performance reasons. class _ClassPrivate: """ parameters_state: dict Dict holding some transient states disable_instance_params: bool Whether to disable instance parameters renamed: bool Whethe the class has been renamed by a super class params: dict Dict of parameter_name:parameter """ __slots__ = [ 'parameters_state', 'disable_instance_params', 'renamed', 'params', 'initialized', 'signature', 'explicit_no_refs', ] def __init__( self, parameters_state=None, disable_instance_params=False, explicit_no_refs=None, renamed=False, params=None, ): if parameters_state is None: 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.parameters_state = parameters_state self.disable_instance_params = disable_instance_params self.renamed = renamed self.params = {} if params is None else params self.initialized = False self.signature = None self.explicit_no_refs = [] if explicit_no_refs is None else explicit_no_refs def __getstate__(self): return {slot: getattr(self, slot) for slot in self.__slots__} def __setstate__(self, state): for k, v in state.items(): setattr(self, k, v) class _InstancePrivate: """ initialized: bool Flag that can be tested to see if e.g. constant Parameters can still be set parameters_state: dict Dict holding some transient states dynamic_watchers: defaultdict Dynamic watchers ref_watchers: list[Watcher] Watchers used for internal references params: dict Dict of parameter_name:parameter refs: dict Dict of parameter name:reference watchers: dict Dict of dict: parameter_name: parameter_attribute (e.g. 'value'): list of `Watcher`s values: dict Dict of parameter name: value """ __slots__ = [ 'initialized', 'parameters_state', 'dynamic_watchers', 'params', 'async_refs', 'refs', 'ref_watchers', 'syncing', 'watchers', 'values', 'explicit_no_refs', ] def __init__( self, initialized=False, parameters_state=None, dynamic_watchers=None, refs=None, params=None, watchers=None, values=None, explicit_no_refs=None ): self.initialized = initialized self.explicit_no_refs = [] if explicit_no_refs is None else explicit_no_refs self.syncing = set() if parameters_state is None: 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.ref_watchers = [] self.async_refs = {} self.parameters_state = parameters_state self.dynamic_watchers = defaultdict(list) if dynamic_watchers is None else dynamic_watchers self.params = {} if params is None else params self.refs = {} if refs is None else refs self.watchers = {} if watchers is None else watchers self.values = {} if values is None else values def __getstate__(self): return {slot: getattr(self, slot) for slot in self.__slots__} def __setstate__(self, state): for k, v in state.items(): setattr(self, k, v) class Parameterized(metaclass=ParameterizedMetaclass): """ 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 # Setting a Parameter value in an __init__ block before calling # Parameterized.__init__ (via super() generally) already sets the # _InstancePrivate namespace over the _ClassPrivate namespace # (see Parameter.__set__) so we shouldn't override it here. if not isinstance(self._param__private, _InstancePrivate): self._param__private = _InstancePrivate( explicit_no_refs=type(self)._param__private.explicit_no_refs ) # Skip generating a custom instance name when a class in the hierarchy # has overriden the default of the `name` Parameter. if self.param.name.default == self.__class__.__name__: self.param._generate_name() refs, deps = self.param._setup_params(**params) object_count += 1 self._param__private.initialized = True self.param._setup_refs(deps) self.param._update_deps(init=True) self._param__private.refs = refs @property def param(self): return Parameters(self.__class__, self=self) #PARAM3_DEPRECATION @property @_deprecated(extra_msg="Use `inst.param.watchers` instead.", warning_cat=_ParamFutureWarning) def _param_watchers(self): return self._param__private.watchers #PARAM3_DEPRECATION @_param_watchers.setter @_deprecated(extra_msg="Use `inst.param.watchers = ...` instead.", warning_cat=_ParamFutureWarning) def _param_watchers(self, value): self._param__private.watchers = value # '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. """ explicit_no_refs = type(self)._param__private.explicit_no_refs self._param__private = _InstancePrivate(explicit_no_refs=explicit_no_refs) self._param__private.initialized = False _param__private = state.get('_param__private', None) if _param__private is None: _param__private = _InstancePrivate(explicit_no_refs=explicit_no_refs) # When making a copy the internal watchers have to be # recreated and point to the new instance if _param__private.watchers: param_watchers = _param__private.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 state.pop('param', None) for name,value in state.items(): setattr(self,name,value) self._param__private.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 = [f'{name}={val!r}' for name, val in self.param.values().items()] 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 f"<{self.__class__.__name__} {self.name}>" 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 {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) def _pprint(self, imports=None, prefix="\n ",unknown_value='', qualify=False, separator=""): """ Same as self.param.pprint, except that X.classname(Y is replaced with X.classname.instance(Y """ r = self.param.pprint(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 # PARAM3_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: """ The same as Python's "property" attribute, but allows the accessor methods to be overridden in subclasses. .. deprecated:: 2.0.0 """ # 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): warnings.warn( message="overridable_property has been deprecated.", category=_ParamDeprecationWarning, stacklevel=2, ) 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-2.1.1/param/parameters.py000066400000000000000000003166041463636336300164570ustar00rootroot00000000000000""" 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 collections import copy import datetime as dt import glob import inspect import numbers import os.path import pathlib import re import sys import typing import warnings from collections import OrderedDict from contextlib import contextmanager from .parameterized import ( Parameterized, Parameter, ParameterizedFunction, ParamOverrides, String, Undefined, get_logger, instance_descriptor, dt_types, _int_types, _identity_hook ) from ._utils import ( ParamDeprecationWarning as _ParamDeprecationWarning, _deprecate_positional_args, _deprecated, _dict_update, _validate_error_prefix, _deserialize_from_path, _named_objs, _produce_value, _get_min_max_value, _is_number, concrete_descendents, _abbreviate_paths, _to_datetime, ) #----------------------------------------------------------------------------- # Utilities #----------------------------------------------------------------------------- def param_union(*parameterizeds, warn=True): """ Given a set of Parameterized objects, returns a dictionary with the union of all param name,value pairs across them. Parameters ---------- warn : bool, optional Wether to warn if the same parameter have been given multiple values, otherwise use the last value, by default True Returns ------- dict Union of all param name,value pairs """ d = {} for o in parameterizeds: for k in o.param: if k != 'name': if k in d and warn: get_logger().warning(f"overwriting parameter {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_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 Infinity: """ 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().__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 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 #----------------------------------------------------------------------------- # Parameters #----------------------------------------------------------------------------- #----------------------------------------------------------------------------- # Dynamic/Number #----------------------------------------------------------------------------- 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. 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 @typing.overload def __init__( self, default=None, *, doc=None, label=None, precedence=None, instantiate=False, constant=False, readonly=False, pickle_default_value=True, allow_None=False, per_instance=True, allow_refs=False, nested_refs=False ): ... def __init__(self, default=Undefined, **params): """ Call the superclass's __init__ and set instantiate=True if the default is dynamic. """ super().__init__(default=default, **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().__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().__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().__get__(obj,objtype),'_Dynamic_last') def _inspect(self,obj,objtype=None): """Return the last generated value for this parameter.""" gen=super().__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().__get__(obj,objtype) if hasattr(gen,'_Dynamic_last'): return self._produce_value(gen,force=True) else: return gen class __compute_set_hook: """Remove when set_hook is removed""" def __call__(self, p): return _identity_hook def __repr__(self): return repr(self.sig) @property def sig(self): return None _compute_set_hook = __compute_set_hook() 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'] _slot_defaults = _dict_update( Dynamic._slot_defaults, default=0.0, bounds=None, softbounds=None, inclusive_bounds=(True,True), step=None, set_hook=_compute_set_hook, ) @typing.overload def __init__( self, default=0.0, *, bounds=None, softbounds=None, inclusive_bounds=(True,True), step=None, set_hook=None, allow_None=False, doc=None, label=None, precedence=None, instantiate=False, constant=False, readonly=False, pickle_default_value=True, per_instance=True, allow_refs=False, nested_refs=False ): ... @_deprecate_positional_args def __init__(self, default=Undefined, *, bounds=Undefined, softbounds=Undefined, inclusive_bounds=Undefined, step=Undefined, set_hook=Undefined, **params): """ Initialize this parameter object and store the bounds. Non-dynamic default values are checked against the bounds. """ super().__init__(default=default, **params) self.set_hook = set_hook self.bounds = bounds self.inclusive_bounds = inclusive_bounds self.softbounds = softbounds self.step = step self._validate(self.default) def __get__(self, obj, objtype): """ Same as the superclass's __get__, but if the value was dynamically generated, check the bounds. """ result = super().__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().__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( f"{_validate_error_prefix(self)} must be at most " f"{vmax}, not {val}." ) else: if not val < vmax: raise ValueError( f"{_validate_error_prefix(self)} must be less than " f"{vmax}, not {val}." ) if vmin is not None: if incmin is True: if not val >= vmin: raise ValueError( f"{_validate_error_prefix(self)} must be at least " f"{vmin}, not {val}." ) else: if not val > vmin: raise ValueError( f"{_validate_error_prefix(self)} must be greater than " f"{vmin}, not {val}." ) def _validate_value(self, val, allow_None): if (allow_None and val is None) or (callable(val) and not inspect.isgeneratorfunction(val)): return if not _is_number(val): raise ValueError( f"{_validate_error_prefix(self)} only takes numeric values, " f"not {type(val)}." ) def _validate_step(self, val, step): if step is not None and not _is_number(step): raise ValueError( f"{_validate_error_prefix(self, 'step')} can only be " f"None or a numeric value, not {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().__setstate__(state) class Integer(Number): """Numeric Parameter required to be an Integer""" _slot_defaults = _dict_update(Number._slot_defaults, default=0) def _validate_value(self, val, allow_None): if callable(val): return if allow_None and val is None: return if not isinstance(val, _int_types): raise ValueError( f"{_validate_error_prefix(self)} must be an integer, " f"not {type(val)}." ) def _validate_step(self, val, step): if step is not None and not isinstance(step, int): raise ValueError( f"{_validate_error_prefix(self, 'step')} can only be " f"None or an integer value, not {type(step)}." ) class Magnitude(Number): """Numeric Parameter required to be in the range [0.0-1.0].""" _slot_defaults = _dict_update(Number._slot_defaults, default=1.0, bounds=(0.0,1.0)) @typing.overload def __init__( self, default=1.0, *, bounds=(0.0, 1.0), softbounds=None, inclusive_bounds=(True,True), step=None, set_hook=None, allow_None=False, doc=None, label=None, precedence=None, instantiate=False, constant=False, readonly=False, pickle_default_value=True, per_instance=True, allow_refs=False, nested_refs=False ): ... def __init__(self, default=Undefined, *, bounds=Undefined, softbounds=Undefined, inclusive_bounds=Undefined, step=Undefined, set_hook=Undefined, **params): super().__init__( default=default, bounds=bounds, softbounds=softbounds, inclusive_bounds=inclusive_bounds, step=step, set_hook=set_hook, **params ) class Date(Number): """ Date parameter of datetime or date type. """ _slot_defaults = _dict_update(Number._slot_defaults, default=None) @typing.overload def __init__( self, default=None, *, bounds=None, softbounds=None, inclusive_bounds=(True,True), step=None, set_hook=None, doc=None, label=None, precedence=None, instantiate=False, constant=False, readonly=False, pickle_default_value=True, allow_None=False, per_instance=True, allow_refs=False, nested_refs=False ): ... def __init__(self, default=Undefined, **kwargs): super().__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( f"{_validate_error_prefix(self)} only takes datetime and " f"date types, not {type(val)}." ) def _validate_step(self, val, step): if step is not None and not isinstance(step, dt_types): raise ValueError( f"{_validate_error_prefix(self, 'step')} can only be None, " f"a datetime or date type, not {type(step)}." ) def _validate_bounds(self, val, bounds, inclusive_bounds): val = _to_datetime(val) bounds = None if bounds is None else map(_to_datetime, bounds) return super()._validate_bounds(val, bounds, inclusive_bounds) @classmethod def serialize(cls, value): if value is None: return None 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' or value is None: return None return dt.datetime.strptime(value, "%Y-%m-%dT%H:%M:%S.%f") class CalendarDate(Number): """ Parameter specifically allowing dates (not datetimes). """ _slot_defaults = _dict_update(Number._slot_defaults, default=None) @typing.overload def __init__( self, default=None, *, bounds=None, softbounds=None, inclusive_bounds=(True,True), step=None, set_hook=None, doc=None, label=None, precedence=None, instantiate=False, constant=False, readonly=False, pickle_default_value=True, allow_None=False, per_instance=True, allow_refs=False, nested_refs=False ): ... def __init__(self, default=Undefined, **kwargs): super().__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( f"{_validate_error_prefix(self)} only takes date types." ) def _validate_step(self, val, step): if step is not None and not isinstance(step, dt.date): raise ValueError( f"{_validate_error_prefix(self, 'step')} can only be None or " f"a date type, not {type(step)}." ) @classmethod def serialize(cls, value): if value is None: return None return value.strftime("%Y-%m-%d") @classmethod def deserialize(cls, value): if value == 'null' or value is None: return None return dt.datetime.strptime(value, "%Y-%m-%d").date() #----------------------------------------------------------------------------- # Boolean #----------------------------------------------------------------------------- class Boolean(Parameter): """Binary or tristate Boolean Parameter.""" _slot_defaults = _dict_update(Parameter._slot_defaults, default=False) @typing.overload def __init__( self, default=False, *, allow_None=False, doc=None, label=None, precedence=None, instantiate=False, constant=False, readonly=False, pickle_default_value=True, per_instance=True, allow_refs=False, nested_refs=False ): ... @_deprecate_positional_args def __init__(self, default=Undefined, **params): super().__init__(default=default, **params) self._validate(self.default) def _validate_value(self, val, allow_None): if allow_None: if not isinstance(val, bool) and val is not None: raise ValueError( f"{_validate_error_prefix(self)} only takes a " f"boolean value or None, not {val!r}." ) elif not isinstance(val, bool): raise ValueError( f"{_validate_error_prefix(self)} must be True or False, " f"not {val!r}." ) def _validate(self, val): self._validate_value(val, self.allow_None) 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'] @typing.overload def __init__( self, default=False, *, allow_None=False, doc=None, label=None, precedence=None, instantiate=False, constant=False, readonly=False, pickle_default_value=True, per_instance=True, allow_refs=False, nested_refs=False ): ... @_deprecate_positional_args def __init__(self,default=False,**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().__init__(default=default,**params) def _reset_event(self, obj, val): val = False if obj is None: self.default = val else: obj._param__private.values[self.name] = val self._post_setter(obj, val) @instance_descriptor def __set__(self, obj, val): if self._mode in ['set-reset', 'set']: super().__set__(obj, val) if self._mode in ['set-reset', 'reset']: self._reset_event(obj, val) #----------------------------------------------------------------------------- # Tuple #----------------------------------------------------------------------------- class __compute_length_of_default: def __call__(self, p): return len(p.default) def __repr__(self): return repr(self.sig) @property def sig(self): return None _compute_length_of_default = __compute_length_of_default() class Tuple(Parameter): """A tuple Parameter (e.g. ('a',7.6,[3,5])) with a fixed tuple length.""" __slots__ = ['length'] _slot_defaults = _dict_update(Parameter._slot_defaults, default=(0,0), length=_compute_length_of_default) @typing.overload def __init__( self, default=(0,0), *, length=None, doc=None, label=None, precedence=None, instantiate=False, constant=False, readonly=False, pickle_default_value=True, allow_None=False, per_instance=True, allow_refs=False, nested_refs=False ): ... @_deprecate_positional_args def __init__(self, default=Undefined, *, length=Undefined, **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().__init__(default=default, **params) if length is Undefined and self.default is None: raise ValueError( f"{_validate_error_prefix(self, 'length')} must be " "specified if no default is supplied." ) elif default is not Undefined and default: self.length = len(default) else: self.length = length self._validate(self.default) def _validate_value(self, val, allow_None): if val is None and allow_None: return if not isinstance(val, tuple): raise ValueError( f"{_validate_error_prefix(self)} only takes a tuple value, " f"not {type(val)}." ) def _validate_length(self, val, length): if val is None and self.allow_None: return if not len(val) == length: raise ValueError( f"{_validate_error_prefix(self, 'length')} is not " f"of the correct length ({len(val)} instead of {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 None return list(value) # As JSON has no tuple representation @classmethod def deserialize(cls, value): if value == 'null' or value is None: 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()._validate_value(val, allow_None) if allow_None and val is None: return for n in val: if _is_number(n): continue raise ValueError( f"{_validate_error_prefix(self)} only takes numeric " f"values, not {type(n)}." ) class XYCoordinates(NumericTuple): """A NumericTuple for an X,Y coordinate.""" _slot_defaults = _dict_update(NumericTuple._slot_defaults, default=(0.0, 0.0)) @typing.overload def __init__( self, default=(0.0, 0.0), *, length=None, allow_None=False, doc=None, label=None, precedence=None, instantiate=False, constant=False, readonly=False, pickle_default_value=True, per_instance=True, allow_refs=False, nested_refs=False ): ... def __init__(self, default=Undefined, **params): super().__init__(default=default, length=2, **params) class Range(NumericTuple): """ A numeric range with optional bounds and softbounds. """ __slots__ = ['bounds', 'inclusive_bounds', 'softbounds', 'step'] _slot_defaults = _dict_update( NumericTuple._slot_defaults, default=None, bounds=None, inclusive_bounds=(True,True), softbounds=None, step=None ) @typing.overload def __init__( self, default=None, *, bounds=None, softbounds=None, inclusive_bounds=(True,True), step=None, length=None, doc=None, label=None, precedence=None, instantiate=False, constant=False, readonly=False, pickle_default_value=True, allow_None=False, per_instance=True, allow_refs=False, nested_refs=False ): ... @_deprecate_positional_args def __init__(self, default=Undefined, *, bounds=Undefined, softbounds=Undefined, inclusive_bounds=Undefined, step=Undefined, **params): self.bounds = bounds self.inclusive_bounds = inclusive_bounds self.softbounds = softbounds self.step = step super().__init__(default=default,length=2,**params) def _validate(self, val): super()._validate(val) self._validate_bounds(val, self.bounds, self.inclusive_bounds, 'bound') self._validate_bounds(val, self.softbounds, self.inclusive_bounds, 'softbound') self._validate_step(val, self.step) self._validate_order(val, self.step, allow_None=self.allow_None) def _validate_step(self, val, step): if step is not None: if not _is_number(step): raise ValueError( f"{_validate_error_prefix(self, 'step')} can only be None " f"or a numeric value, not {type(step)}." ) elif step == 0: raise ValueError( f"{_validate_error_prefix(self, 'step')} cannot be 0." ) def _validate_order(self, val, step, allow_None): if val is None and allow_None: return elif val is not None and (val[0] is None or val[1] is None): return start, end = val if step is not None and step > 0 and not start <= end: raise ValueError( f"{_validate_error_prefix(self)} end {end} is less than its " f"start {start} with positive step {step}." ) elif step is not None and step < 0 and not start >= end: raise ValueError( f"{_validate_error_prefix(self)} start {start} is less than its " f"start {end} with negative step {step}." ) def _validate_bound_type(self, value, position, kind): if not _is_number(value): raise ValueError( f"{_validate_error_prefix(self)} {position} {kind} can only be " f"None or a numerical value, not {type(value)}." ) def _validate_bounds(self, val, bounds, inclusive_bounds, kind): if bounds is not None: for pos, v in zip(['lower', 'upper'], bounds): if v is None: continue self._validate_bound_type(v, pos, kind) if kind == 'softbound': return 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( f"{_validate_error_prefix(self)} {bound} bound must be in " f"range {self.rangestr()}, not {v}." ) 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 f'{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_bound_type(self, value, position, kind): if not isinstance(value, dt_types): raise ValueError( f"{_validate_error_prefix(self)} {position} {kind} can only be " f"None or a date/datetime value, not {type(value)}." ) def _validate_bounds(self, val, bounds, inclusive_bounds, kind): val = None if val is None else tuple(map(_to_datetime, val)) bounds = None if bounds is None else tuple(map(_to_datetime, bounds)) super()._validate_bounds(val, bounds, inclusive_bounds, kind) 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( f"{_validate_error_prefix(self)} only takes a tuple value, " f"not {type(val)}." ) for n in val: if isinstance(n, dt_types): continue raise ValueError( f"{_validate_error_prefix(self)} only takes date/datetime " f"values, not {type(n)}." ) start, end = val if not end >= start: raise ValueError( f"{_validate_error_prefix(self)} end datetime {val[1]} " f"is before start datetime {val[0]}." ) @classmethod def serialize(cls, value): if value is None: return None # 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' or value is None: 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( f"{_validate_error_prefix(self)} only takes date types, " f"not {val}." ) start, end = val if not end >= start: raise ValueError( f"{_validate_error_prefix(self)} end date {val[1]} is before " f"start date {val[0]}." ) def _validate_bound_type(self, value, position, kind): if not isinstance(value, dt.date): raise ValueError( f"{_validate_error_prefix(self)} {position} {kind} can only be " f"None or a date value, not {type(value)}." ) @classmethod def serialize(cls, value): if value is None: return None # As JSON has no tuple representation return [v.strftime("%Y-%m-%d") for v in value] @classmethod def deserialize(cls, value): if value == 'null' or value is None: return None # As JSON has no tuple representation return tuple([dt.datetime.strptime(v, "%Y-%m-%d").date() for v in value]) #----------------------------------------------------------------------------- # Callable #----------------------------------------------------------------------------- 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. """ @typing.overload def __init__( self, default=None, *, allow_None=False, doc=None, label=None, precedence=None, instantiate=False, constant=False, readonly=False, pickle_default_value=True, per_instance=True, allow_refs=False, nested_refs=False ): ... @_deprecate_positional_args def __init__(self, default=Undefined, **params): super().__init__(default=default, **params) self._validate(self.default) def _validate_value(self, val, allow_None): if (allow_None and val is None) or callable(val): return raise ValueError( f"{_validate_error_prefix(self)} only takes a callable object, " f"not objects of {type(val)}." ) def _validate(self, val): self._validate_value(val, self.allow_None) 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 #----------------------------------------------------------------------------- # Composite #----------------------------------------------------------------------------- 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'] @typing.overload def __init__( self, *, attribs=None, allow_None=False, doc=None, label=None, precedence=None, instantiate=False, constant=False, readonly=False, pickle_default_value=True, per_instance=True, allow_refs=False, nested_refs=False ): ... @_deprecate_positional_args def __init__(self, *, attribs=Undefined, **kw): if attribs is Undefined: attribs = [] super().__init__(default=Undefined, **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( f"{_validate_error_prefix(self)} got the wrong number " f"of values (needed {len(attribs)}, but got {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) #----------------------------------------------------------------------------- # Selector #----------------------------------------------------------------------------- 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 ListProxy(list): """ Container that supports both list-style and dictionary-style updates. Useful for replacing code that originally accepted lists but needs to support dictionary access (typically for naming items). """ def __init__(self, iterable, parameter=None): super().__init__(iterable) self._parameter = parameter def _warn(self, method): clsname = type(self._parameter).__name__ get_logger().warning( '{clsname}.objects{method} is deprecated if objects attribute ' 'was declared as a dictionary. Use `{clsname}.objects[label] ' '= value` instead.'.format(clsname=clsname, method=method) ) @contextmanager def _trigger(self, trigger=True): trigger = 'objects' in self._parameter.watchers and trigger old = dict(self._parameter.names) or list(self._parameter._objects) yield if trigger: value = self._parameter.names or self._parameter._objects self._parameter._trigger_event('objects', old, value) def __getitem__(self, index): if self._parameter.names: return self._parameter.names[index] return super().__getitem__(index) def __setitem__(self, index, object, trigger=True): if isinstance(index, (int, slice)): if self._parameter.names: self._warn('[index] = object') with self._trigger(): super().__setitem__(index, object) self._parameter._objects[index] = object return if self and not self._parameter.names: self._parameter.names = _named_objs(self) with self._trigger(trigger): if index in self._parameter.names: old = self._parameter.names[index] idx = self.index(old) super().__setitem__(idx, object) self._parameter._objects[idx] = object else: super().append(object) self._parameter._objects.append(object) self._parameter.names[index] = object def __eq__(self, other): eq = super().__eq__(other) if self._parameter.names and eq is NotImplemented: return dict(zip(self._parameter.names, self)) == other return eq def __ne__(self, other): return not self.__eq__(other) def append(self, object): if self._parameter.names: self._warn('.append') with self._trigger(): super().append(object) self._parameter._objects.append(object) def copy(self): if self._parameter.names: return self._parameter.names.copy() return list(self) def clear(self): with self._trigger(): super().clear() self._parameter._objects.clear() self._parameter.names.clear() def extend(self, objects): if self._parameter.names: self._warn('.append') with self._trigger(): super().extend(objects) self._parameter._objects.extend(objects) def get(self, key, default=None): if self._parameter.names: return self._parameter.names.get(key, default) return _named_objs(self).get(key, default) def insert(self, index, object): if self._parameter.names: self._warn('.insert') with self._trigger(): super().insert(index, object) self._parameter._objects.insert(index, object) def items(self): if self._parameter.names: return self._parameter.names.items() return _named_objs(self).items() def keys(self): if self._parameter.names: return self._parameter.names.keys() return _named_objs(self).keys() def pop(self, *args): index = args[0] if args else -1 if isinstance(index, int): with self._trigger(): super().pop(index) object = self._parameter._objects.pop(index) if self._parameter.names: self._parameter.names = { k: v for k, v in self._parameter.names.items() if v is object } return if self and not self._parameter.names: raise ValueError( 'Cannot pop an object from {clsname}.objects if ' 'objects was not declared as a dictionary.' ) with self._trigger(): object = self._parameter.names.pop(*args) super().remove(object) self._parameter._objects.remove(object) return object def remove(self, object): with self._trigger(): super().remove(object) self._parameter._objects.remove(object) if self._parameter.names: copy = self._parameter.names.copy() self._parameter.names.clear() self._parameter.names.update({ k: v for k, v in copy.items() if v is not object }) def update(self, objects, **items): if not self._parameter.names: self._parameter.names = _named_objs(self) objects = objects.items() if isinstance(objects, dict) else objects with self._trigger(): for i, o in enumerate(objects): if not isinstance(o, collections.abc.Sequence): raise TypeError( f'cannot convert dictionary update sequence element #{i} to a sequence' ) o = tuple(o) n = len(o) if n != 2: raise ValueError( f'dictionary update sequence element #{i} has length {n}; 2 is required' ) k, v = o self.__setitem__(k, v, trigger=False) for k, v in items.items(): self.__setitem__(k, v, trigger=False) def values(self): if self._parameter.names: return self._parameter.names.values() return _named_objs(self).values() class __compute_selector_default: """ Using a function instead of setting default to [] in _slot_defaults, as if it were modified in place later, which would happen with check_on_set set to False, then the object in _slot_defaults would itself be updated and the next Selector instance created wouldn't have [] as the default but a populated list. """ def __call__(self, p): return [] def __repr__(self): return repr(self.sig) @property def sig(self): return [] _compute_selector_default = __compute_selector_default() class __compute_selector_checking_default: def __call__(self, p): return len(p.objects) != 0 def __repr__(self): return repr(self.sig) @property def sig(self): return None _compute_selector_checking_default = __compute_selector_checking_default() class _SignatureSelector(Parameter): # Needs docstring; why is this a separate mixin? _slot_defaults = _dict_update( SelectorBase._slot_defaults, _objects=_compute_selector_default, compute_default_fn=None, check_on_set=_compute_selector_checking_default, allow_None=None, instantiate=False, default=None, ) @classmethod def _modified_slots_defaults(cls): defaults = super()._modified_slots_defaults() defaults['objects'] = defaults.pop('_objects') return defaults class Selector(SelectorBase, _SignatureSelector): """ 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. empty_default is an internal argument that does not have a slot. """ __slots__ = ['_objects', 'compute_default_fn', 'check_on_set', 'names'] @typing.overload def __init__( self, *, objects=[], default=None, instantiate=False, compute_default_fn=None, check_on_set=None, allow_None=None, empty_default=False, doc=None, label=None, precedence=None, constant=False, readonly=False, pickle_default_value=True, per_instance=True, allow_refs=False, nested_refs=False ): ... # Selector is usually used to allow selection from a list of # existing objects, therefore instantiate is False by default. @_deprecate_positional_args def __init__(self, *, objects=Undefined, default=Undefined, instantiate=Undefined, compute_default_fn=Undefined, check_on_set=Undefined, allow_None=Undefined, empty_default=False, **params): autodefault = Undefined if objects is not Undefined and objects: if isinstance(objects, dict): autodefault = list(objects.values())[0] elif isinstance(objects, list): autodefault = objects[0] default = autodefault if (not empty_default and default is Undefined) else default self.objects = objects self.compute_default_fn = compute_default_fn self.check_on_set = check_on_set super().__init__( default=default, instantiate=instantiate, **params) # Required as Parameter sets allow_None=True if default is None if allow_None is Undefined: self.allow_None = self._slot_defaults['allow_None'] else: self.allow_None = allow_None if self.default is not None: self._validate_value(self.default) self._update_state() def _update_state(self): if self.check_on_set is False and self.default is not None: self._ensure_value_is_in_objects(self.default) @property def objects(self): return ListProxy(self._objects, self) @objects.setter def objects(self, objects): if isinstance(objects, collections.abc.Mapping): self.names = objects self._objects = list(objects.values()) else: self.names = {} self._objects = objects # 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() self._ensure_value_is_in_objects(self.default) def _validate(self, val): if not self.check_on_set: self._ensure_value_is_in_objects(val) return self._validate_value(val) def _validate_value(self, val): if self.check_on_set and not (self.allow_None and val is None) and val not in self.objects: 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( f"{_validate_error_prefix(self)} does not accept {val!r}; " f"valid options include: {items!r}" ) 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. """ @typing.overload def __init__( self, default=None, *, objects=[], instantiate=False, compute_default_fn=None, check_on_set=None, allow_None=None, empty_default=False, doc=None, label=None, precedence=None, constant=False, readonly=False, pickle_default_value=True, per_instance=True, allow_refs=False, nested_refs=False ): ... @_deprecate_positional_args def __init__(self, default=Undefined, *, objects=Undefined, **kwargs): super().__init__(objects=objects, default=default, empty_default=True, **kwargs) class FileSelector(Selector): """ Given a path glob, allows one file to be selected from those matching. """ __slots__ = ['path'] _slot_defaults = _dict_update( Selector._slot_defaults, path="", ) @typing.overload def __init__( self, default=None, *, path="", objects=[], instantiate=False, compute_default_fn=None, check_on_set=None, allow_None=None, empty_default=False, doc=None, label=None, precedence=None, constant=False, readonly=False, pickle_default_value=True, per_instance=True, allow_refs=False, nested_refs=False ): ... @_deprecate_positional_args def __init__(self, default=Undefined, *, path=Undefined, **kwargs): self.default = default self.path = path self.update(path=path) if default is not Undefined: self.default = default super().__init__(default=self.default, objects=self._objects, **kwargs) def _on_set(self, attribute, old, new): super()._on_set(attribute, new, old) if attribute == 'path': self.update(path=new) def update(self, path=Undefined): if path is Undefined: path = self.path if path == "": self.objects = [] else: # Convert using os.fspath and pathlib.Path to handle ensure # the path separators are consistent (on Windows in particular) pathpattern = os.fspath(pathlib.Path(path)) self.objects = sorted(glob.glob(pathpattern)) 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().get_range()) class ListSelector(Selector): """ Variant of Selector where the value can be multiple objects from a list of possible objects. """ @typing.overload def __init__( self, default=None, *, objects=[], instantiate=False, compute_default_fn=None, check_on_set=None, allow_None=None, empty_default=False, doc=None, label=None, precedence=None, constant=False, readonly=False, pickle_default_value=True, per_instance=True, allow_refs=False, nested_refs=False ): ... @_deprecate_positional_args def __init__(self, default=Undefined, *, objects=Undefined, **kwargs): super().__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 self._validate_type(val) if self.check_on_set: self._validate_value(val) else: for v in val: self._ensure_value_is_in_objects(v) def _validate_type(self, val): if not isinstance(val, list): raise ValueError( f"{_validate_error_prefix(self)} only takes list types, " f"not {val!r}." ) def _validate_value(self, val): self._validate_type(val) if val is not None: for o in val: super()._validate_value(o) def _update_state(self): if self.check_on_set is False and self.default is not None: for o in self.default: self._ensure_value_is_in_objects(o) class MultiFileSelector(ListSelector): """ Given a path glob, allows multiple files to be selected from the list of matches. """ __slots__ = ['path'] _slot_defaults = _dict_update( Selector._slot_defaults, path="", ) @typing.overload def __init__( self, default=None, *, path="", objects=[], compute_default_fn=None, check_on_set=None, allow_None=None, empty_default=False, doc=None, label=None, precedence=None, instantiate=False, constant=False, readonly=False, pickle_default_value=True, per_instance=True, allow_refs=False, nested_refs=False ): ... @_deprecate_positional_args def __init__(self, default=Undefined, *, path=Undefined, **kwargs): self.default = default self.path = path self.update(path=path) super().__init__(default=default, objects=self._objects, **kwargs) def _on_set(self, attribute, old, new): super()._on_set(attribute, new, old) if attribute == 'path': self.update(path=new) def update(self, path=Undefined): if path is Undefined: path = self.path self.objects = sorted(glob.glob(path)) if self.default and all([o in self.objects for o in self.default]): return elif not self.default: return self.default = self.objects def get_range(self): return _abbreviate_paths(self.path,super().get_range()) class ClassSelector(SelectorBase): """ Parameter allowing selection of either a subclass or an instance of a class or tuple 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'] _slot_defaults = _dict_update(SelectorBase._slot_defaults, instantiate=True, is_instance=True) @typing.overload def __init__( self, *, class_, default=None, instantiate=True, is_instance=True, allow_None=False, doc=None, label=None, precedence=None, constant=False, readonly=False, pickle_default_value=True, per_instance=True, allow_refs=False, nested_refs=False ): ... @_deprecate_positional_args def __init__(self, *, class_, default=Undefined, instantiate=Undefined, is_instance=Undefined, **params): self.class_ = class_ self.is_instance = is_instance super().__init__(default=default,instantiate=instantiate,**params) self._validate(self.default) def _validate(self, val): super()._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__ if is_instance: if not (isinstance(val, class_)): raise ValueError( f"{_validate_error_prefix(self)} value must be an instance of {class_name}, not {val!r}.") else: if not (issubclass(val, class_)): raise ValueError( f"{_validate_error_prefix(self)} value must be a subclass of {class_name}, not {val}.") 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 Dict(ClassSelector): """ Parameter whose value is a dictionary. """ @typing.overload def __init__( self, default=None, *, is_instance=True, allow_None=False, doc=None, label=None, precedence=None, instantiate=True, constant=False, readonly=False, pickle_default_value=True, per_instance=True, allow_refs=False, nested_refs=False ): ... def __init__(self, default=Undefined, **params): super().__init__(default=default, class_=dict, **params) class Array(ClassSelector): """ Parameter whose value is a numpy array. """ @typing.overload def __init__( self, default=None, *, is_instance=True, allow_None=False, doc=None, label=None, precedence=None, instantiate=True, constant=False, readonly=False, pickle_default_value=True, per_instance=True, allow_refs=False, nested_refs=False ): ... def __init__(self, default=Undefined, **params): from numpy import ndarray super().__init__(default=default, class_=ndarray, **params) @classmethod def serialize(cls, value): if value is None: return None return value.tolist() @classmethod def deserialize(cls, value): if value == 'null' or value is None: return None import numpy if isinstance(value, str): return _deserialize_from_path( {'.npy': numpy.load, '.txt': lambda x: numpy.loadtxt(str(x))}, value, 'Array' ) else: return numpy.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'] _slot_defaults = _dict_update( ClassSelector._slot_defaults, rows=None, columns=None, ordered=None ) @typing.overload def __init__( self, default=None, *, rows=None, columns=None, ordered=None, is_instance=True, allow_None=False, doc=None, label=None, precedence=None, instantiate=True, constant=False, readonly=False, pickle_default_value=True, per_instance=True, allow_refs=False, nested_refs=False ): ... @_deprecate_positional_args def __init__(self, default=Undefined, *, rows=Undefined, columns=Undefined, ordered=Undefined, **params): from pandas import DataFrame as pdDFrame self.rows = rows self.columns = columns self.ordered = ordered super().__init__(default=default, class_=pdDFrame, **params) self._validate(self.default) def _length_bounds_check(self, bounds, length, name): message = f'{name} length {length} does not match declared bounds of {bounds}' if not isinstance(bounds, tuple): if (bounds != length): raise ValueError(f"{_validate_error_prefix(self)}: {message}") 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(f"{_validate_error_prefix(self)}: {message}") def _validate(self, val): super()._validate(val) if isinstance(self.columns, set) and self.ordered is True: raise ValueError( f'{_validate_error_prefix(self)}: columns cannot be ordered ' f'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) - {str(el) for el in val.columns} if difference: raise ValueError( f"{_validate_error_prefix(self)}: provided columns " f"{list(val.columns)} does not contain required " f"columns {sorted(self.columns)}" ) else: self._length_bounds_check(self.columns, len(val.columns), 'column') if self.ordered: if list(val.columns) != list(self.columns): raise ValueError( f"{_validate_error_prefix(self)}: provided columns " f"{list(val.columns)} must exactly match {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 None return value.to_dict('records') @classmethod def deserialize(cls, value): if value == 'null' or value is None: return None import pandas if isinstance(value, str): return _deserialize_from_path( { '.csv': pandas.read_csv, '.dta': pandas.read_stata, '.feather': pandas.read_feather, '.h5': pandas.read_hdf, '.hdf5': pandas.read_hdf, '.json': pandas.read_json, '.ods': pandas.read_excel, '.parquet': pandas.read_parquet, '.pkl': pandas.read_pickle, '.tsv': lambda x: pandas.read_csv(x, sep='\t'), '.xlsm': pandas.read_excel, '.xlsx': pandas.read_excel, }, value, 'DataFrame') else: return pandas.DataFrame(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'] _slot_defaults = _dict_update( ClassSelector._slot_defaults, rows=None, allow_None=False ) @typing.overload def __init__( self, default=None, *, rows=None, allow_None=False, is_instance=True, doc=None, label=None, precedence=None, instantiate=True, constant=False, readonly=False, pickle_default_value=True, per_instance=True, allow_refs=False, nested_refs=False ): ... @_deprecate_positional_args def __init__(self, default=Undefined, *, rows=Undefined, allow_None=Undefined, **params): from pandas import Series as pdSeries self.rows = rows super().__init__(default=default, class_=pdSeries, allow_None=allow_None, **params) self._validate(self.default) def _length_bounds_check(self, bounds, length, name): message = f'{name} length {length} does not match declared bounds of {bounds}' if not isinstance(bounds, tuple): if (bounds != length): raise ValueError(f"{_validate_error_prefix(self)}: {message}") 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(f"{_validate_error_prefix(self)}: {message}") def _validate(self, val): super()._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') #----------------------------------------------------------------------------- # List #----------------------------------------------------------------------------- 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_'] _slot_defaults = _dict_update( Parameter._slot_defaults, class_=None, item_type=None, bounds=(0, None), instantiate=True, default=[], ) @typing.overload def __init__( self, default=[], *, class_=None, item_type=None, instantiate=True, bounds=(0, None), allow_None=False, doc=None, label=None, precedence=None, constant=False, readonly=False, pickle_default_value=True, per_instance=True, allow_refs=False, nested_refs=False ): ... @_deprecate_positional_args def __init__(self, default=Undefined, *, class_=Undefined, item_type=Undefined, instantiate=Undefined, bounds=Undefined, **params): if class_ is not Undefined: # PARAM3_DEPRECATION warnings.warn( message="The 'class_' attribute on 'List' is deprecated. Use instead 'item_type'", category=_ParamDeprecationWarning, stacklevel=3, ) if item_type is not Undefined and class_ is not Undefined: self.item_type = item_type elif item_type is Undefined or item_type is None: self.item_type = class_ else: self.item_type = item_type self.class_ = self.item_type self.bounds = bounds Parameter.__init__(self, default=default, instantiate=instantiate, **params) self._validate(self.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( f"{_validate_error_prefix(self)} length must be between " f"{min_length} and {max_length} (inclusive), not {l}." ) elif min_length is not None: if not min_length <= l: raise ValueError( f"{_validate_error_prefix(self)} length must be at " f"least {min_length}, not {l}." ) elif max_length is not None: if not l <= max_length: raise ValueError( f"{_validate_error_prefix(self)} length must be at " f"most {max_length}, not {l}." ) def _validate_value(self, val, allow_None): if allow_None and val is None: return if not isinstance(val, list): raise ValueError( f"{_validate_error_prefix(self)} must be a list, not an " f"object of {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( f"{_validate_error_prefix(self)} items must be instances " f"of {item_type!r}, not {type(v)}." ) 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()._validate_value(val, allow_None) if allow_None and val is None: return for v in val: if callable(v): continue raise ValueError( f"{_validate_error_prefix(self)} items must be callable, " f"not {v!r}." ) #----------------------------------------------------------------------------- # Path #----------------------------------------------------------------------------- # 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 OSError(f"{ftype} '{path}' not found.") 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 OSError(ftype + " " + os.path.split(path)[1] + " was not found in the following place(s): " + str(paths_tried) + ".") # PARAM3_DEPRECATION @_deprecated() 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 Parameters 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). Parameters ---------- search_paths : list, default=[os.getcwd()] List of paths to search the path from check_exists: boolean, default=True If True (default) the path must exist on instantiation and set, otherwise the path can optionally exist. """ __slots__ = ['search_paths', 'check_exists'] _slot_defaults = _dict_update( Parameter._slot_defaults, check_exists=True, ) @typing.overload def __init__( self, default=None, *, search_paths=None, check_exists=True, allow_None=False, doc=None, label=None, precedence=None, instantiate=False, constant=False, readonly=False, pickle_default_value=True, per_instance=True, allow_refs=False, nested_refs=False ): ... @_deprecate_positional_args def __init__(self, default=Undefined, *, search_paths=Undefined, check_exists=Undefined, **params): if search_paths is Undefined: search_paths = [] self.search_paths = search_paths if check_exists is not Undefined and not isinstance(check_exists, bool): raise ValueError("'check_exists' attribute value must be a boolean") self.check_exists = check_exists super().__init__(default,**params) self._validate(self.default) 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: raise ValueError(f'{_validate_error_prefix(self)} does not accept None') else: if not isinstance(val, (str, pathlib.Path)): raise ValueError(f'{_validate_error_prefix(self)} only take str or pathlib.Path types') try: self._resolve(val) except OSError as e: if self.check_exists: raise OSError(e.args[0]) from None def __get__(self, obj, objtype): """ Return an absolute, normalized path (see resolve_path). """ raw_path = super().__get__(obj,objtype) if raw_path is None: path = None else: try: path = self._resolve(raw_path) except OSError: if self.check_exists: raise else: path = raw_path return path def __getstate__(self): # don't want to pickle the search_paths state = super().__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) #----------------------------------------------------------------------------- # Color #----------------------------------------------------------------------------- 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'] _slot_defaults = _dict_update(Parameter._slot_defaults, allow_named=True) @typing.overload def __init__( self, default=None, *, allow_named=True, allow_None=False, doc=None, label=None, precedence=None, instantiate=False, constant=False, readonly=False, pickle_default_value=True, per_instance=True, allow_refs=False, nested_refs=False ): ... @_deprecate_positional_args def __init__(self, default=Undefined, *, allow_named=Undefined, **kwargs): super().__init__(default=default, **kwargs) self.allow_named = allow_named self._validate(self.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, str): raise ValueError( f"{_validate_error_prefix(self)} expects a string value, " f"not an object of {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.lower() not in self._named_colors: raise ValueError( f"{_validate_error_prefix(self)} only takes RGB hex codes " f"or named colors, received '{val}'." ) elif not is_hex: raise ValueError( f"{_validate_error_prefix(self)} only accepts valid RGB hex " f"codes, received {val!r}." ) #----------------------------------------------------------------------------- # Bytes #----------------------------------------------------------------------------- class Bytes(Parameter): """ A Bytes Parameter, with a default value and optional regular expression (regex) matching. Similar to the String parameter, but instead of type string this parameter only allows objects of type bytes (e.g. b'bytes'). """ __slots__ = ['regex'] _slot_defaults = _dict_update( Parameter._slot_defaults, default=b"", regex=None, allow_None=False, ) @typing.overload def __init__( self, default=b"", *, regex=None, allow_None=False, doc=None, label=None, precedence=None, instantiate=False, constant=False, readonly=False, pickle_default_value=True, per_instance=True, allow_refs=False, nested_refs=False ): ... @_deprecate_positional_args def __init__(self, default=Undefined, *, regex=Undefined, allow_None=Undefined, **kwargs): super().__init__(default=default, **kwargs) self.regex = regex self._validate(self.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( f"{_validate_error_prefix(self)} value {val!r} " f"does not match regex {regex!r}." ) def _validate_value(self, val, allow_None): if allow_None and val is None: return if not isinstance(val, bytes): raise ValueError( f"{_validate_error_prefix(self)} only takes a byte string value, " f"not value of {type(val)}." ) def _validate(self, val): self._validate_value(val, self.allow_None) self._validate_regex(val, self.regex) param-2.1.1/param/reactive.py000066400000000000000000001315111463636336300161060ustar00rootroot00000000000000""" reactive API `rx` is a wrapper around a Python object that lets users create reactive expression pipelines by calling existing APIs on an object with dynamic parameters or widgets. An `rx` instance watches what operations are applied to the object and records these on each instance, which are then strung together into a chain. The original input to an `rx` object is stored in a mutable list and can be accessed via the `_obj` property. The shared mutable data structure ensures that all `rx` instances created from the same object can hold a shared reference that can be updated, e.g. via the `.value` property or because the input was itself a reference to some object that can potentially be updated. When an operation is applied to an `rx` instance, it will record the operation and create a new instance using the `_clone` method, e.g. `dfi.head()` first records that the `'head'` attribute is accessed, which is achieved by overriding `__getattribute__`. A new reactive object is returned, which will then record that it is being called, and that new object will be itself called, as `rx` implements `__call__`. `__call__` returns another `rx` instance. To be able to watch all the potential operations that may be applied to an object, `rx` implements: - `__getattribute__`: Watching for attribute accesses - `__call__`: Intercepting both actual calls or method calls if an attribute was previously accessed - `__getitem__`: Intercepting indexing operations - Operators: Implementing all valid operators `__gt__`, `__add__`, etc. - `__array_ufunc__`: Intercepting numpy universal function calls The `rx` object evaluates operations lazily, but whenever the current value is needed the operations are automatically evaluated. Note that even attribute access or tab-completion operations can result in evaluation of the pipeline. This is very useful in a REPL, as this allows inspecting the transformed object at any point of the pipeline, and as such provide correct auto-completion and docstrings. E.g. executing `dfi.A.max?` in an interactive REPL or notebook where it allows returning the docstring of the method being accessed. The actual operations are stored as a dictionary on the `_operation` attribute of each instance. They contain 4 keys: - `fn`: The function to apply (either an actual function or a string indicating the operation is a method on the object) - `args`: Any arguments to supply to the `fn`. - `kwargs`: Any keyword arguments to supply to the `fn`. - `reverse`: If the function is not a method this indicates whether the first arg and the input object should be supplied in reverse order. The `_depth` attribute starts at 0 and is incremented by 1 every time a new `rx` instance is created part of a chain. The root instance in a reactive expression has a `_depth` of 0. A reactive expression can consist of multiple chains, such as `dfi[dfi.A > 1]`, as the `rx` instance is referenced twice in the expression. As a consequence `_depth` is not the total count of `rx` instance creations of a pipeline, it is the count of instances created in the outer chain. In the example, that would be `dfi[]`. Each `rx` instance keeps a reference to the previous instance in the chain and each instance tracks whether its current value is up-to-date via the `_dirty` attribute, which is set to False if any dependency changes. The `_method` attribute is a string that temporarily stores the method/attr accessed on the object, e.g. `_method` is 'head' in `dfi.head()`, until the `rx` instance created in the pipeline is called at which point `_method` is reset to None. In cases such as `dfi.head` or `dfi.A`, `_method` is not (yet) reset to None. At this stage the `rx` instance returned has its `_current` attribute not updated, e.g. `dfi.A._current` is still the original dataframe, not the 'A' series. Keeping `_method` is thus useful for instance to display `dfi.A`, as the evaluation of the object will check whether `_method` is set or not, and if it's set it will use it to compute the object returned, e.g. the series `df.A` or the method `df.head`, and display its repr. """ from __future__ import annotations import asyncio import inspect import math import operator from collections.abc import Iterable, Iterator from functools import partial from types import FunctionType, MethodType from typing import Any, Callable, Optional from .depends import depends from .display import _display_accessors, _reactive_display_objs from .parameterized import ( Parameter, Parameterized, Skip, Undefined, eval_function_with_deps, get_method_owner, register_reference_transform, resolve_ref, resolve_value, transform_reference ) from .parameters import Boolean, Event from ._utils import _to_async_gen, iscoroutinefunction, full_groupby class Wrapper(Parameterized): """ Helper class to allow updating literal values easily. """ object = Parameter(allow_refs=False) class GenWrapper(Parameterized): """ Helper class to allow streaming from generator functions. """ object = Parameter(allow_refs=True) class Trigger(Parameterized): """ Helper class to allow triggering an event under some condition. """ value = Event() def __init__(self, parameters=None, internal=False, **params): super().__init__(**params) self.internal = internal self.parameters = parameters class Resolver(Parameterized): """ Helper class to allow (recursively) resolving references. """ object = Parameter(allow_refs=True) recursive = Boolean(default=False) value = Parameter() def __init__(self, **params): self._watchers = [] super().__init__(**params) def _resolve_value(self, *events): nested = self.param.object.nested_refs refs = resolve_ref(self.object, nested) value = resolve_value(self.object, nested) if self.recursive: new_refs = [r for r in resolve_ref(value, nested) if r not in refs] while new_refs: refs += new_refs value = resolve_value(value, nested) new_refs = [r for r in resolve_ref(value, nested) if r not in refs] if events: self._update_refs(refs) self.value = value return refs @depends('object', watch=True, on_init=True) def _resolve_object(self): refs = self._resolve_value() self._update_refs(refs) def _update_refs(self, refs): for w in self._watchers: (w.inst or w.cls).param.unwatch(w) self._watchers = [] for _, params in full_groupby(refs, lambda x: id(x.owner)): self._watchers.append( params[0].owner.param.watch(self._resolve_value, [p.name for p in params]) ) class NestedResolver(Resolver): object = Parameter(allow_refs=True, nested_refs=True) class reactive_ops: """ Namespace for reactive operators. Implements operators that cannot be implemented using regular Python syntax. """ def __init__(self, reactive): self._reactive = reactive def _as_rx(self): return self._reactive if isinstance(self._reactive, rx) else self() def __call__(self): rxi = self._reactive return rxi if isinstance(rx, rx) else rx(rxi) def and_(self, other): """ Replacement for the ``and`` statement. """ return self._as_rx()._apply_operator(lambda obj, other: obj and other, other) def bool(self): """ __bool__ cannot be implemented so it is provided as a method. """ return self._as_rx()._apply_operator(bool) def buffer(self, n): """ Collects the last n items that were emitted. """ items = [] def collect(new, n): items.append(new) while len(items) > n: items.pop(0) return items return self._as_rx()._apply_operator(collect, n) def in_(self, other): """ Replacement for the ``in`` statement. """ return self._as_rx()._apply_operator(operator.contains, other, reverse=True) def is_(self, other): """ Replacement for the ``is`` statement. """ return self._as_rx()._apply_operator(operator.is_, other) def is_not(self, other): """ Replacement for the ``is not`` statement. """ return self._as_rx()._apply_operator(operator.is_not, other) def len(self): """ __len__ cannot be implemented so it is provided as a method. """ return self._as_rx()._apply_operator(len) def map(self, func, /, *args, **kwargs): """ Apply a function to each item. Arguments --------- func: function Function to apply. args: iterable, optional Positional arguments to pass to `func`. kwargs: mapping, optional A dictionary of keywords to pass to `func`. """ if inspect.isasyncgenfunction(func) or inspect.isgeneratorfunction(func): raise TypeError( "Cannot map a generator function. Only regular function " "or coroutine functions are permitted." ) if inspect.iscoroutinefunction(func): async def apply(vs, *args, **kwargs): return list(await asyncio.gather(*(func(v, *args, **kwargs) for v in vs))) else: def apply(vs, *args, **kwargs): return [func(v, *args, **kwargs) for v in vs] return self._as_rx()._apply_operator(apply, *args, **kwargs) def not_(self): """ __bool__ cannot be implemented so not has to be provided as a method. """ return self._as_rx()._apply_operator(operator.not_) def or_(self, other): """ Replacement for the ``or`` statement. """ return self._as_rx()._apply_operator(lambda obj, other: obj or other, other) def pipe(self, func, /, *args, **kwargs): """ Apply chainable functions. Arguments --------- func: function Function to apply. args: iterable, optional Positional arguments to pass to `func`. kwargs: mapping, optional A dictionary of keywords to pass to `func`. """ return self._as_rx()._apply_operator(func, *args, **kwargs) def resolve(self, nested=True, recursive=False): """ Resolves references held by the expression. As an example if the expression returns a list of parameters this operation will return a list of the parameter values. Arguments --------- nested: bool Whether to resolve references contained within nested objects, i.e. tuples, lists, sets and dictionaries. recursive: bool Whether to recursively resolve references, i.e. if a reference itself returns a reference we recurse into it until no more references can be resolved. """ resolver_type = NestedResolver if nested else Resolver resolver = resolver_type(object=self._reactive, recursive=recursive) return resolver.param.value.rx() def updating(self): """ Returns a new expression that is True while the expression is updating. """ wrapper = Wrapper(object=False) self._watch(lambda e: wrapper.param.update(object=True), precedence=-999) self._watch(lambda e: wrapper.param.update(object=False), precedence=999) return wrapper.param.object.rx() def when(self, *dependencies, initial=Undefined): """ Returns a reactive expression that emits the contents of this expression only when the dependencies change. If initial value is provided and the dependencies are all param.Event types the expression will not be evaluated until the first event is triggered. Arguments --------- dependencies: param.Parameter | rx A dependency that will trigger an update in the output. initial: object Object that will stand in for the actual value until the first time a param.Event in the dependencies is triggered. """ deps = [p for d in dependencies for p in resolve_ref(d)] is_event = all(isinstance(dep, Event) for dep in deps) def eval(*_, evaluated=[]): if is_event and initial is not Undefined and not evaluated: # Abuse mutable default value to keep track of evaluation state evaluated.append(True) return initial else: return self.value return bind(eval, *deps).rx() def where(self, x, y): """ Returns either x or y depending on the current state of the expression, i.e. replaces a ternary if statement. Arguments --------- x: object The value to return if the expression evaluates to True. y: object The value to return if the expression evaluates to False. """ xrefs = resolve_ref(x) yrefs = resolve_ref(y) if isinstance(self._reactive, rx): params = self._reactive._params else: params = resolve_ref(self._reactive) trigger = Trigger(parameters=params) if xrefs: def trigger_x(*args): if self.value: trigger.param.trigger('value') bind(trigger_x, *xrefs, watch=True) if yrefs: def trigger_y(*args): if not self.value: trigger.param.trigger('value') bind(trigger_y, *yrefs, watch=True) def ternary(condition, _): return resolve_value(x) if condition else resolve_value(y) return bind(ternary, self._reactive, trigger.param.value) # Operations to get the output and set the input of an expression @property def value(self): """ Returns the current state of the reactive expression by evaluating the pipeline. """ if isinstance(self._reactive, rx): return self._reactive._resolve() elif isinstance(self._reactive, Parameter): return getattr(self._reactive.owner, self._reactive.name) else: return self._reactive() @value.setter def value(self, new): """ Allows overriding the original input to the pipeline. """ if isinstance(self._reactive, Parameter): raise AttributeError( "`Parameter.rx.value = value` is not supported. Cannot override " "parameter value." ) elif not isinstance(self._reactive, rx): raise AttributeError( "`bind(...).rx.value = value` is not supported. Cannot override " "the output of a function." ) elif self._reactive._root is not self._reactive: raise AttributeError( "The value of a derived expression cannot be set. Ensure you " "set the value on the root node wrapping a concrete value, e.g.:" "\n\n a = rx(1)\n b = a + 1\n a.rx.value = 2\n\n " "is valid but you may not set `b.rx.value = 2`." ) if self._reactive._wrapper is None: raise AttributeError( "Setting the value of a reactive expression is only " "supported if it wraps a concrete value. A reactive " "expression wrapping a Parameter or another dynamic " "reference cannot be updated." ) self._reactive._wrapper.object = resolve_value(new) def watch(self, fn=None, onlychanged=True, queued=False, precedence=0): """ Adds a callable that observes the output of the pipeline. If no callable is provided this simply causes the expression to be eagerly evaluated. """ if precedence < 0: raise ValueError("User-defined watch callbacks must declare " "a positive precedence. Negative precedences " "are reserved for internal Watchers.") self._watch(fn, onlychanged=onlychanged, queued=queued, precedence=precedence) def _watch(self, fn=None, onlychanged=True, queued=False, precedence=0): def cb(value): from .parameterized import async_executor if iscoroutinefunction(fn): async_executor(partial(fn, value)) elif fn is not None: fn(value) bind(cb, self._reactive, watch=True) def bind(function, *args, watch=False, **kwargs): """ Given a function, returns a wrapper function that binds the values of some or all arguments to Parameter values and expresses Param dependencies on those values, so that the function can be invoked whenever the underlying values change and the output will reflect those updated values. As for functools.partial, arguments can also be bound to constants, which allows all of the arguments to be bound, leaving a simple callable object. Arguments --------- function: callable The function to bind constant or dynamic args and kwargs to. args: object, param.Parameter Positional arguments to bind to the function. watch: boolean Whether to evaluate the function automatically whenever one of the bound parameters changes. kwargs: object, param.Parameter Keyword arguments to bind to the function. Returns ------- Returns a new function with the args and kwargs bound to it and annotated with all dependencies. """ args, kwargs = ( tuple(transform_reference(arg) for arg in args), {key: transform_reference(arg) for key, arg in kwargs.items()} ) dependencies = {} # If the wrapped function has a dependency add it fn_dep = transform_reference(function) if isinstance(fn_dep, Parameter) or hasattr(fn_dep, '_dinfo'): dependencies['__fn'] = fn_dep # Extract dependencies from args and kwargs for i, p in enumerate(args): if hasattr(p, '_dinfo'): for j, arg in enumerate(p._dinfo['dependencies']): dependencies[f'__arg{i}_arg{j}'] = arg for kw, kwarg in p._dinfo['kw'].items(): dependencies[f'__arg{i}_arg_{kw}'] = kwarg elif isinstance(p, Parameter): dependencies[f'__arg{i}'] = p for kw, v in kwargs.items(): if hasattr(v, '_dinfo'): for j, arg in enumerate(v._dinfo['dependencies']): dependencies[f'__kwarg_{kw}_arg{j}'] = arg for pkw, kwarg in v._dinfo['kw'].items(): dependencies[f'__kwarg_{kw}_{pkw}'] = kwarg elif isinstance(v, Parameter): dependencies[kw] = v def combine_arguments(wargs, wkwargs, asynchronous=False): combined_args = [] for arg in args: if hasattr(arg, '_dinfo'): arg = eval_function_with_deps(arg) elif isinstance(arg, Parameter): arg = getattr(arg.owner, arg.name) combined_args.append(arg) combined_args += list(wargs) combined_kwargs = {} for kw, arg in kwargs.items(): if hasattr(arg, '_dinfo'): arg = eval_function_with_deps(arg) elif isinstance(arg, Parameter): arg = getattr(arg.owner, arg.name) combined_kwargs[kw] = arg for kw, arg in wkwargs.items(): if asynchronous: if kw.startswith('__arg'): index = kw[5:] if index.isdigit(): combined_args[int(index)] = arg elif kw.startswith('__kwarg'): substring = kw[8:] if substring in combined_kwargs: combined_kwargs[substring] = arg continue elif kw.startswith('__arg') or kw.startswith('__kwarg') or kw.startswith('__fn'): continue combined_kwargs[kw] = arg return combined_args, combined_kwargs def eval_fn(): if callable(function): fn = function else: p = transform_reference(function) if isinstance(p, Parameter): fn = getattr(p.owner, p.name) else: fn = eval_function_with_deps(p) return fn if inspect.isgeneratorfunction(function): def wrapped(*wargs, **wkwargs): combined_args, combined_kwargs = combine_arguments( wargs, wkwargs, asynchronous=True ) evaled = eval_fn()(*combined_args, **combined_kwargs) for val in evaled: yield val wrapper_fn = depends(**dependencies, watch=watch)(wrapped) wrapped._dinfo = wrapper_fn._dinfo elif inspect.isasyncgenfunction(function): async def wrapped(*wargs, **wkwargs): combined_args, combined_kwargs = combine_arguments( wargs, wkwargs, asynchronous=True ) evaled = eval_fn()(*combined_args, **combined_kwargs) async for val in evaled: yield val wrapper_fn = depends(**dependencies, watch=watch)(wrapped) wrapped._dinfo = wrapper_fn._dinfo elif iscoroutinefunction(function): @depends(**dependencies, watch=watch) async def wrapped(*wargs, **wkwargs): combined_args, combined_kwargs = combine_arguments( wargs, wkwargs, asynchronous=True ) evaled = eval_fn()(*combined_args, **combined_kwargs) return await evaled else: @depends(**dependencies, watch=watch) def wrapped(*wargs, **wkwargs): combined_args, combined_kwargs = combine_arguments(wargs, wkwargs) return eval_fn()(*combined_args, **combined_kwargs) wrapped.__bound_function__ = function wrapped.rx = reactive_ops(wrapped) _reactive_display_objs.add(wrapped) for name, accessor in _display_accessors.items(): setattr(wrapped, name, accessor(wrapped)) return wrapped class rx: """ `rx` allows wrapping objects and then operating on them interactively while recording any operations applied to them. By recording all arguments or operands in the operations the recorded pipeline can be replayed if an operand represents a dynamic value. Parameters ---------- obj: any A supported data structure object Examples -------- Instantiate it from an object: >>> ifloat = rx(3.14) >>> ifloat * 2 6.28 Then update the original value and see the new result: >>> ifloat.value = 1 2 """ _accessors: dict[str, Callable[[rx], Any]] = {} _display_options: tuple[str] = () _display_handlers: dict[type, tuple[Any, dict[str, Any]]] = {} _method_handlers: dict[str, Callable] = {} @classmethod def register_accessor( cls, name: str, accessor: Callable[[rx], Any], predicate: Optional[Callable[[Any], bool]] = None ): """ Registers an accessor that extends rx with custom behavior. Arguments --------- name: str The name of the accessor will be attribute-accessible under. accessor: Callable[[rx], any] A callable that will return the accessor namespace object given the rx object it is registered on. predicate: Callable[[Any], bool] | None """ cls._accessors[name] = (accessor, predicate) @classmethod def register_display_handler(cls, obj_type, handler, **kwargs): """ Registers a display handler for a specific type of object, making it possible to define custom display options for specific objects. Arguments --------- obj_type: type | callable The type to register a custom display handler on. handler: Viewable | callable A Viewable or callable that is given the object to be displayed and the custom keyword arguments. kwargs: dict[str, Any] Additional display options to register for this type. """ cls._display_handlers[obj_type] = (handler, kwargs) @classmethod def register_method_handler(cls, method, handler): """ Registers a handler that is called when a specific method on an object is called. """ cls._method_handlers[method] = handler def __new__(cls, obj=None, **kwargs): wrapper = None obj = transform_reference(obj) if kwargs.get('fn'): # rx._clone codepath fn = kwargs.pop('fn') wrapper = kwargs.pop('_wrapper', None) elif inspect.isgeneratorfunction(obj) or iscoroutinefunction(obj): # Resolves generator and coroutine functions lazily wrapper = GenWrapper(object=obj) fn = bind(lambda obj: obj, wrapper.param.object) obj = Undefined elif isinstance(obj, (FunctionType, MethodType)) and hasattr(obj, '_dinfo'): # Bound functions and methods are resolved on access fn = obj obj = None elif isinstance(obj, Parameter): fn = bind(lambda obj: obj, obj) obj = getattr(obj.owner, obj.name) else: # For all other objects wrap them so they can be updated # via .rx.value property wrapper = Wrapper(object=obj) fn = bind(lambda obj: obj, wrapper.param.object) inst = super(rx, cls).__new__(cls) inst._fn = fn inst._shared_obj = kwargs.get('_shared_obj', None if obj is None else [obj]) inst._wrapper = wrapper return inst def __init__( self, obj=None, operation=None, fn=None, depth=0, method=None, prev=None, _shared_obj=None, _current=None, _wrapper=None, **kwargs ): # _init is used to prevent to __getattribute__ to execute its # specialized code. self._init = False display_opts = {} for _, opts in self._display_handlers.values(): for k, o in opts.items(): display_opts[k] = o display_opts.update({ dopt: kwargs.pop(dopt) for dopt in self._display_options + tuple(display_opts) if dopt in kwargs }) self._display_opts = display_opts self._method = method self._operation = operation self._depth = depth self._dirty = _current is None self._dirty_obj = False self._current_task = None self._error_state = None self._current_ = _current if isinstance(obj, rx) and not prev: self._prev = obj else: self._prev = prev # Define special trigger parameter if operation has to be lazily evaluated if operation and (iscoroutinefunction(operation['fn']) or inspect.isgeneratorfunction(operation['fn'])): self._trigger = Trigger(internal=True) self._current_ = Undefined else: self._trigger = None self._root = self._compute_root() self._fn_params = self._compute_fn_params() self._internal_params = self._compute_params() # Filter params that external objects depend on, ensuring # that Trigger parameters do not cause double execution self._params = [ p for p in self._internal_params if (not isinstance(p.owner, Trigger) or p.owner.internal) or any (p not in self._internal_params for p in p.owner.parameters) ] self._setup_invalidations(depth) self._kwargs = kwargs self.rx = reactive_ops(self) self._init = True for name, accessor in _display_accessors.items(): setattr(self, name, accessor(self)) for name, (accessor, predicate) in rx._accessors.items(): if predicate is None or predicate(self._current): setattr(self, name, accessor(self)) @property def _obj(self): if self._shared_obj is None: self._obj = eval_function_with_deps(self._fn) elif self._root._dirty_obj: root = self._root root._shared_obj[0] = eval_function_with_deps(root._fn) root._dirty_obj = False return self._shared_obj[0] @_obj.setter def _obj(self, obj): if self._shared_obj is None: self._shared_obj = [obj] else: self._shared_obj[0] = obj @property def _current(self): if self._error_state: raise self._error_state elif self._dirty or self._root._dirty_obj: self._resolve() return self._current_ def _compute_root(self): if self._prev is None: return self root = self while root._prev is not None: root = root._prev return root def _compute_fn_params(self) -> list[Parameter]: if self._fn is None: return [] owner = get_method_owner(self._fn) if owner is not None: deps = [ dep.pobj for dep in owner.param.method_dependencies(self._fn.__name__) ] return deps dinfo = getattr(self._fn, '_dinfo', {}) args = list(dinfo.get('dependencies', [])) kwargs = list(dinfo.get('kw', {}).values()) return args + kwargs def _compute_params(self) -> list[Parameter]: ps = list(self._fn_params) if self._trigger: ps.append(self._trigger.param.value) # Collect parameters on previous objects in chain prev = self._prev while prev is not None: for p in prev._params: if p not in ps: ps.append(p) prev = prev._prev if self._operation is None: return ps # Accumulate dependencies in args and/or kwargs for ref in resolve_ref(self._operation['fn']): if ref not in ps: ps.append(ref) for arg in list(self._operation['args'])+list(self._operation['kwargs'].values()): for ref in resolve_ref(arg, recursive=True): if ref not in ps: ps.append(ref) return ps def _setup_invalidations(self, depth: int = 0): """ Since the parameters of the pipeline can change at any time we have to invalidate the internal state of the pipeline. To handle both invalidations of the inputs of the pipeline and the pipeline itself we set up watchers on both. 1. The first invalidation we have to set up is to re-evaluate the function that feeds the pipeline. Only the root node of a pipeline has to perform this invalidation because all leaf nodes inherit the same shared_obj. This avoids evaluating the same function for every branch of the pipeline. 2. The second invalidation is for the pipeline itself, i.e. if any parameter changes we have to notify the pipeline that it has to re-evaluate the pipeline. This is done by marking the pipeline as `_dirty`. The next time the `_current` value is requested the value is resolved by re-executing the pipeline. """ if self._fn is not None: for _, params in full_groupby(self._fn_params, lambda x: id(x.owner)): fps = [p.name for p in params if p in self._root._fn_params] if fps: params[0].owner.param._watch(self._invalidate_obj, fps, precedence=-1) for _, params in full_groupby(self._internal_params, lambda x: id(x.owner)): params[0].owner.param._watch(self._invalidate_current, [p.name for p in params], precedence=-1) def _invalidate_current(self, *events): if all(event.obj is self._trigger for event in events): return self._dirty = True self._error_state = None def _invalidate_obj(self, *events): self._root._dirty_obj = True self._error_state = None async def _resolve_async(self, obj): self._current_task = task = asyncio.current_task() if inspect.isasyncgen(obj): async for val in obj: if self._current_task is not task: break self._current_ = val self._trigger.param.trigger('value') else: value = await obj if self._current_task is task: self._current_ = value self._trigger.param.trigger('value') def _lazy_resolve(self, obj): from .parameterized import async_executor if inspect.isgenerator(obj): obj = _to_async_gen(obj) async_executor(partial(self._resolve_async, obj)) def _resolve(self): if self._error_state: raise self._error_state elif self._dirty or self._root._dirty_obj: try: obj = self._obj if self._prev is None else self._prev._resolve() if obj is Skip or obj is Undefined: self._current_ = Undefined raise Skip operation = self._operation if operation: obj = self._eval_operation(obj, operation) if inspect.isasyncgen(obj) or inspect.iscoroutine(obj) or inspect.isgenerator(obj): self._lazy_resolve(obj) obj = Skip if obj is Skip: raise Skip except Skip: self._dirty = False return self._current_ except Exception as e: self._error_state = e raise e self._current_ = current = obj else: current = self._current_ self._dirty = False if self._method: # E.g. `pi = dfi.A` leads to `pi._method` equal to `'A'`. current = getattr(current, self._method, current) if hasattr(current, '__call__'): self.__call__.__func__.__doc__ = self.__call__.__doc__ return current def _transform_output(self, obj): """ Applies custom display handlers before their output. """ applies = False for predicate, (handler, opts) in self._display_handlers.items(): display_opts = { k: v for k, v in self._display_opts.items() if k in opts } display_opts.update(self._kwargs) try: applies = predicate(obj, **display_opts) except TypeError: applies = predicate(obj) if applies: new = handler(obj, **display_opts) if new is not obj: return new return obj @property def _callback(self): params = self._params def evaluate(*args, **kwargs): out = self._current if self._method: out = getattr(out, self._method) return self._transform_output(out) if params: return bind(evaluate, *params) return evaluate def _clone(self, operation=None, copy=False, **kwargs): operation = operation or self._operation depth = self._depth + 1 if copy: kwargs = dict( self._kwargs, _current=self._current, method=self._method, prev=self._prev, **kwargs ) else: kwargs = dict(prev=self, **dict(self._kwargs, **kwargs)) kwargs = dict(self._display_opts, **kwargs) return type(self)( self._obj, operation=operation, depth=depth, fn=self._fn, _shared_obj=self._shared_obj, _wrapper=self._wrapper, **kwargs ) def __dir__(self): current = self._current if self._method: current = getattr(current, self._method) extras = {attr for attr in dir(current) if not attr.startswith('_')} try: return sorted(set(super().__dir__()) | extras) except Exception: return sorted(set(dir(type(self))) | set(self.__dict__) | extras) def _resolve_accessor(self): if not self._method: # No method is yet set, as in `dfi.A`, so return a copied clone. return self._clone(copy=True) # This is executed when one runs e.g. `dfi.A > 1`, in which case after # dfi.A the _method 'A' is set (in __getattribute__) which allows # _resolve_accessor to record the attribute access as an operation. operation = { 'fn': getattr, 'args': (self._method,), 'kwargs': {}, 'reverse': False } self._method = None return self._clone(operation) def __getattribute__(self, name): self_dict = super().__getattribute__('__dict__') if not self_dict.get('_init') or name == 'rx' or name.startswith('_'): return super().__getattribute__(name) current = self_dict['_current_'] dirty = self_dict['_dirty'] if dirty: self._resolve() current = self_dict['_current_'] method = self_dict['_method'] if method: current = getattr(current, method) # Getting all the public attributes available on the current object, # e.g. `sum`, `head`, etc. extras = [d for d in dir(current) if not d.startswith('_')] if (name in extras or current is Undefined) and name not in super().__dir__(): new = self._resolve_accessor() # Setting the method name for a potential use later by e.g. an # operator or method, as in `dfi.A > 2`. or `dfi.A.max()` new._method = name try: new.__doc__ = getattr(current, name).__doc__ except Exception: pass return new return super().__getattribute__(name) def __call__(self, *args, **kwargs): new = self._clone(copy=True) method = new._method or '__call__' if method == '__call__' and self._depth == 0 and not hasattr(self._current, '__call__'): return self.set_display(*args, **kwargs) if method in rx._method_handlers: handler = rx._method_handlers[method] method = handler(self) new._method = None kwargs = dict(kwargs) operation = { 'fn': method, 'args': args, 'kwargs': kwargs, 'reverse': False } return new._clone(operation) #---------------------------------------------------------------- # rx pipeline APIs #---------------------------------------------------------------- def __array_ufunc__(self, ufunc, method, *args, **kwargs): new = self._resolve_accessor() operation = { 'fn': getattr(ufunc, method), 'args': args[1:], 'kwargs': kwargs, 'reverse': False } return new._clone(operation) def _apply_operator(self, operator, *args, reverse=False, **kwargs): new = self._resolve_accessor() operation = { 'fn': operator, 'args': args, 'kwargs': kwargs, 'reverse': reverse } return new._clone(operation) # Builtin functions def __abs__(self): return self._apply_operator(abs) def __str__(self): return self._apply_operator(str) def __round__(self, ndigits=None): args = () if ndigits is None else (ndigits,) return self._apply_operator(round, *args) # Unary operators def __ceil__(self): return self._apply_operator(math.ceil) def __floor__(self): return self._apply_operator(math.floor) def __invert__(self): return self._apply_operator(operator.inv) def __neg__(self): return self._apply_operator(operator.neg) def __pos__(self): return self._apply_operator(operator.pos) def __trunc__(self): return self._apply_operator(math.trunc) # Binary operators def __add__(self, other): return self._apply_operator(operator.add, other) def __and__(self, other): return self._apply_operator(operator.and_, other) def __contains_(self, other): return self._apply_operator(operator.contains, other) def __divmod__(self, other): return self._apply_operator(divmod, other) def __eq__(self, other): return self._apply_operator(operator.eq, other) def __floordiv__(self, other): return self._apply_operator(operator.floordiv, other) def __ge__(self, other): return self._apply_operator(operator.ge, other) def __gt__(self, other): return self._apply_operator(operator.gt, other) def __le__(self, other): return self._apply_operator(operator.le, other) def __lt__(self, other): return self._apply_operator(operator.lt, other) def __lshift__(self, other): return self._apply_operator(operator.lshift, other) def __matmul__(self, other): return self._apply_operator(operator.matmul, other) def __mod__(self, other): return self._apply_operator(operator.mod, other) def __mul__(self, other): return self._apply_operator(operator.mul, other) def __ne__(self, other): return self._apply_operator(operator.ne, other) def __or__(self, other): return self._apply_operator(operator.or_, other) def __rshift__(self, other): return self._apply_operator(operator.rshift, other) def __pow__(self, other): return self._apply_operator(operator.pow, other) def __sub__(self, other): return self._apply_operator(operator.sub, other) def __truediv__(self, other): return self._apply_operator(operator.truediv, other) def __xor__(self, other): return self._apply_operator(operator.xor, other) # Reverse binary operators def __radd__(self, other): return self._apply_operator(operator.add, other, reverse=True) def __rand__(self, other): return self._apply_operator(operator.and_, other, reverse=True) def __rdiv__(self, other): return self._apply_operator(operator.div, other, reverse=True) def __rdivmod__(self, other): return self._apply_operator(divmod, other, reverse=True) def __rfloordiv__(self, other): return self._apply_operator(operator.floordiv, other, reverse=True) def __rlshift__(self, other): return self._apply_operator(operator.rlshift, other) def __rmod__(self, other): return self._apply_operator(operator.mod, other, reverse=True) def __rmul__(self, other): return self._apply_operator(operator.mul, other, reverse=True) def __ror__(self, other): return self._apply_operator(operator.or_, other, reverse=True) def __rpow__(self, other): return self._apply_operator(operator.pow, other, reverse=True) def __rrshift__(self, other): return self._apply_operator(operator.rrshift, other) def __rsub__(self, other): return self._apply_operator(operator.sub, other, reverse=True) def __rtruediv__(self, other): return self._apply_operator(operator.truediv, other, reverse=True) def __rxor__(self, other): return self._apply_operator(operator.xor, other, reverse=True) def __getitem__(self, other): return self._apply_operator(operator.getitem, other) def __iter__(self): if isinstance(self._current, Iterator): while True: try: new = self._apply_operator(next) new.rx.value except RuntimeError: break yield new return elif not isinstance(self._current, Iterable): raise TypeError(f'cannot unpack non-iterable {type(self._current).__name__} object.') items = self._apply_operator(list) for i in range(len(self._current)): yield items[i] def _eval_operation(self, obj, operation): fn, args, kwargs = operation['fn'], operation['args'], operation['kwargs'] resolved_args = [] for arg in args: val = resolve_value(arg) if val is Skip or val is Undefined: raise Skip resolved_args.append(val) resolved_kwargs = {} for k, arg in kwargs.items(): val = resolve_value(arg) if val is Skip or val is Undefined: raise Skip resolved_kwargs[k] = val if isinstance(fn, str): obj = getattr(obj, fn)(*resolved_args, **resolved_kwargs) elif operation.get('reverse'): obj = fn(resolved_args[0], obj, *resolved_args[1:], **resolved_kwargs) else: obj = fn(obj, *resolved_args, **resolved_kwargs) return obj def _rx_transform(obj): if not isinstance(obj, rx): return obj return bind(lambda *_: obj.rx.value, *obj._params) register_reference_transform(_rx_transform) param-2.1.1/param/serializer.py000066400000000000000000000264121463636336300164600ustar00rootroot00000000000000""" 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: """ 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-2.1.1/param/version.py000066400000000000000000000746011463636336300157770ustar00rootroot00000000000000""" 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): kwargs = {} if os.name == 'nt': kwargs['creationflags'] = subprocess.CREATE_NO_WINDOW proc = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=cwd, **kwargs) 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: """ 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().__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 OSError: 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 OSError: 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) 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' or 'main' and has a dot in it if tag not in ['', 'master', 'main'] 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: """ 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 OSError: 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 '{}-{}-g{}{}'.format(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-2.1.1/pyproject.toml000066400000000000000000000113461463636336300155510ustar00rootroot00000000000000[build-system] requires = ["hatchling", "hatch-vcs"] build-backend = "hatchling.build" [project] name = "param" dynamic = ["version"] description = "Make your Python code clearer and more reliable by declaring Parameters." readme = "README.md" license = { text = "BSD-3-Clause" } requires-python = ">=3.8" authors = [ { name = "HoloViz", email = "developers@holoviz.org" }, ] maintainers = [ { name = "HoloViz", email = "developers@holoviz.org" }, ] classifiers = [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "Intended Audience :: Science/Research", "License :: OSI Approved :: BSD License", "Natural Language :: English", "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Topic :: Scientific/Engineering", "Topic :: Software Development :: Libraries", ] dependencies = [] [project.optional-dependencies] examples = [ "aiohttp", "pandas", "panel", ] doc = [ "param[examples]", "nbsite ==0.8.4", "sphinx-remove-toctrees", ] tests = [ "coverage[toml]", "pytest", "pytest-asyncio", ] tests-deser = [ "xlrd", "openpyxl", "odfpy", "pyarrow", "tables", ] tests-examples = [ "pytest", "pytest-asyncio", "pytest-xdist", "nbval", "param[examples]", ] tests-full = [ "param[tests]", "param[tests-examples]", "param[tests-deser]", "numpy", "pandas", "ipython", "jsonschema", "gmpy", "cloudpickle", "nest_asyncio", ] lint = [ "flake8", "pre-commit", ] all = [ "param[tests-full]", "param[doc]", "param[lint]", ] [project.urls] Homepage = "https://param.holoviz.org/" Tracker = "https://github.com/holoviz/param/issues" Releases = "https://github.com/holoviz/param/releases" Source = "https://github.com/holoviz/param" HoloViz = "https://holoviz.org/" [tool.hatch.version] source = "vcs" [tool.hatch.build.targets.wheel] include = [ "/param", "/numbergen", ] [tool.hatch.build.targets.sdist] include = [ "/param", "/numbergen", "/tests", ] [tool.hatch.build.hooks.vcs] version-file = "param/_version.py" [tool.hatch.envs.default] dependencies = [ # Linters "param[lint]", # Base tests dependencies "param[tests]", # Examples tests dependencies "param[tests-examples]", # Deserializatoin dependencies "param[tests-deser]", # Additional tests dependencies, not including gmpy as # it's tricky to install. "ipython", "jsonschema", "numpy", "pandas", "cloudpickle", "nest_asyncio", # To keep __version__ up-to-date in editable installs "setuptools_scm", ] post-install-commands = [ "python -m pip install pre-commit", "pre-commit install", ] [tool.hatch.envs.default.scripts] tests = "pytest {args:tests}" examples = "pytest -n logical --dist loadscope --nbval-lax {args:doc}" [tool.hatch.envs.docs] template = "docs" features = ["doc"] python = "3.9" [tool.hatch.envs.docs.scripts] build = [ "sphinx-build -b html doc builtdocs", ] [tool.hatch.envs.tests] template = "tests" dependencies = [ "param[tests]", "ipython", "jsonschema", ] [[tool.hatch.envs.tests.matrix]] python = [ "3.8", "3.9", "3.10", "3.11", "3.12", "pypy3.9", ] [tool.hatch.envs.tests.scripts] tests = "pytest {args:tests}" with_coverage = [ "coverage run --source=numbergen,param -m pytest -v {args:tests}", "coverage report", "coverage xml", ] [tool.hatch.envs.tests.overrides] # Only install these on non PyPy environments name."^(?!pypy).*".dependencies = [ "numpy", "pandas", "xlrd", "openpyxl", "odfpy", "pyarrow", "cloudpickle", "nest_asyncio", ] # Only install gmpy on Linux on these version # Only install tables (deser HDF5) on Linux on these version matrix.python.dependencies = [ { value = "gmpy", if = ["3.8", "3.9", "3.10"], platform = ["linux"] }, { value = "tables", if = ["3.8", "3.9", "3.10", "3.11", "3.12"], platform = ["linux"] }, ] [tool.hatch.envs.tests_examples] template = "tests_examples" dependencies = [ "param[tests-examples]", "ipython", "jsonschema", ] [[tool.hatch.envs.tests_examples.matrix]] python = [ "3.8", "3.9", "3.10", "3.11", "3.12", ] [tool.hatch.envs.tests_examples.scripts] examples = "pytest -v -n logical --dist loadscope --nbval-lax {args:doc}" [tool.pytest.ini_options] python_files = "test*.py" filterwarnings = [ "error", ] xfail_strict = "true" asyncio_mode = "auto" [tool.coverage.report] omit = ["param/version.py"] param-2.1.1/tests/000077500000000000000000000000001463636336300137725ustar00rootroot00000000000000param-2.1.1/tests/__init__.py000066400000000000000000000000001463636336300160710ustar00rootroot00000000000000param-2.1.1/tests/conftest.py000066400000000000000000000004711463636336300161730ustar00rootroot00000000000000import param import pytest param.parameterized.warnings_as_exceptions = True @pytest.fixture def dataframe(): import pandas as pd return pd.DataFrame({ 'int': [1, 2, 3], 'float': [3.14, 6.28, 9.42], 'str': ['A', 'B', 'C'] }, index=[1, 2, 3], columns=['int', 'float', 'str']) param-2.1.1/tests/testaddparameter.py000066400000000000000000000100071463636336300176730ustar00rootroot00000000000000""" At the time of adding these tests, the implementation of add_parameter had not changed since it was committed (f10b324 in July 2012). The tests are checking that (not fully understood) implementation: @classmethod def _add_parameter(cls, param_name,param_obj): ''' Add a new Parameter object into this object's class. Supposed to result in a Parameter equivalent to one declared in the class's source code. ''' # CEBALERT: can't we just do # setattr(cls,param_name,param_obj)? The metaclass's # __setattr__ is actually written to handle that. (Would also # need to do something about the params() cache. That cache # is a pain, but it definitely improved the startup time; it # would be worthwhile making sure no method except for one # "add_param()" method has to deal with it (plus any future # remove_param() method.) type.__setattr__(cls,param_name,param_obj) cls.__metaclass__._initialize_parameter(cls,param_name,param_obj) # delete cached params() try: delattr(cls,'_%s__params'%cls.__name__) except AttributeError: pass """ import param import pytest def test_add_parameter_class(): class P(param.Parameterized): x = param.Parameter() P.param.add_parameter('y', param.Parameter()) assert 'y' in P.param # Check the name is set assert P.param.y.name == 'y' def test_add_parameter_instance(): class P(param.Parameterized): x = param.Parameter() p = P() p.param.add_parameter('y', param.Parameter()) assert 'y' in p.param assert p.param.y.name == 'y' def test_add_parameter_class_validation(): class P(param.Parameterized): x = param.Parameter() P.param.add_parameter('y', param.Number()) with pytest.raises(ValueError, match=r"Number parameter 'P.y' only takes numeric values, not ."): P.y = 'test' def test_add_parameter_instance_validation(): class P(param.Parameterized): x = param.Parameter() P.param.add_parameter('y', param.Number()) p = P() with pytest.raises(ValueError, match=r"Number parameter 'P.y' only takes numeric values, not ."): p.y = 'test' def test_add_parameter_cache_cleared(): # Not sure why it's supposed to delete the Parameters cache, test it anyway class P(param.Parameterized): x = param.Parameter() # Generate the cache P.param.objects(instance=True) assert 'x' in P._param__private.params P.param.add_parameter('y', param.Parameter()) # Check the cache has been removed (not sure why) assert not P._param__private.params def test_add_parameter_subclass(): class A(param.Parameterized): x = param.Parameter() class B(A): pass B.param.add_parameter('y', param.Parameter()) assert 'y' not in A.param assert 'y' in B.param def test_add_parameter_override(): class P(param.Parameterized): x = param.Parameter(default=1) origin = P.param.x new = param.Parameter(default=2) P.param.add_parameter('x', new) assert P.param.x.default == 2 assert P.param.x is new assert P.param.x is not origin def test_add_parameter_inheritance(): class A(param.Parameterized): x = param.Parameter(default=1) class B(A): pass B.param.add_parameter('x', param.Parameter(doc='some doc')) assert B.param.x.default == 1 assert B.param.x.doc == 'some doc' def test_add_parameter_watch_class(): class P(param.Parameterized): x = param.Parameter() P.param.add_parameter('y', param.Parameter()) acc = [] P.param.watch(lambda e: acc.append(e), 'y') P.y = 1 assert len(acc) == 1 def test_add_parameter_watch_instance(): class P(param.Parameterized): x = param.Parameter() P.param.add_parameter('y', param.Parameter()) p = P() acc = [] p.param.watch(lambda e: acc.append(e), 'y') p.y = 1 assert len(acc) == 1 param-2.1.1/tests/testbind.py000066400000000000000000000146111463636336300161630ustar00rootroot00000000000000import operator from param import Callable, Integer, Number, Parameterized, String, bind def identity(*args, **kwargs): return args, kwargs class Parameters(Parameterized): string = String(default="string") integer = Integer(default=7) number = Number(default=3.14) function = Callable() def test_bind_constant_arg(): assert bind(identity, 1)() == ((1,), {}) def test_bind_constant_arg_partial(): assert bind(identity, 1)(2) == ((1, 2), {}) def test_bind_constant_args(): assert bind(identity, 1, 2)() == ((1, 2), {}) def test_bind_constant_kwarg(): assert bind(identity, foo=1)() == ((), {'foo': 1}) def test_bind_constant_kwarg_partial(): assert bind(identity, foo=1)(bar=2) == ((), {'foo': 1, 'bar': 2}) def test_bind_constant_kwargs(): assert bind(identity, foo=1, bar=2)() == ((), {'foo': 1, 'bar': 2}) def test_bind_constant_args_and_kwargs(): assert bind(identity, 1, bar=2)() == ((1,), {'bar': 2}) def test_bind_constant_args_and_kwargs_partial(): assert bind(identity, 1, bar=3)(2, baz=4) == ((1, 2), {'bar': 3, 'baz': 4}) def test_curry_bind_args(): assert bind(bind(identity, 1), 2)() == ((1, 2), {}) def test_curry_bind_kwargs(): assert bind(bind(identity, foo=1), bar=2)() == ((), {'foo': 1, 'bar': 2}) def test_bind_class_param_as_arg(): bound_fn = bind(identity, Parameters.param.string) assert bound_fn() == (('string',), {}) def test_bind_class_params_as_args(): bound_fn = bind(identity, Parameters.param.string, Parameters.param.number) assert bound_fn() == (('string', 3.14), {}) def test_bind_class_param_as_kwarg(): bound_fn = bind(identity, string=Parameters.param.string) assert bound_fn() == ((), {'string': 'string'}) def test_bind_class_params_as_kwargs(): bound_fn = bind( identity, string=Parameters.param.string, num=Parameters.param.number ) assert bound_fn() == ((), {'string': 'string', 'num': 3.14}) def test_bind_class_params_as_args_and_kwargs(): bound_fn = bind( identity, Parameters.param.string, num=Parameters.param.number ) assert bound_fn() == (('string',), {'num': 3.14}) def test_bind_instance_param_as_arg(): P = Parameters() bound_fn = bind(identity, P.param.string) assert bound_fn() == (('string',), {}) P.string = 'baz' assert bound_fn() == (('baz',), {}) def test_bind_instance_params_as_args(): P = Parameters() bound_fn = bind(identity, P.param.string, P.param.number) assert bound_fn() == (('string', 3.14), {}) P.string = 'baz' assert bound_fn() == (('baz', 3.14), {}) P.number = 6.28 assert bound_fn() == (('baz', 6.28), {}) def test_bind_instance_params_and_constant_as_args(): P = Parameters() bound_fn = bind(identity, P.param.string, 'foo', P.param.number) assert bound_fn() == (('string', 'foo', 3.14), {}) P.string = 'baz' assert bound_fn() == (('baz', 'foo', 3.14), {}) P.number = 6.28 assert bound_fn() == (('baz', 'foo', 6.28), {}) def test_bind_instance_param_as_kwarg(): P = Parameters() bound_fn = bind(identity, string=P.param.string) assert bound_fn() == ((), {'string': 'string'}) P.string = 'baz' assert bound_fn() == ((), {'string': 'baz'}) def test_bind_instance_params_as_kwargs(): P = Parameters() bound_fn = bind( identity, string=P.param.string, num=P.param.number ) assert bound_fn() == ((), {'string': 'string', 'num': 3.14}) P.string = 'baz' assert bound_fn() == ((), {'string': 'baz', 'num': 3.14}) P.number = 6.28 assert bound_fn() == ((), {'string': 'baz', 'num': 6.28}) def test_bind_instance_params_and_constant_as_kwargs(): P = Parameters() bound_fn = bind( identity, string=P.param.string, num=P.param.number, foo='bar' ) assert bound_fn() == ((), {'string': 'string', 'num': 3.14, 'foo': 'bar'}) P.string = 'baz' assert bound_fn() == ((), {'string': 'baz', 'num': 3.14, 'foo': 'bar'}) P.number = 6.28 assert bound_fn() == ((), {'string': 'baz', 'num': 6.28, 'foo': 'bar'}) def test_bind_instance_params_as_args_and_kwargs(): P = Parameters() bound_fn = bind( identity, P.param.string, num=P.param.number ) assert bound_fn() == (('string',), {'num': 3.14}) P.string = 'baz' assert bound_fn() == (('baz',), {'num': 3.14}) P.number = 6.28 assert bound_fn() == (('baz',), {'num': 6.28}) def test_bind_instance_params_and_constants_as_args_and_kwargs(): P = Parameters() bound_fn = bind( identity, 'foo', P.param.string, num=P.param.number, bar=6 ) assert bound_fn() == (('foo', 'string',), {'num': 3.14, 'bar': 6}) P.string = 'baz' assert bound_fn() == (('foo', 'baz',), {'num': 3.14, 'bar': 6}) P.number = 6.28 assert bound_fn() == (('foo', 'baz',), {'num': 6.28, 'bar': 6}) def test_bind_curry_function_with_deps(): P = Parameters() bound_fn = bind( identity, P.param.string, num=P.param.number ) curried_fn = bind(bound_fn, const=3) assert curried_fn() == (('string',), {'num': 3.14, 'const': 3}) P.string = 'baz' assert curried_fn() == (('baz',), {'num': 3.14, 'const': 3}) P.number = 6.28 assert curried_fn() == (('baz',), {'num': 6.28, 'const': 3}) def test_bind_bound_function_to_arg(): P = Parameters(integer=1) def add1(value): return value + 1 def divide(value): return value / 2 bound_function = bind(divide, bind(add1, P.param.integer)) assert bound_function() == 1 P.integer = 3 assert bound_function() == 2 def test_bind_bound_function_to_kwarg(): P = Parameters(integer=1) def add1(value): return value + 1 def divide(divisor=2, value=0): return value / divisor bound_function = bind(divide, value=bind(add1, P.param.integer)) assert bound_function() == 1 P.integer = 3 assert bound_function() == 2 def test_bind_dynamic_function(): P = Parameters(function=operator.add) bound_function = bind(P.param.function, 3, 2) assert bound_function() == 5 P.function = operator.sub assert bound_function() == 1 def test_bind_generator(): P = Parameters(integer=7) def gen(arg): yield arg yield arg + 1 yield arg + 2 bound_function = bind(gen, P.param.integer) for i, v in enumerate(bound_function()): assert v == (7 + i) P.integer = 3 for i, v in enumerate(bound_function()): assert v == (3 + i) param-2.1.1/tests/testbooleanparam.py000066400000000000000000000075361463636336300177170ustar00rootroot00000000000000""" Unit test for Boolean parameters. """ import unittest import param from .utils import check_defaults class TestBooleanParameters(unittest.TestCase): def setUp(self): super().setUp() class P(param.Parameterized): e = param.Boolean() f = param.Boolean(default=None) self.P = P def _check_defaults(self, p): assert p.default is False assert p.allow_None is False def test_defaults_class(self): class A(param.Parameterized): b = param.Boolean() check_defaults(A.param.b, label='B') self._check_defaults(A.param.b) def test_defaults_inst(self): class A(param.Parameterized): b = param.Boolean() a = A() check_defaults(a.param.b, label='B') self._check_defaults(a.param.b) def test_defaults_unbound(self): b = param.Boolean() check_defaults(b, label=None) self._check_defaults(b) def test_default_is_None(self): p = self.P() assert p.f is None assert p.param.f.allow_None is True p.f = True p.f = None assert p.f is None def test_raise_None_when_not_allowed(self): p = self.P() msg = r"Boolean parameter 'P.e' must be True or False, not None" with self.assertRaisesRegex(ValueError, msg): p.e = None with self.assertRaisesRegex(ValueError, msg): self.P.e = None def test_bad_type(self): msg = r"Boolean parameter 'P.e' must be True or False, not 'test'" with self.assertRaisesRegex(ValueError, msg): self.P.e = 'test' with self.assertRaisesRegex(ValueError, msg): self.P(e='test') p = self.P() with self.assertRaisesRegex(ValueError, msg): p.e = 'test' def test_bad_default_type(self): msg = r"Boolean parameter 'b' must be True or False, not 'test'\." with self.assertRaisesRegex(ValueError, msg): class A(param.Parameterized): b = param.Boolean(default='test') class TestEventParameters(unittest.TestCase): def setUp(self): super().setUp() class P(param.Parameterized): e = param.Event() f = param.Event(default=None) self.P = P def _check_defaults(self, p): assert p.default is False assert p.allow_None is False def test_defaults_class(self): class A(param.Parameterized): b = param.Event() check_defaults(A.param.b, label='B') self._check_defaults(A.param.b) def test_defaults_inst(self): class A(param.Parameterized): b = param.Event() a = A() check_defaults(a.param.b, label='B') self._check_defaults(a.param.b) def test_defaults_unbound(self): b = param.Event() check_defaults(b, label=None) self._check_defaults(b) def test_resets_to_false(self): p = self.P() p.e = True assert p.e is False def test_default_is_None(self): p = self.P() assert p.f is None assert p.param.f.allow_None is True p.f = None assert p.f is False def test_raise_None_when_not_allowed(self): p = self.P() msg = r"Event parameter 'P.e' must be True or False, not None" with self.assertRaisesRegex(ValueError, msg): p.e = None with self.assertRaisesRegex(ValueError, msg): self.P.e = None def test_bad_type(self): msg = r"Event parameter 'P.e' must be True or False, not 'test'" with self.assertRaisesRegex(ValueError, msg): self.P.e = 'test' with self.assertRaisesRegex(ValueError, msg): self.P(e='test') p = self.P() with self.assertRaisesRegex(ValueError, msg): p.e = 'test' param-2.1.1/tests/testbytesparam.py000066400000000000000000000054141463636336300174170ustar00rootroot00000000000000""" Unit test for Bytes parameters """ import unittest import pytest from .utils import check_defaults import param ip_regex = br'^((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 TestBytesParameters(unittest.TestCase): def _check_defaults(self, p): assert p.default == b'' assert p.allow_None is False assert p.regex is None def test_defaults_class(self): class A(param.Parameterized): b = param.Bytes() check_defaults(A.param.b, label='B') self._check_defaults(A.param.b) def test_defaults_inst(self): class A(param.Parameterized): b = param.Bytes() a = A() check_defaults(a.param.b, label='B') self._check_defaults(a.param.b) def test_defaults_unbound(self): b = param.Bytes() check_defaults(b, label=None) self._check_defaults(b) def test_bytes_default_type(self): with pytest.raises(ValueError): class A(param.Parameterized): s = param.Bytes('abc') def test_bytes_value_type(self): class A(param.Parameterized): s = param.Bytes() with pytest.raises(ValueError): A(s='abc') def test_regex_ok(self): class A(param.Parameterized): s = param.Bytes(b'0.0.0.0', regex=ip_regex) a = A() a.s = b'123.123.0.1' def test_reject_none(self): class A(param.Parameterized): s = param.Bytes(b'0.0.0.0', regex=ip_regex) a = A() exception = "Bytes parameter 'A.s' only takes a byte string value, not value of ." 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.Bytes(None, regex=ip_regex) a = A() a.s = b'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.Bytes(b'0.0.0.0', regex=ip_regex) a = A() exception = "Bytes parameter 'A.s' value b'123.123.0.256' does not match regex %r." % ip_regex with self.assertRaises(ValueError) as e: a.s = b'123.123.0.256' self.assertEqual(str(e.exception), exception) def test_regex_incorrect_default(self): exception = f"Bytes parameter 's' value b'' does not match regex {ip_regex!r}." with self.assertRaises(ValueError) as e: class A(param.Parameterized): s = param.Bytes(regex=ip_regex) # default value '' does not match regular expression self.assertEqual(str(e.exception), exception) param-2.1.1/tests/testcalendardateparam.py000066400000000000000000000056101463636336300206760ustar00rootroot00000000000000""" Unit test for CalendarDate parameters. """ import datetime as dt import re import unittest import pytest import param from .utils import check_defaults class TestDateTimeParameters(unittest.TestCase): def _check_defaults(self, p): assert p.default is None assert p.allow_None is True assert p.bounds is None assert p.softbounds is None assert p.inclusive_bounds == (True, True) assert p.step is None def test_defaults_class(self): class A(param.Parameterized): d = param.CalendarDate() check_defaults(A.param.d, label='D') self._check_defaults(A.param.d) def test_defaults_inst(self): class A(param.Parameterized): d = param.CalendarDate() a = A() check_defaults(a.param.d, label='D') self._check_defaults(a.param.d) def test_defaults_unbound(self): d = param.CalendarDate() check_defaults(d, label=None) self._check_defaults(d) def test_initialization_out_of_bounds(self): with pytest.raises(ValueError): class Q(param.Parameterized): q = param.CalendarDate(dt.date(2017,2,27), bounds=(dt.date(2017,2,1), dt.date(2017,2,26))) 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))) with pytest.raises(ValueError): Q.q = dt.date(2017,2,27) 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)) with pytest.raises(ValueError): Q.q = dt.date(2017,2,26) 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, match=re.escape('CalendarDate parameter only takes date types.')): param.CalendarDate(dt.datetime(2021, 8, 16, 10)) def test_step_invalid_type_parameter(self): with pytest.raises( ValueError, match=re.escape("Attribute 'step' of CalendarDate parameter can only be None or a date type, not .") ): param.CalendarDate(dt.date(2017,2,27), step=3.2) param-2.1.1/tests/testcalendardaterangeparam.py000066400000000000000000000102361463636336300217130ustar00rootroot00000000000000""" Unit tests for CalendarDateRange parameter. """ import datetime as dt import re import unittest import param import pytest # Assuming tests of range parameter cover most of what's needed to # test date range. class TestDateTimeRange(unittest.TestCase): def _check_defaults(self, p): assert p.default is None assert p.allow_None is True assert p.length == 2 assert p.bounds is None assert p.softbounds is None assert p.inclusive_bounds == (True, True) assert p.step is None def test_defaults_class(self): class P(param.Parameterized): r = param.CalendarDateRange() self._check_defaults(P.param.r) def test_defaults_inst(self): class P(param.Parameterized): r = param.CalendarDateRange() p = P() self._check_defaults(p.param.r) def test_defaults_unbound(self): r = param.CalendarDateRange() self._check_defaults(r) bad_range = (dt.date(2017,2,27),dt.date(2017,2,26)) def test_wrong_type_default(self): with pytest.raises( ValueError, match=re.escape("CalendarDateRange parameter 'a' only takes date types, not (1.0, 2.0).") ): class Q(param.Parameterized): a = param.CalendarDateRange(default=(1.0,2.0)) def test_wrong_type_init(self): class Q(param.Parameterized): a = param.CalendarDateRange() with pytest.raises( ValueError, match=re.escape("CalendarDateRange parameter 'Q.a' end date 2017-02-26 is before start date 2017-02-27.") ): Q(a=self.bad_range) def test_wrong_type_set(self): class Q(param.Parameterized): a = param.CalendarDateRange() q = Q() with pytest.raises( ValueError, match=re.escape("CalendarDateRange parameter 'Q.a' end date 2017-02-26 is before start date 2017-02-27.") ): q.a = self.bad_range def test_start_before_end_default(self): with pytest.raises( ValueError, match=re.escape("CalendarDateRange parameter 'a' end date 2017-02-26 is before start date 2017-02-27.") ): class Q(param.Parameterized): a = param.CalendarDateRange(default=self.bad_range) def test_start_before_end_init(self): class Q(param.Parameterized): a = param.CalendarDateRange() with pytest.raises( ValueError, match=re.escape("CalendarDateRange parameter 'Q.a' end date 2017-02-26 is before start date 2017-02-27.") ): Q(a=self.bad_range) def test_start_before_end_set(self): class Q(param.Parameterized): a = param.CalendarDateRange() q = Q() with pytest.raises( ValueError, match=re.escape("CalendarDateRange parameter 'Q.a' end date 2017-02-26 is before start date 2017-02-27.") ): q.a = self.bad_range def test_validate_bounds_wrong_type_lower(self): msg = re.escape("CalendarDateRange parameter lower bound can only be None or a date value, not .") with pytest.raises(ValueError, match=msg): param.CalendarDateRange(bounds=('a', dt.date(2017,2,27))) def test_validate_bounds_wrong_type_upper(self): msg = re.escape("CalendarDateRange parameter upper bound can only be None or a date value, not .") with pytest.raises(ValueError, match=msg): param.CalendarDateRange(bounds=(dt.date(2017,2,27), 'b')) def test_validate_softbounds_wrong_type_lower(self): msg = re.escape("CalendarDateRange parameter lower softbound can only be None or a date value, not .") with pytest.raises(ValueError, match=msg): param.CalendarDateRange(softbounds=('a', dt.date(2017,2,27))) def test_validate_softbounds_wrong_type_upper(self): msg = re.escape("CalendarDateRange parameter upper softbound can only be None or a date value, not .") with pytest.raises(ValueError, match=msg): param.CalendarDateRange(softbounds=(dt.date(2017,2,27), 'b')) param-2.1.1/tests/testcallable.py000066400000000000000000000003771463636336300170120ustar00rootroot00000000000000import param import pytest def test_callable_validate(): with pytest.raises( ValueError, match=r"Callable parameter 'c' only takes a callable object, not objects of \." ): c = param.Callable('wrong') # noqa param-2.1.1/tests/testclassselector.py000066400000000000000000000114531463636336300201160ustar00rootroot00000000000000""" Unit test for ClassSelector parameters. """ import unittest from numbers import Number import param from .utils import check_defaults class TestClassSelectorParameters(unittest.TestCase): def setUp(self): super().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 _check_defaults(self, p): assert p.default is None assert p.allow_None is True assert p.instantiate is True assert p.is_instance is True def test_defaults_class(self): class P(param.Parameterized): s = param.ClassSelector(class_=int) check_defaults(P.param.s, label='S', skip=['instantiate']) self._check_defaults(P.param.s) assert P.param.s.class_ is int def test_defaults_inst(self): class P(param.Parameterized): s = param.ClassSelector(class_=int) p = P() check_defaults(p.param.s, label='S', skip=['instantiate']) self._check_defaults(p.param.s) assert p.param.s.class_ is int def test_defaults_unbound(self): s = param.ClassSelector(class_=int) check_defaults(s, label=None, skip=['instantiate']) self._check_defaults(s) assert s.class_ is int 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 'P.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 'P.f' value must be a subclass of Number, not ." 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 'P.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 'P.h' value must be a subclass of \(int, str\), not ." with self.assertRaisesRegex(ValueError, exception): self.P(h=float) class TestDictParameters(unittest.TestCase): def _check_defaults(self, p): assert p.default is None assert p.allow_None is True assert p.instantiate is True assert p.is_instance is True assert p.class_ == dict def test_defaults_class(self): class P(param.Parameterized): s = param.Dict() check_defaults(P.param.s, label='S', skip=['instantiate']) self._check_defaults(P.param.s) def test_defaults_inst(self): class P(param.Parameterized): s = param.Dict() p = P() check_defaults(p.param.s, label='S', skip=['instantiate']) self._check_defaults(p.param.s) def test_defaults_unbound(self): s = param.Dict() check_defaults(s, label=None, skip=['instantiate']) self._check_defaults(s) 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 'Test.items' value must be an instance of dict, not 3." with self.assertRaisesRegex(ValueError, exception): test.items = 3 param-2.1.1/tests/testcolorparameter.py000066400000000000000000000060431463636336300202660ustar00rootroot00000000000000""" Unit test for Color parameters. """ import re import unittest from .utils import check_defaults import param import pytest class TestColorParameters(unittest.TestCase): def _check_defaults(self, p): assert p.default is None assert p.allow_None is True assert p.allow_named is True def test_defaults_class(self): class A(param.Parameterized): c = param.Color() check_defaults(A.param.c, label='C') self._check_defaults(A.param.c) def test_defaults_inst(self): class A(param.Parameterized): c = param.Color() a = A() check_defaults(a.param.c, label='C') self._check_defaults(a.param.c) def test_defaults_unbound(self): c = param.Color() check_defaults(c, label=None) self._check_defaults(c) def test_wrong_type(self): with pytest.raises( ValueError, match=re.escape("Color parameter 'q' expects a string value, not an object of .") ): q = param.Color(1) # noqa def test_initialization_invalid_string(self): with pytest.raises( ValueError, match=re.escape("Color parameter 'q' only accepts valid RGB hex codes, received 'red'."), ): class Q(param.Parameterized): q = param.Color('red', allow_named=False) def test_set_invalid_string(self): class Q(param.Parameterized): q = param.Color(allow_named=False) with pytest.raises( ValueError, match=re.escape("Color parameter 'Q.q' only accepts valid RGB hex codes, received 'red'."), ): Q.q = 'red' def test_set_invalid_named_color(self): class Q(param.Parameterized): q = param.Color(allow_named=True) with pytest.raises( ValueError, match=re.escape("Color parameter 'Q.q' only takes RGB hex codes or named colors, received 'razzmatazz'."), ): Q.q = 'razzmatazz' def test_invalid_long_hex(self): class Q(param.Parameterized): q = param.Color() with pytest.raises( ValueError, match=re.escape("Color parameter 'Q.q' only takes RGB hex codes or named colors, received '#gfffff'.") ): Q.q = '#gfffff' 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') def test_valid_named_color_mixed_case(self): class Q(param.Parameterized): q = param.Color(allow_named=True) Q.q = 'WhiteSmoke' self.assertEqual(Q.q, 'WhiteSmoke') param-2.1.1/tests/testcomparator.py000066400000000000000000000016121463636336300174130ustar00rootroot00000000000000import datetime import decimal 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() _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)}) @pytest.mark.parametrize('obj', _supported.values(), ids=_supported.keys()) def test_comparator_equal(obj): assert Comparator.is_equal(obj, obj) param-2.1.1/tests/testcompositeparams.py000066400000000000000000000070531463636336300204570ustar00rootroot00000000000000""" Unit test for composite parameters. Originally implemented as doctests in Topographica in the file testCompositeParameter.txt """ import unittest import param from .utils import check_defaults class TestCompositeParameters(unittest.TestCase): def setUp(self): super().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: "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 _check_defaults(self, p): assert p.default is None assert p.allow_None is True assert p.attribs == [] def test_defaults_class(self): class P(param.Parameterized): c = param.Composite() check_defaults(P.param.c, label='C') self._check_defaults(P.param.c) assert P.param.c.objtype is P def test_defaults_inst(self): class P(param.Parameterized): c = param.Composite() p = P() check_defaults(p.param.c, label='C') self._check_defaults(p.param.c) assert p.param.c.objtype is P def test_defaults_unbound(self): c = param.Composite() check_defaults(c, label=None) self._check_defaults(c) assert not hasattr(c, 'objtype') 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-2.1.1/tests/testcustomparam.py000066400000000000000000000064501463636336300176040ustar00rootroot00000000000000""" Unit tests for checking the API defended to create custom Parameters. """ import param import pytest from .utils import check_defaults @pytest.fixture def custom_parameter(): class CustomParameter(param.Parameter): def __init__(self, default='custom', **params): super().__init__(default=default, **params) return CustomParameter @pytest.fixture def custom_parameter_with_slot(): class CustomParameter(param.Parameter): __slots__ = ['foo', 'bar'] def __init__(self, default='custom', foo=True, bar=None, **params): super().__init__(default=default, **params) self.foo = foo self.bar = bar return CustomParameter def _check_simple_defaults(param): assert param.default == 'custom' assert param.allow_None is False def _check_slot_defaults(param): assert param.default == 'custom' assert param.allow_None is False assert param.foo is True assert param.bar is None def test_customparam_defaults_unbound(custom_parameter): c = custom_parameter() check_defaults(c, label=None) _check_simple_defaults(c) def test_customparam_defaults_class(custom_parameter): class P(param.Parameterized): c = custom_parameter() check_defaults(P.param.c, label='C') _check_simple_defaults(P.param.c) def test_customparam_defaults_inst(custom_parameter): class P(param.Parameterized): c = custom_parameter() p = P() check_defaults(p.param.c, label='C') _check_simple_defaults(p.param.c) def test_customparam_slot_defaults_unbound(custom_parameter_with_slot): c = custom_parameter_with_slot() check_defaults(c, label=None) _check_slot_defaults(c) def test_customparam_slot_defaults_class(custom_parameter_with_slot): class P(param.Parameterized): c = custom_parameter_with_slot() check_defaults(P.param.c, label='C') _check_slot_defaults(P.param.c) def test_customparam_slot_defaults_inst(custom_parameter_with_slot): class P(param.Parameterized): c = custom_parameter_with_slot() p = P() check_defaults(p.param.c, label='C') _check_slot_defaults(p.param.c) def test_customparam_inheritance(custom_parameter_with_slot): class A(param.Parameterized): c = param.Parameter(doc='foo') class B(A): c = custom_parameter_with_slot() assert B.param.c.doc == 'foo' assert B().param.c.doc == 'foo' def test_customparam_inheritance_override(custom_parameter_with_slot): class A(param.Parameterized): c = param.Parameter(doc='foo') class B(A): c = custom_parameter_with_slot(doc='bar') assert B.param.c.doc == 'bar' assert B().param.c.doc == 'bar' def test_inheritance_parameter_attribute_without_default(): class CustomParameter(param.Parameter): __slots__ = ['foo'] # foo has no default value defined in _slot_defaults def __init__(self, foo=param.Undefined, **params): super().__init__(**params) # To trigger Parameter.__getattribute__ self.foo = foo if self.foo == 'bar': pass with pytest.raises( KeyError, match="Slot 'foo' on unbound parameter 'CustomParameter' has no default value defined in `_slot_defaults`" ): CustomParameter() param-2.1.1/tests/testdateparam.py000066400000000000000000000116131463636336300172040ustar00rootroot00000000000000""" Unit test for Date parameters. """ import datetime as dt import json import re import unittest import param import pytest try: import numpy as np except: np = None from .utils import check_defaults class TestDateParameters(unittest.TestCase): def _check_defaults(self, p): assert p.default is None assert p.allow_None is True assert p.bounds is None assert p.softbounds is None assert p.inclusive_bounds == (True, True) assert p.step is None def test_defaults_class(self): class A(param.Parameterized): d = param.Date() check_defaults(A.param.d, label='D') self._check_defaults(A.param.d) def test_defaults_inst(self): class A(param.Parameterized): d = param.Date() a = A() check_defaults(a.param.d, label='D') self._check_defaults(a.param.d) def test_defaults_unbound(self): d = param.Date() check_defaults(d, label=None) self._check_defaults(d) def test_initialization_out_of_bounds(self): with pytest.raises( ValueError, match=re.escape("Date parameter 'q' must be at most 2017-02-26 00:00:00, not 2017-02-27 00:00:00.") ): class Q(param.Parameterized): q = param.Date(dt.datetime(2017,2,27), bounds=(dt.datetime(2017,2,1), dt.datetime(2017,2,26))) 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))) with pytest.raises( ValueError, match=re.escape("Date parameter 'Q.q' must be at most 2017-02-26 00:00:00, not 2017-02-27 00:00:00.") ): Q.q = dt.datetime(2017,2,27) 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)) with pytest.raises( ValueError, match=re.escape("Date parameter 'Q.q' must be less than 2017-02-26 00:00:00, not 2017-02-26 00:00:00.") ): Q.q = dt.datetime(2017,2,26) 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))) def test_wrong_type(self): with pytest.raises( ValueError, match=re.escape("Date parameter 'q' only takes datetime and date types, not .") ): q = param.Date('wrong') # noqa def test_step_invalid_type_datetime_parameter(self): exception = re.escape("Attribute 'step' of Date parameter can only be None, a datetime or date type, not .") with pytest.raises(ValueError, match=exception): param.Date(dt.datetime(2017,2,27), step=3.2) @pytest.mark.skipif(np is None, reason='NumPy is not available') def test_support_mixed_date_datetime_bounds(self): # No error when comparing date and Python and Numpy datetimes date_bounds = (dt.date(2020, 1, 1), dt.date(2025, 1, 1)) datetime_bounds = (dt.datetime(2020, 1, 1), dt.datetime(2025, 1, 1)) numpy_bounds = (np.datetime64('2020-01-01T00:00'), np.datetime64('2025-01-01T00:00')) date_val = dt.date(2021, 1, 1) datetime_val = dt.datetime(2021, 1, 1) numpy_val = np.datetime64('2021-01-01T00:00') class A(param.Parameterized): s = param.Date(default=datetime_val, bounds=date_bounds) t = param.Date(default=numpy_val, bounds=date_bounds) u = param.Date(default=date_val, bounds=datetime_bounds) v = param.Date(default=numpy_val, bounds=datetime_bounds) w = param.Date(default=date_val, bounds=numpy_bounds) x = param.Date(default=datetime_val, bounds=numpy_bounds) a = A() a.s = date_val a.s = datetime_val def test_date_serialization(): class User(param.Parameterized): A = param.Date(default=None) # Validate round is possible User.param.deserialize_parameters(User.param.serialize_parameters()) serialized_data = '{"name": "User", "A": null}' deserialized_data = {"name": "User", "A": None} assert serialized_data == json.dumps(deserialized_data) assert serialized_data == User.param.serialize_parameters() assert deserialized_data == User.param.deserialize_parameters(serialized_data) param-2.1.1/tests/testdaterangeparam.py000066400000000000000000000200571463636336300202230ustar00rootroot00000000000000""" Unit tests for DateRange parameter. """ import datetime as dt import re import unittest import param import pytest from .utils import check_defaults 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(unittest.TestCase): def _check_defaults(self, p): assert p.default is None assert p.allow_None is True assert p.length == 2 assert p.bounds is None assert p.softbounds is None assert p.inclusive_bounds == (True, True) assert p.step is None def test_defaults_class(self): class P(param.Parameterized): r = param.DateRange() check_defaults(P.param.r, label='R') self._check_defaults(P.param.r) def test_defaults_inst(self): class P(param.Parameterized): r = param.DateRange() p = P() check_defaults(p.param.r, label='R') self._check_defaults(p.param.r) def test_defaults_unbound(self): r = param.DateRange() check_defaults(r, label=None) self._check_defaults(r) bad_range = (dt.datetime(2017,2,27),dt.datetime(2017,2,26)) def test_wrong_type_default(self): with pytest.raises( ValueError, match=re.escape("DateRange parameter 'a' only takes a tuple value, not .") ): class Q(param.Parameterized): a = param.DateRange(default='wrong') def test_wrong_inner_type_default(self): with pytest.raises( ValueError, match=re.escape("DateRange parameter 'a' only takes date/datetime values, not .") ): 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=re.escape("DateRange parameter 'Q.a' only takes date/datetime values, not .") ): 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=re.escape("DateRange parameter 'Q.a' only takes date/datetime values, not .") ): q.a = (1.0, 2.0) def test_wrong_type_init(self): class Q(param.Parameterized): a = param.DateRange() with pytest.raises( ValueError, match=re.escape("DateRange parameter 'Q.a' only takes a tuple value, not .") ): Q(a='wrong') def test_wrong_type_set(self): class Q(param.Parameterized): a = param.DateRange() q = Q() with pytest.raises( ValueError, match=re.escape("DateRange parameter 'Q.a' only takes a tuple value, not .") ): q.a = 'wrong' def test_start_before_end_default(self): with pytest.raises( ValueError, match=re.escape("DateRange parameter 'a' 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=re.escape("DateRange parameter 'Q.a' 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=re.escape("DateRange parameter 'Q.a' 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_change_value_type(self): class DateSlider(param.Parameterized): date = param.DateRange( default=(dt.date(2021, 1, 1), dt.date(2024, 1, 1)), bounds=(dt.date(2020, 1, 1), dt.date(2025, 1, 1)), ) ds = DateSlider() # Change the value from date to datetime without erroring ds.date = (dt.datetime(2022, 1, 1), dt.datetime(2023, 1, 1)) @pytest.mark.skipif(np is None, reason='NumPy is not available') def test_support_mixed_date_datetime_bounds(self): # No error when comparing date and Python and Numpy datetimes date_bounds = (dt.date(2020, 1, 1), dt.date(2025, 1, 1)) datetime_bounds = (dt.datetime(2020, 1, 1), dt.datetime(2025, 1, 1)) numpy_bounds = (np.datetime64('2020-01-01T00:00'), np.datetime64('2025-01-01T00:00')) date_val = (dt.date(2021, 1, 1), dt.date(2024, 1, 1)) datetime_val = (dt.datetime(2021, 1, 1), dt.datetime(2024, 1, 1)) numpy_val = (np.datetime64('2021-01-01T00:00'), np.datetime64('2024-01-01T00:00')) class A(param.Parameterized): s = param.DateRange(default=datetime_val, bounds=date_bounds) t = param.DateRange(default=numpy_val, bounds=date_bounds) u = param.DateRange(default=date_val, bounds=datetime_bounds) v = param.DateRange(default=numpy_val, bounds=datetime_bounds) w = param.DateRange(default=date_val, bounds=numpy_bounds) x = param.DateRange(default=datetime_val, bounds=numpy_bounds) a = A() a.s = date_val a.s = datetime_val @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=re.escape("DateRange parameter 'a' 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'))) def test_validate_bounds_wrong_type_lower(self): msg = re.escape("DateRange parameter lower bound can only be None or a date/datetime value, not .") with pytest.raises(ValueError, match=msg): param.DateRange(bounds=('a', dt.datetime(2017,2,27))) def test_validate_bounds_wrong_type_upper(self): msg = re.escape("DateRange parameter upper bound can only be None or a date/datetime value, not .") with pytest.raises(ValueError, match=msg): param.DateRange(bounds=(dt.datetime(2017,2,27), 'b')) def test_validate_softbounds_wrong_type_lower(self): msg = re.escape("DateRange parameter lower softbound can only be None or a date/datetime value, not .") with pytest.raises(ValueError, match=msg): param.DateRange(softbounds=('a', dt.datetime(2017,2,27))) def test_validate_softbounds_wrong_type_upper(self): msg = re.escape("DateRange parameter upper softbound can only be None or a date/datetime value, not .") with pytest.raises(ValueError, match=msg): param.DateRange(softbounds=(dt.datetime(2017,2,27), 'b')) param-2.1.1/tests/testdefaults.py000066400000000000000000000136211463636336300170560ustar00rootroot00000000000000""" Do all subclasses of Parameter supply a valid default? """ import unittest import pytest import param from param import concrete_descendents, Parameter # import all parameter types from param import * # noqa from param import ClassSelector from .utils import check_defaults kw_args = { ClassSelector: dict(class_=object), } skip = [] 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_unbound(parameter): def test(self): # instantiate parameter with no default (but supply # any required args) p = parameter(**kw_args.get(parameter, {})) for slot in param.parameterized.get_all_slots(parameter): # Handled in a special way, skip it if parameter == param.Composite and slot == 'objtype': continue assert getattr(p, slot) is not param.Undefined return test def add_test_class(parameter): def test(self): # instantiate parameter with no default (but supply # any required args) class P(param.Parameterized): p = parameter(**kw_args.get(parameter, {})) for slot in param.parameterized.get_all_slots(parameter): # Handled in a special way, skip it if type(parameter) == param.Composite and slot == 'objtype': continue assert getattr(P.param.p, slot) is not param.Undefined # Handled in a special way, skip it if parameter == param.Composite: continue assert P.p == P.param.p.default return test def add_test_inst(parameter): def test(self): # instantiate parameter with no default (but supply # any required args) class P(param.Parameterized): p = parameter(**kw_args.get(parameter, {})) inst = P() for slot in param.parameterized.get_all_slots(parameter): # Handled in a special way, skip it if type(parameter) == param.Composite and slot == 'objtype': continue assert getattr(inst.param.p, slot) is not param.Undefined # Handled in a special way, skip it if parameter == param.Composite: continue assert inst.p == inst.param.p.default return test for p_name, p_type in concrete_descendents(Parameter).items(): dict_["test_default_of_unbound_%s"%p_name] = add_test_unbound(p_type) if p_name not in skip else test_skip dict_["test_default_of_class_%s"%p_name] = add_test_class(p_type) if p_name not in skip else test_skip dict_["test_default_of_inst_%s"%p_name] = add_test_inst(p_type) if p_name not in skip else test_skip return type.__new__(mcs, name, bases, dict_) class TestDefaults(unittest.TestCase, metaclass=DefaultsMetaclassTest): pass def test_defaults_parameter_inst(): class A(param.Parameterized): s = param.Parameter() a = A() check_defaults(a.param.s, label='S') assert a.param.s.default is None assert a.param.s.allow_None is True def test_defaults_parameter_class(): class A(param.Parameterized): s = param.Parameter() check_defaults(A.param.s, label='S') assert A.param.s.default is None assert A.param.s.allow_None is True def test_defaults_parameter_unbound(): s = param.Parameter() check_defaults(s, label=None) assert s.default is None assert s.allow_None is True def test_defaults_parameter_inst_allow_None(): class A(param.Parameterized): s1 = param.Parameter(default='not None') s2 = param.Parameter(default='not None', allow_None=False) s3 = param.Parameter(default='not None', allow_None=True) s4 = param.Parameter(default=None) s5 = param.Parameter(default=None, allow_None=False) s6 = param.Parameter(default=None, allow_None=True) a = A() assert a.param.s1.allow_None is False assert a.param.s2.allow_None is False assert a.param.s3.allow_None is True assert a.param.s4.allow_None is True assert a.param.s5.allow_None is True assert a.param.s6.allow_None is True def test_defaults_parameter_class_allow_None(): class A(param.Parameterized): s1 = param.Parameter(default='not None') s2 = param.Parameter(default='not None', allow_None=False) s3 = param.Parameter(default='not None', allow_None=True) s4 = param.Parameter(default=None) s5 = param.Parameter(default=None, allow_None=False) s6 = param.Parameter(default=None, allow_None=True) assert A.param.s1.allow_None is False assert A.param.s2.allow_None is False assert A.param.s3.allow_None is True assert A.param.s4.allow_None is True assert A.param.s5.allow_None is True assert A.param.s6.allow_None is True def test_defaults_parameter_unbound_allow_None(): s1 = param.Parameter(default='not None') s2 = param.Parameter(default='not None', allow_None=False) s3 = param.Parameter(default='not None', allow_None=True) s4 = param.Parameter(default=None) s5 = param.Parameter(default=None, allow_None=False) s6 = param.Parameter(default=None, allow_None=True) assert s1.allow_None is False assert s2.allow_None is False assert s3.allow_None is True assert s4.allow_None is True assert s5.allow_None is True assert s6.allow_None is True param-2.1.1/tests/testdeprecations.py000066400000000000000000000312251463636336300177270ustar00rootroot00000000000000""" Test deprecation warnings. """ import re import warnings import param import pytest @pytest.fixture(autouse=True) def specific_filter(): """ Used to make sure warnings are set up with the right stacklevel. """ with warnings.catch_warnings(): warnings.simplefilter('ignore') warnings.filterwarnings('error', module=__name__) yield class TestDeprecateParameter: def test_deprecate_posargs_Parameter(self): with pytest.raises(param._utils.ParamDeprecationWarning): param.Parameter(1, 'doc') def test_deprecate_List_class_(self): with pytest.raises(param._utils.ParamDeprecationWarning): param.List(class_=int) def test_deprecate_Number_set_hook(self): class P(param.Parameterized): n = param.Number(set_hook=lambda obj, val: val) p = P() with pytest.raises(param._utils.ParamDeprecationWarning): p.n = 1 class TestDeprecateInitModule: def test_deprecate_as_unicode(self): with pytest.raises(param._utils.ParamDeprecationWarning): param.as_unicode(1) def test_deprecate_is_ordered_dict(self): with pytest.raises(param._utils.ParamDeprecationWarning): param.is_ordered_dict(dict()) def test_deprecate_produce_value(self): with pytest.raises(param._utils.ParamDeprecationWarning): param.produce_value(1) def test_deprecate_hasbable(self): with pytest.raises(param._utils.ParamDeprecationWarning): param.hashable('s') def test_deprecate_named_objs(self): with pytest.raises(param._utils.ParamDeprecationWarning): param.named_objs(dict()) def test_deprecate_normalize_path(self): with pytest.raises(param._utils.ParamDeprecationWarning): param.normalize_path() def test_deprecate_abbreviate_path(self): with pytest.raises(param._utils.ParamDeprecationWarning): param.abbreviate_paths() class TestDeprecateParameterizedModule: def test_deprecate_overridable_property(self): with pytest.raises(param._utils.ParamDeprecationWarning): class Foo: def _x(self): pass x = param.parameterized.overridable_property(_x) def test_deprecate_batch_watch(self): class P(param.Parameterized): x = param.Parameter() p = P() with pytest.raises(param._utils.ParamDeprecationWarning): with param.parameterized.batch_watch(p): pass def test_deprecate_add_metaclass(self): class MC(type): pass with pytest.raises(param._utils.ParamDeprecationWarning): @param.parameterized.add_metaclass(MC) class Base: pass def test_deprecate_recursive_repr(self): with pytest.raises(param._utils.ParamDeprecationWarning): param.parameterized.recursive_repr(lambda: '') def test_deprecate_all_equal(self): with pytest.raises(param._utils.ParamDeprecationWarning): param.parameterized.all_equal(1, 1) def test_deprecate_param_watchers(self): with pytest.raises(param._utils.ParamFutureWarning): param.parameterized.Parameterized()._param_watchers def test_deprecate_param_watchers_setter(self): with pytest.raises(param._utils.ParamFutureWarning): param.parameterized.Parameterized()._param_watchers = {} def test_param_error_unsafe_ops_before_initialized(self): class P(param.Parameterized): x = param.Parameter() def __init__(self, **params): with pytest.raises( param._utils.ParamFutureWarning, match=re.escape( 'Looking up instance Parameter objects (`.param.objects()`) until ' 'the Parameterized instance has been fully initialized is deprecated and will raise an error in a future version. ' 'Ensure you have called `super().__init__(**params)` in your Parameterized ' 'constructor before trying to access instance Parameter objects, or ' 'looking up the class Parameter objects with `.param.objects(instance=False)` ' 'may be enough for your use case.', ) ): self.param.objects() with pytest.raises( param._utils.ParamFutureWarning, match=re.escape( 'Triggering watchers on a partially initialized Parameterized instance ' 'is deprecated and will raise an error in a future version. ' 'Ensure you have called super().__init__(**params) in ' 'the Parameterized instance constructor before trying to set up a watcher.', ) ): self.param.trigger('x') with pytest.raises( param._utils.ParamFutureWarning, match=re.escape( '(Un)registering a watcher on a partially initialized Parameterized instance ' 'is deprecated and will raise an error in a future version. Ensure ' 'you have called super().__init__(**) in the Parameterized instance ' 'constructor before trying to set up a watcher.', ) ): self.param.watch(print, 'x') self.param.objects(instance=False) super().__init__(**params) P() # Inheritance tests to be move to testparameterizedobject.py when warnings will be turned into errors def test_inheritance_with_incompatible_defaults(self): class A(param.Parameterized): p = param.String() class B(A): pass with pytest.raises( param._utils.ParamFutureWarning, match=re.escape( "Number parameter 'C.p' failed to validate its " "default value on class creation, this is going to raise " "an error in the future. The Parameter type changed between class 'C' " "and one of its parent classes (B, A) which made it invalid. " "Please fix the Parameter type." "\nValidation failed with:\nNumber parameter 'C.p' only takes numeric values, not ." ) ): class C(B): p = param.Number() def test_inheritance_default_validation_with_more_specific_type(self): class A(param.Parameterized): p = param.Tuple(default=('a', 'b')) class B(A): pass with pytest.raises( param._utils.ParamFutureWarning, match=re.escape( "NumericTuple parameter 'C.p' failed to validate its " "default value on class creation, this is going to raise " "an error in the future. The Parameter type changed between class 'C' " "and one of its parent classes (B, A) which made it invalid. " "Please fix the Parameter type." "\nValidation failed with:\nNumericTuple parameter 'C.p' only takes numeric values, not ." ) ): class C(B): p = param.NumericTuple() def test_inheritance_with_changing_bounds(self): class A(param.Parameterized): p = param.Number(default=5) class B(A): pass with pytest.raises( param._utils.ParamFutureWarning, match=re.escape( "Number parameter 'C.p' failed to validate its " "default value on class creation, this is going to raise " "an error in the future. The Parameter is defined with attributes " "which when combined with attributes inherited from its parent " "classes (B, A) make it invalid. Please fix the Parameter attributes." "\nValidation failed with:\nNumber parameter 'C.p' must be at most 3, not 5." ) ): class C(B): p = param.Number(bounds=(-1, 3)) def test_inheritance_with_changing_default(self): class A(param.Parameterized): p = param.Number(default=5, bounds=(3, 10)) class B(A): pass with pytest.raises( param._utils.ParamFutureWarning, match=re.escape( "Number parameter 'C.p' failed to validate its " "default value on class creation, this is going to raise " "an error in the future. The Parameter is defined with attributes " "which when combined with attributes inherited from its parent " "classes (B, A) make it invalid. Please fix the Parameter attributes." "\nValidation failed with:\nNumber parameter 'C.p' must be at least 3, not 1." ) ): class C(B): p = param.Number(default=1) def test_inheritance_with_changing_class_(self): class A(param.Parameterized): p = param.ClassSelector(class_=int, default=5) class B(A): pass with pytest.raises( param._utils.ParamFutureWarning, match=re.escape( "ClassSelector parameter 'C.p' failed to validate its " "default value on class creation, this is going to raise " "an error in the future. The Parameter is defined with attributes " "which when combined with attributes inherited from its parent " "classes (B, A) make it invalid. Please fix the Parameter attributes." "\nValidation failed with:\nClassSelector parameter 'C.p' value must be an instance of str, not 5." ) ): class C(B): p = param.ClassSelector(class_=str) class TestDeprecateParameters: def test_deprecate_print_param_defaults(self): class P(param.Parameterized): x = param.Parameter() p = P() with pytest.raises(param._utils.ParamDeprecationWarning): p.param.print_param_defaults() def test_deprecate_set_default(self): class P(param.Parameterized): x = param.Parameter() with pytest.raises(param._utils.ParamDeprecationWarning): P.param.set_default('x', 1) def test_deprecate__add_parameter(self): class P(param.Parameterized): x = param.Parameter() with pytest.raises(param._utils.ParamDeprecationWarning): P.param._add_parameter('y', param.Parameter()) def test_deprecate_params(self): class P(param.Parameterized): x = param.Parameter() p = P() with pytest.raises(param._utils.ParamDeprecationWarning): p.param.params() def test_deprecate_set_param(self): class P(param.Parameterized): x = param.Parameter() p = P() with pytest.raises(param._utils.ParamDeprecationWarning): p.param.set_param('x', 1) def test_deprecate_get_param_values(self): class P(param.Parameterized): x = param.Parameter() p = P() with pytest.raises(param._utils.ParamDeprecationWarning): p.param.get_param_values() def test_deprecate_params_depended_on(self): class P(param.Parameterized): x = param.Parameter() p = P() with pytest.raises(param._utils.ParamDeprecationWarning): p.param.params_depended_on() def test_deprecate_defaults(self): class P(param.Parameterized): x = param.Parameter() p = P() with pytest.raises(param._utils.ParamDeprecationWarning): p.param.defaults() def test_deprecate_print_param_values(self): class P(param.Parameterized): x = param.Parameter() p = P() with pytest.raises(param._utils.ParamDeprecationWarning): p.param.print_param_values() def test_deprecate_message(self): class P(param.Parameterized): x = param.Parameter() p = P() with pytest.raises(param._utils.ParamDeprecationWarning): p.param.message('test') def test_deprecate_verbose(self): class P(param.Parameterized): x = param.Parameter() p = P() with pytest.raises(param._utils.ParamDeprecationWarning): p.param.verbose('test') def test_deprecate_debug(self): class P(param.Parameterized): x = param.Parameter() p = P() with pytest.raises(param._utils.ParamDeprecationWarning): p.param.debug('test') param-2.1.1/tests/testdynamicparams.py000066400000000000000000000214641463636336300201030ustar00rootroot00000000000000""" 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): super().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['x']._value_is_dynamic(self.t1), True) def test_set_dynamic_time_fn_y(self): self.assertEqual( self.t1.param['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' in t9._param__private.values, 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['y'].default, True) self.assertEqual(self.TestPO2().param['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['y'].default, True) class TestDynamicTimeDependent(TestDynamicParameters): def setUp(self): super().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().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-2.1.1/tests/testfiledeserialization.py000066400000000000000000000152361463636336300213010ustar00rootroot00000000000000""" Test deserialization routines that read from file """ import unittest import param import sys from unittest import skipIf from tempfile import mkdtemp from shutil import rmtree try: import numpy as np ndarray = np.array([[1,2,3],[4,5,6]]) except: np = ndarray = None try: import pandas as pd pd_ver = pd.__version__.split('.') df = pd.DataFrame({'A':[1,2,3], 'B':[1.1,2.2,3.3]}) modern_pd = pd if (int(pd_ver[0]) >= 1 and int(pd_ver[1]) >= 2) else None except: pd = df1 = df2 = modern_pd = None # The writer could be xlsxwriter, but the sufficient condition is the presence of # openpyxl try: import openpyxl as xlsxm except: xlsxm = None try: import odf as ods except: ods = None # prior to pandas version 1.2, xlrd was always the default excel reader (though it # had to be be of a version before xlrd's 2.0). xls = None try: import xlrd as xls if int(xls.__version__.split('.')[0]) > 2: raise Exception() except: if modern_pd is None: xlsxm = None try: import pyarrow.feather as feather except: feather = None try: import fastparquet as parquet except: parquet = None try: import pyarrow as parquet except: pass try: import tables as hdf5 except: hdf5 = None np_skip = skipIf(np is None, "NumPy is not available") pd_skip = skipIf(pd is None, "pandas is not available") modern_pd_skip = skipIf(modern_pd is None, "pandas is too old") xlsxm_skip = skipIf(xlsxm is None, "openpyxl is not available") ods_skip = skipIf(ods is None, "odfpy is not available") xls_skip = skipIf(xls is None, "xlrd is not available") feather_skip = skipIf(feather is None, "pyarrow.feather is not available") parquet_skip = skipIf(parquet is None, "fastparquet and pyarrow are not available") hdf5_skip = skipIf(hdf5 is None, "pytables is not available") class P(param.Parameterized): array = None if np is None else param.Array(default=ndarray) data_frame = None if pd is None else param.DataFrame(default=df) class TestFileDeserialization(unittest.TestCase): def run(self, result): self.temp_dir = mkdtemp().replace('\\', '/') try: return super(TestFileDeserialization, self).run(result=result) finally: rmtree(self.temp_dir, ignore_errors=True) @np_skip def _test_deserialize_array(self, obj, path, pname, check=True): # assumes the parameter has already been serialized to path! deserialized = obj.param.deserialize_value( pname, '"{}"'.format(path), mode='json') if check: self.assertTrue(np.array_equal(deserialized, getattr(obj, pname))) @np_skip def test_fail_to_deserialize(self): path = '{}/val.foo'.format(self.temp_dir) with self.assertRaisesRegex(IOError, "does not exist or is not a file"): self._test_deserialize_array(P, path, 'array') with open(path, 'w'): pass with self.assertRaisesRegex(ValueError, "no deserialization method for files"): self._test_deserialize_array(P, path, 'array') path = '{}/val.npy'.format(self.temp_dir) with open(path, 'w'): pass with self.assertRaises(Exception): self._test_deserialize_array(P, path, 'array') @np_skip def test_array_npy(self): path = '{}/val.npy'.format(self.temp_dir) np.save(path, P.array) self._test_deserialize_array(P, path, 'array') @np_skip def test_array_txt(self): path = '{}/val.txt'.format(self.temp_dir) np.savetxt(path, P.array) self._test_deserialize_array(P, path, 'array') @np_skip def test_array_txt_gz(self): path = '{}/val.txt.gz'.format(self.temp_dir) np.savetxt(path, P.array) self._test_deserialize_array(P, path, 'array') @pd_skip def test_data_frame_pkl(self): path = '{}/val.pkl.zip'.format(self.temp_dir) P.data_frame.to_pickle(path) self._test_deserialize_array(P, path, 'data_frame') @pd_skip def test_data_frame_csv(self): path = '{}/val.csv.bz2'.format(self.temp_dir) P.data_frame.to_csv(path, index=False) self._test_deserialize_array(P, path, 'data_frame') @pd_skip def test_data_frame_tsv(self): path = '{}/val.tsv'.format(self.temp_dir) P.data_frame.to_csv(path, index=False, sep='\t') self._test_deserialize_array(P, path, 'data_frame') @pd_skip def test_data_frame_json(self): path = '{}/val.json'.format(self.temp_dir) P.data_frame.to_json(path) self._test_deserialize_array(P, path, 'data_frame') # FIXME(sdrobert): xls are old-style excel files. There are two distinct engines for # reading and writing these, and the writer engine is deprecated by pandas. We could # store the serialized file as a byte array to future-proof somewhat, but that would # break if we ever decided to change the default data_frame value. Who cares. @pd_skip @xlsxm_skip def test_data_frame_xlsm(self): path = '{}/val.xlsm'.format(self.temp_dir) P.data_frame.to_excel(path, index=False) self._test_deserialize_array(P, path, 'data_frame') @pd_skip @xlsxm_skip def test_data_frame_xlsx(self): path = '{}/val.xlsx'.format(self.temp_dir) P.data_frame.to_excel(path, index=False) self._test_deserialize_array(P, path, 'data_frame') @pd_skip @ods_skip @skipIf(sys.version_info[0] < 3, "py2k pandas does not support 'ods'") def test_data_frame_ods(self): path = '{}/val.ods'.format(self.temp_dir) P.data_frame.to_excel(path, index=False) self._test_deserialize_array(P, path, 'data_frame') @pd_skip @feather_skip def test_data_frame_feather(self): path = '{}/val.feather'.format(self.temp_dir) P.data_frame.to_feather(path) self._test_deserialize_array(P, path, 'data_frame') @pd_skip @parquet_skip def test_data_frame_parquet(self): path = '{}/val.parquet'.format(self.temp_dir) P.data_frame.to_parquet(path) self._test_deserialize_array(P, path, 'data_frame') @pd_skip def test_data_frame_stata(self): path = '{}/val.dta'.format(self.temp_dir) P.data_frame.to_stata(path, write_index=False) self._test_deserialize_array(P, path, 'data_frame') @pd_skip @hdf5_skip def test_data_frame_hdf5(self): path = '{}/val.h5'.format(self.temp_dir) P.data_frame.to_hdf(path, key='df') self._test_deserialize_array(P, path, 'data_frame') path = '{}/val.hdf5'.format(self.temp_dir) P.data_frame.to_hdf(path, key='df') self._test_deserialize_array(P, path, 'data_frame') param-2.1.1/tests/testfileselector.py000066400000000000000000000112531463636336300177260ustar00rootroot00000000000000import os import shutil import tempfile import unittest import param from .utils import check_defaults class TestFileSelectorParameters(unittest.TestCase): def setUp(self): super().setUp() tmpdir1 = tempfile.mkdtemp() fa = os.path.join(tmpdir1, 'a.txt') fb = os.path.join(tmpdir1, 'b.txt') glob1 = os.path.join(tmpdir1, '*') open(fa, 'w').close() open(fb, 'w').close() tmpdir2 = tempfile.mkdtemp() fc = os.path.join(tmpdir2, 'c.txt') fd = os.path.join(tmpdir2, 'd.txt') glob2 = os.path.join(tmpdir2, '*') open(fc, 'w').close() open(fd, 'w').close() self.tmpdir1 = tmpdir1 self.tmpdir2 = tmpdir2 self.fa = fa self.fb = fb self.fc = fc self.fd = fd self.glob1 = glob1 self.glob2 = glob2 class P(param.Parameterized): a = param.FileSelector(path=glob1) b = param.FileSelector(default=fa, path=glob1) self.P = P def tearDown(self): shutil.rmtree(self.tmpdir1) shutil.rmtree(self.tmpdir2) def _check_defaults(self, p): assert p.default is None assert p.allow_None is None assert p.objects == [] assert p.compute_default_fn is None assert p.check_on_set is False assert p.names == {} assert p.path == "" def test_defaults_class(self): class P(param.Parameterized): s = param.FileSelector() check_defaults(P.param.s, label='S') self._check_defaults(P.param.s) def test_defaults_inst(self): class P(param.Parameterized): s = param.FileSelector() p = P() check_defaults(p.param.s, label='S') self._check_defaults(p.param.s) def test_defaults_unbound(self): s = param.FileSelector() check_defaults(s, label=None) self._check_defaults(s) def test_default_to_first(self): p = self.P() assert p.a == p.param.a.objects[0] def test_default_is_honored(self): p = self.P() assert p.b == self.fa assert p.param.b.default in [self.fa, self.fb] def test_allow_default_None(self): class P(param.Parameterized): a = param.FileSelector(default=None) def test_default_not_in_glob(self): with self.assertRaises(ValueError): class P(param.Parameterized): a = param.FileSelector(default='not/in/glob', path=self.glob1) def test_objects_auto_set(self): p = self.P() assert p.param.a.objects == [self.fa, self.fb] def test_set_object_constructor(self): p = self.P(a=self.fb) assert p.a == self.fb def test_set_object_outside_bounds(self): p = self.P() with self.assertRaises(ValueError): p.a = '/not/in/glob' def test_set_path_and_update(self): p = self.P() p.param.b.path = self.glob2 p.param.b.update() assert p.param.b.objects == [self.fc, self.fd] assert p.param.b.default in [self.fc, self.fd] # Default updated but not the value itself assert p.b == self.fa def test_path_autoupdate(self): p = self.P() p.param.b.path = self.glob2 assert p.param.b.objects == [self.fc, self.fd] assert p.param.b.default in [self.fc, self.fd] # Default updated but not the value itself assert p.b == self.fa def test_get_range(self): p = self.P() r = p.param.a.get_range() assert r['a.txt'] == self.fa assert r['b.txt'] == self.fb p.param.a.path = self.glob2 p.param.a.update() r = p.param.a.get_range() assert r['c.txt'] == self.fc assert r['d.txt'] == self.fd def test_update_file_removed(self): p = self.P() assert p.param.b.objects == [self.fa, self.fb] assert p.param.b.default in [self.fa, self.fb] os.remove(self.fa) p.param.b.update() assert p.param.b.objects == [self.fb] assert p.param.b.default == self.fb def test_fileselector_glob_parent(tmpdir): # https://github.com/holoviz/param/issues/139 ncwd = tmpdir / 'folder' data = tmpdir / 'data' data.mkdir() (data / 'foo.txt').write_text('foo', encoding='utf-8') ncwd.mkdir() cwd = os.getcwd() os.chdir(ncwd) try: if os.name == 'nt': default = r'..\data\foo.txt' else: default = '../data/foo.txt' class P(param.Parameterized): fs = param.FileSelector(default, path='../data/*.txt') assert len(P.param.fs.objects) == 1 assert P.fs == default finally: os.chdir(cwd) param-2.1.1/tests/testipythonmagic.py000066400000000000000000000075521463636336300177500ustar00rootroot00000000000000""" 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.") 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): super().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-2.1.1/tests/testjsonserialization.py000066400000000000000000000317111463636336300210160ustar00rootroot00000000000000""" Testing JSON serialization of parameters and the corresponding schemas. """ import datetime import json import unittest from unittest import SkipTest, skipIf import param 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") 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], item_type=int) f = param.List([1,2,3]) g = param.Date(default=datetime.datetime.now()) g2 = None if np is None 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) )) ae = None if np is None else param.DateRange(default=(npdt1, npdt2)) af = None if pd is None else param.DateRange(default=(pdts1, pdts2)) test = TestSet(a=29) class TestSerialization(unittest.TestCase): """ 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') @np_skip def test_serialize_date_numpy_class(self): self._test_serialize(TestSet, 'g2') @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') @np_skip def test_serialize_datetime_range_numpy_class(self): self._test_serialize(TestSet, 'ae') @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(unittest.TestCase): 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-2.1.1/tests/testlist.py000066400000000000000000000176511463636336300162310ustar00rootroot00000000000000import re import unittest import param import pytest from .utils import check_defaults # 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(unittest.TestCase): def setUp(self): super().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)) m = param.List([1, 2, 3], bounds=(3, None)) n = param.List([1], bounds=(None, 3)) self.P = P def _check_defaults(self, p): assert p.default == [] assert p.allow_None is False assert p.class_ is None assert p.item_type is None assert p.bounds == (0, None) assert p.instantiate is True def test_defaults_class(self): class P(param.Parameterized): l = param.List() check_defaults(P.param.l, label='L', skip=['instantiate']) self._check_defaults(P.param.l) def test_defaults_inst(self): class P(param.Parameterized): l = param.List() p = P() check_defaults(p.param.l, label='L', skip=['instantiate']) self._check_defaults(p.param.l) def test_defaults_unbound(self): l = param.List() check_defaults(l, label=None, skip=['instantiate']) self._check_defaults(l) 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() with pytest.raises( ValueError, match=re.escape("List parameter 'P.l' length must be between 0 and 10 (inclusive), not 11.") ): p.l = [6] * 11 def test_set_object_outside_lower_bounds(self): p = self.P() with pytest.raises( ValueError, match=re.escape("List parameter 'P.m' length must be at least 3, not 2.") ): p.m = [6, 7] def test_set_object_outside_upper_bounds(self): p = self.P() with pytest.raises( ValueError, match=re.escape("List parameter 'P.n' length must be at most 3, not 4.") ): p.n = [6] * 4 def test_set_object_wrong_type(self): p = self.P() with pytest.raises( TypeError, match=re.escape("List parameter 'P.e' items must be instances of , not .") ): p.e=['s'] def test_set_object_not_None(self): p = self.P(e=[6]) with pytest.raises( ValueError, match=re.escape("List parameter 'P.e' must be a list, not an object of .") ): p.e = None def test_inheritance_behavior1(self): class A(param.Parameterized): p = param.List() class B(A): p = param.List() assert B.param.p.default == [] assert B.param.p.instantiate is True assert B.param.p.bounds == (0, None) b = B() assert b.param.p.default == [] assert b.param.p.instantiate is True assert b.param.p.bounds == (0, None) def test_inheritance_behavior2(self): class A(param.Parameterized): p = param.List(default=[0, 1]) class B(A): p = param.List() # B inherits default from A assert B.param.p.default == [0 ,1] assert B.param.p.instantiate is True assert B.param.p.bounds == (0, None) b = B() assert b.param.p.default == [0, 1] assert b.param.p.instantiate is True assert b.param.p.bounds == (0, None) def test_inheritance_behavior3(self): class A(param.Parameterized): p = param.List(default=[0, 1], bounds=(1, 10)) class B(A): p = param.List() # B inherits default and bounds from A assert B.param.p.default == [0, 1] assert B.param.p.instantiate is True assert B.param.p.bounds == (1, 10) b = B() assert b.param.p.default == [0, 1] assert b.param.p.instantiate is True assert b.param.p.bounds == (1, 10) def test_inheritance_behavior4(self): class A(param.Parameterized): p = param.List(default=[0], item_type=int) class B(A): p = param.List() # B inherit item_type assert B.param.p.default == [0] assert B.param.p.instantiate is True assert B.param.p.bounds == (0, None) assert B.param.p.item_type == int b = B() assert b.param.p.default == [0] assert b.param.p.instantiate is True assert b.param.p.bounds == (0, None) assert b.param.p.item_type == int def test_inheritance_behavior5(self): class A(param.Parameterized): p = param.List(default=[0, 1], allow_None=True) class B(A): p = param.List() # B does not inherit allow_None assert B.param.p.default == [0, 1] assert B.param.p.allow_None is False assert B.param.p.instantiate is True assert B.param.p.bounds == (0, None) b = B() assert b.param.p.default == [0, 1] assert b.param.p.allow_None is False assert b.param.p.instantiate is True assert b.param.p.bounds == (0, None) def test_inheritance_behavior6(self): class A(param.Parameterized): p = param.List(default=[0, 1], bounds=(1, 10)) class B(A): p = param.List(default=[0, 1, 2, 3]) assert B.param.p.default == [0, 1, 2, 3] assert B.param.p.instantiate is True assert B.param.p.bounds == (1, 10) b = B() assert b.param.p.default == [0, 1, 2, 3] assert b.param.p.instantiate is True assert b.param.p.bounds == (1, 10) class TestHookListParameters(unittest.TestCase): def setUp(self): super().setUp() class P(param.Parameterized): e = param.HookList([abs]) l = param.HookList(bounds=(0,10)) self.P = P def _check_defaults(self, p): assert p.default == [] assert p.allow_None is False assert p.class_ is None assert p.item_type is None assert p.bounds == (0, None) assert p.instantiate is True def test_defaults_class(self): class P(param.Parameterized): l = param.HookList() check_defaults(P.param.l, label='L', skip=['instantiate']) self._check_defaults(P.param.l) def test_defaults_inst(self): class P(param.Parameterized): l = param.HookList() p = P() check_defaults(p.param.l, label='L', skip=['instantiate']) self._check_defaults(p.param.l) def test_defaults_unbound(self): l = param.HookList() check_defaults(l, label=None, skip=['instantiate']) self._check_defaults(l) def test_default_None(self): class Q(param.Parameterized): r = param.HookList(default=[]) # Also check None) def test_set_object_constructor(self): p = self.P(e=[abs]) self.assertEqual(p.e, [abs]) def test_set_object_outside_bounds(self): p = self.P() with pytest.raises(ValueError): p.l = [abs] * 11 def test_set_object_wrong_type_foo(self): p = self.P() with pytest.raises( ValueError, match=re.escape("HookList parameter 'P.e' items must be callable, not 's'.") ): p.e = ['s'] def test_set_object_not_None(self): p = self.P() with pytest.raises(ValueError): p.e = None param-2.1.1/tests/testlistselector.py000066400000000000000000000154231463636336300177650ustar00rootroot00000000000000import re import unittest import param import pytest from .utils import check_defaults # 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): super().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) j = param.ListSelector(objects=[11], check_on_set=False, allow_None=True) self.P = P def _check_defaults(self, p): assert p.default is None assert p.allow_None is None assert p.objects == [] assert p.compute_default_fn is None assert p.check_on_set is False assert p.names == {} def test_defaults_class(self): class P(param.Parameterized): s = param.ListSelector() check_defaults(P.param.s, label='S') self._check_defaults(P.param.s) def test_defaults_inst(self): class P(param.Parameterized): s = param.ListSelector() p = P() check_defaults(p.param.s, label='S') self._check_defaults(p.param.s) def test_defaults_unbound(self): s = param.ListSelector() check_defaults(s, label=None) self._check_defaults(s) 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_allow_None_is_None(self): p = self.P() assert p.param.e.allow_None is None assert p.param.f.allow_None is None assert p.param.g.allow_None is None assert p.param.h.allow_None is None assert p.param.i.allow_None is None 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] assert p.f == [9] assert p.param.f.objects == [10, 9] assert self.P.param.f.objects == [10] p.g = [7] assert p.g == [7] assert p.param.g.objects == self.P.param.g.objects assert p.param.g.objects == [7, 8] p.i = [12] assert p.i == [12] assert p.param.i.objects == [9, 7, 12] assert self.P.param.i.objects == [9, 7] 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_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_one_object_allow_None_check_on_set(self): p = self.P(e=[6]) p.j = None assert p.j is None assert p.param.j.objects == [11] p.j = [12] assert p.j == [12] assert p.param.j.objects == [11, 12] 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.") def test_bad_default(self): with pytest.raises( ValueError, match=re.escape("ListSelector parameter 'r' only takes list types, not 6."), ): class Q(param.Parameterized): r = param.ListSelector(default=6,check_on_set=True) def test_implied_check_on_set(self): with pytest.raises( ValueError, match=re.escape("ListSelector parameter 'r' only takes list types, not 7."), ): 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]) def test_default_not_checked_to_be_iterable(self): with pytest.raises( ValueError, match=re.escape("ListSelector parameter 'r' only takes list types, not 6."), ): 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(ValueError): 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['r'].compute_default() self.assertEqual(Q.r, [1,2,3]) self.assertEqual(Q.param['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['r'].compute_default() def test_initialization_bad_iterable(self): with self.assertRaises(ValueError): class Q(param.Parameterized): j = param.ListSelector('ab', objects=['a', 'b', 'c', 'd']) def test_set_bad_iterable(self): class Q(param.Parameterized): r = param.ListSelector(objects=['a', 'b', 'c', 'd']) q = Q() with self.assertRaises(ValueError): q.r = 'ab' param-2.1.1/tests/testmultifileselector.py000066400000000000000000000104711463636336300210020ustar00rootroot00000000000000import os import shutil import tempfile import unittest import param from .utils import check_defaults class TestMultiFileSelectorParameters(unittest.TestCase): def setUp(self): super().setUp() tmpdir1 = tempfile.mkdtemp() fa = os.path.join(tmpdir1, 'a.txt') fb = os.path.join(tmpdir1, 'b.txt') glob1 = os.path.join(tmpdir1, '*') open(fa, 'w').close() open(fb, 'w').close() tmpdir2 = tempfile.mkdtemp() fc = os.path.join(tmpdir2, 'c.txt') fd = os.path.join(tmpdir2, 'd.txt') glob2 = os.path.join(tmpdir2, '*') open(fc, 'w').close() open(fd, 'w').close() self.tmpdir1 = tmpdir1 self.tmpdir2 = tmpdir2 self.fa = fa self.fb = fb self.fc = fc self.fd = fd self.glob1 = glob1 self.glob2 = glob2 class P(param.Parameterized): a = param.MultiFileSelector(path=glob1) b = param.MultiFileSelector(default=[fa], path=glob1) self.P = P def tearDown(self): shutil.rmtree(self.tmpdir1) shutil.rmtree(self.tmpdir2) def _check_defaults(self, p): assert p.default is None assert p.allow_None is None assert p.objects == [] assert p.compute_default_fn is None assert p.check_on_set is False assert p.names == {} assert p.path == '' def test_defaults_class(self): class P(param.Parameterized): s = param.MultiFileSelector() check_defaults(P.param.s, label='S') self._check_defaults(P.param.s) def test_defaults_inst(self): class P(param.Parameterized): s = param.MultiFileSelector() p = P() check_defaults(p.param.s, label='S') self._check_defaults(p.param.s) def test_defaults_unbound(self): s = param.MultiFileSelector() check_defaults(s, label=None) self._check_defaults(s) def test_default_is_None(self): p = self.P() assert p.a is None assert p.param.a.default is None def test_default_is_honored(self): p = self.P() assert p.b == [self.fa] assert p.param.b.default ==[self.fa] def test_allow_default_None(self): class P(param.Parameterized): a = param.MultiFileSelector(default=None) def test_objects_auto_set(self): p = self.P() assert p.param.a.objects == [self.fa, self.fb] def test_default_not_in_glob(self): with self.assertRaises(ValueError): class P(param.Parameterized): a = param.MultiFileSelector(default=['not/in/glob'], path=self.glob1) def test_objects_auto_set_sorted(self): p = self.P() assert sorted(p.param.a.objects) == sorted([self.fa, self.fb]) def test_set_object_constructor(self): p = self.P(a=[self.fb]) assert p.a == [self.fb] def test_set_object_outside_bounds(self): p = self.P() with self.assertRaises(ValueError): p.a = ['/not/in/glob'] def test_set_path_and_update(self): p = self.P() p.param.b.path = self.glob2 p.param.b.update() assert sorted(p.param.b.objects) == sorted([self.fc, self.fd]) assert sorted(p.param.b.default) == sorted([self.fc, self.fd]) # Default updated but not the value itself assert p.b == [self.fa] def test_path_autoupdate(self): p = self.P() p.param.b.path = self.glob2 assert sorted(p.param.b.objects) == sorted([self.fc, self.fd]) assert sorted(p.param.b.default) == sorted([self.fc, self.fd]) # Default updated but not the value itself assert p.b == [self.fa] def test_get_range(self): p = self.P() r = p.param.a.get_range() assert r['a.txt'] == self.fa assert r['b.txt'] == self.fb p.param.a.path = self.glob2 p.param.a.update() r = p.param.a.get_range() assert r['c.txt'] == self.fc assert r['d.txt'] == self.fd def test_update_file_removed(self): p = self.P() assert p.param.b.objects == [self.fa, self.fb] assert p.param.b.default == [self.fa] os.remove(self.fa) p.param.b.update() assert p.param.b.objects == [self.fb] assert p.param.b.default == [self.fb] param-2.1.1/tests/testnumbergen.py000066400000000000000000000015721463636336300172330ustar00rootroot00000000000000""" 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-2.1.1/tests/testnumberparameter.py000066400000000000000000000522141463636336300204410ustar00rootroot00000000000000""" Unit test for Number parameters and their subclasses. """ import unittest import pytest import param from .utils import check_defaults try: import numpy as np except ImportError: np = None class TestNumberParameters(unittest.TestCase): def setUp(self): super().setUp() class P(param.Parameterized): b = param.Number(allow_None=False) c = param.Number(default=1, allow_None=True) d = param.Number(default=None) e = param.Number(default=1) f = param.Number(default=1, step=0.5) g = param.Number(default=lambda: 1) h = param.Number(default=1, bounds=(0, 2)) i = param.Number(bounds=(-1, 1)) j = param.Number(bounds=(-1, 1), inclusive_bounds=(False, True)) k = param.Number(bounds=(-1, 1), inclusive_bounds=(True, False)) l = param.Number(bounds=(-1, 1), inclusive_bounds=(False, False)) m = param.Number(bounds=(-1, None)) n = param.Number(bounds=(None, 1)) self.P = P def _check_defaults(self, p): assert p.default == 0.0 assert p.allow_None is False assert p.bounds is None assert p.softbounds is None assert p.inclusive_bounds == (True, True) assert p.step is None def test_defaults_class(self): class A(param.Parameterized): n = param.Number() check_defaults(A.param.n, label='N') self._check_defaults(A.param.n) def test_defaults_inst(self): class A(param.Parameterized): n = param.Number() a = A() check_defaults(a.param.n, label='N') self._check_defaults(a.param.n) def test_defaults_unbound(self): n = param.Number() check_defaults(n, label=None) self._check_defaults(n) def test_allow_None_class(self): self.P.c = None assert self.P.c is None self.P.d = None assert self.P.d is None exception = "Number parameter 'P.b' only takes numeric values, not <(class|type) 'NoneType'>." with self.assertRaisesRegex(ValueError, exception): self.P.b = None def test_allow_None_inst(self): p = self.P() p.c = None assert p.c is None p.d = None assert p.d is None exception = "Number parameter 'P.b' only takes numeric values, not <(class|type) 'NoneType'>." with self.assertRaisesRegex(ValueError, exception): p.b = None def test_initialization_without_step_class(self): self.assertEqual(self.P.param['e'].step, None) def test_initialization_with_step_class(self): self.assertEqual(self.P.param['f'].step, 0.5) def test_initialization_without_step_instance(self): p = self.P() self.assertEqual(p.param['e'].step, None) def test_initialization_with_step_instance(self): p = self.P() self.assertEqual(p.param['f'].step, 0.5) def test_step_invalid_type_number_parameter(self): exception = "Attribute 'step' of Number parameter can only be None or a numeric value" with self.assertRaisesRegex(ValueError, exception): param.Number(step='invalid value') def test_outside_bounds(self): exception = "Number parameter 'P.h' must be at most 2, not 10." with self.assertRaisesRegex(ValueError, exception): self.P.h = 10 p = self.P() with self.assertRaisesRegex(ValueError, exception): p.h = 10 def test_unbounded_side_class(self): self.P.m = 10 assert self.P.m == 10 exception = "Number parameter 'P.m' must be at least -1, not -10." with self.assertRaisesRegex(ValueError, exception): self.P.m = -10 self.P.n = -10 assert self.P.n == -10 exception = "Number parameter 'P.n' must be at most 1, not 10." with self.assertRaisesRegex(ValueError, exception): self.P.n = 10 def test_unbounded_side_inst(self): p = self.P() p.m = 10 assert p.m == 10 exception = "Number parameter 'P.m' must be at least -1, not -10." with self.assertRaisesRegex(ValueError, exception): p.m = -10 p.n = -10 assert p.n == -10 exception = "Number parameter 'P.n' must be at most 1, not 10." with self.assertRaisesRegex(ValueError, exception): p.n = 10 def test_inclusive_bounds_no_error_class(self): self.P.i = -1 assert self.P.i == -1 self.P.i = 1 assert self.P.i == 1 self.P.j = 1 assert self.P.j == 1 self.P.k = -1 assert self.P.k == -1 def test_inclusive_bounds_no_error_inst(self): p = self.P() p.i = -1 assert p.i == -1 p.i = 1 assert p.i == 1 p.j = 1 assert p.j == 1 p.k = -1 assert p.k == -1 def test_inclusive_bounds_error_on_bounds(self): p = self.P() exception = "Number parameter 'P.j' must be greater than -1, not -1." with self.assertRaisesRegex(ValueError, exception): self.P.j = -1 with self.assertRaisesRegex(ValueError, exception): p.j = -1 exception = "Number parameter 'P.k' must be less than 1, not 1." with self.assertRaisesRegex(ValueError, exception): self.P.k = 1 with self.assertRaisesRegex(ValueError, exception): p.k = 1 exception = "Number parameter 'P.l' must be greater than -1, not -1." with self.assertRaisesRegex(ValueError, exception): self.P.l = -1 with self.assertRaisesRegex(ValueError, exception): p.l = -1 exception = "Number parameter 'P.l' must be less than 1, not 1." with self.assertRaisesRegex(ValueError, exception): self.P.l = 1 with self.assertRaisesRegex(ValueError, exception): p.l = 1 def test_inclusive_bounds_error_on_bounds_post(self): exception = "Number parameter 'j' must be greater than -1, not -1." with self.assertRaisesRegex(ValueError, exception): class P(param.Parameterized): j = param.Number(default=-1, bounds=(-1, 1), inclusive_bounds=(False, True)) exception = "Number parameter 'j' must be less than 1, not 1" with self.assertRaisesRegex(ValueError, exception): class Q(param.Parameterized): j = param.Number(default=1, bounds=(-1, 1), inclusive_bounds=(True, False)) exception = "Number parameter 'j' must be greater than -1, not -1." with self.assertRaisesRegex(ValueError, exception): class R(param.Parameterized): j = param.Number(default=-1, bounds=(-1, 1), inclusive_bounds=(False, False)) exception = "Number parameter 'j' must be less than 1, not 1." with self.assertRaisesRegex(ValueError, exception): class S(param.Parameterized): j = param.Number(default=1, bounds=(-1, 1), inclusive_bounds=(False, False)) def test_invalid_default_for_bounds(self): exception = "Number parameter 'n' must be at least 10, not 0.0." with self.assertRaisesRegex(ValueError, exception): class P(param.Parameterized): n = param.Number(bounds=(10, 20)) def test_callable(self): assert self.P.g == 1 p = self.P() assert p.g == 1 def test_callable_wrong_type(self): class Q(param.Parameterized): q = param.Number(default=lambda: 'test') exception = "Number parameter 'Q.q' only takes numeric values, not <(class|type) 'str'>." with self.assertRaisesRegex(ValueError, exception): Q.q q = Q() with self.assertRaisesRegex(ValueError, exception): q.q def test_callable_outside_bounds(self): class Q(param.Parameterized): q = param.Number(default=lambda: 2, bounds=(0, 1)) exception = "Number parameter 'Q.q' must be at most 1, not 2." with self.assertRaisesRegex(ValueError, exception): Q.q q = Q() with self.assertRaisesRegex(ValueError, exception): q.q def test_crop_to_bounds(self): p = self.P() # when allow_None is True assert p.param.d.crop_to_bounds(None) is None # no bounds assert p.param.e.crop_to_bounds(10000) == 10000 # with concrete bounds assert p.param.h.crop_to_bounds(10) == 2 assert p.param.h.crop_to_bounds(-10) == 0 # return default if non numerical assert p.param.e.crop_to_bounds('test') == 1 # Unbound assert p.param.m.crop_to_bounds(10) == 10 assert p.param.n.crop_to_bounds(-10) == -10 def test_inheritance_allow_None_behavior(self): class A(param.Parameterized): p = param.Number(allow_None=True) class B(A): p = param.Number() # A says None is not allowed, B disagrees. assert B.param.p.allow_None is False b = B() assert b.param.p.allow_None is False def test_inheritance_allow_None_behavior2(self): class A(param.Parameterized): p = param.Number(allow_None=False) class B(A): p = param.Number(default=None) # A says None is not allowed, B sets the default to None and recomputes # allow_None. assert B.param.p.allow_None is True b = B() assert b.param.p.allow_None is True def test_inheritance_callable_default_behavior(self): f = lambda: 2 class A(param.Parameterized): p = param.Number(default=f) class B(A): p = param.Number() assert A.p == 2 assert A.param.p.default == f assert A.param.p.instantiate is True # Default is inherited assert B.p == 2 assert B.param.p.default == f assert B.param.p.instantiate is True b = B() assert b.p == 2 assert b.param.p.default == f assert b.param.p.instantiate is True @pytest.mark.skipif(np is None, reason='NumPy is not available') def test_numpy_default(self): class Q(param.Parameterized): a = param.Number(default=np.float32(2.3)) @pytest.mark.skipif(np is None, reason='NumPy is not available') def test_numpy_set(self): class Q(param.Parameterized): a = param.Number() q = Q() q.a = np.float32(2.3) @pytest.mark.skipif(np is None, reason='NumPy is not available') def test_numpy_init(self): class Q(param.Parameterized): a = param.Number() Q(a=np.float32(2.3)) class TestIntegerParameters(unittest.TestCase): def setUp(self): super().setUp() class P(param.Parameterized): b = param.Integer(allow_None=False) c = param.Integer(default=1, allow_None=True) d = param.Integer(default=None) e = param.Integer(default=1) f = param.Integer(default=1, step=1) g = param.Integer(default=lambda: 1) h = param.Integer(default=1, bounds=(0, 2)) i = param.Integer(bounds=(-1, 1)) j = param.Integer(bounds=(-1, 1), inclusive_bounds=(False, True)) k = param.Integer(bounds=(-1, 1), inclusive_bounds=(True, False)) l = param.Integer(bounds=(-1, 1), inclusive_bounds=(False, False)) m = param.Integer(bounds=(-1, None)) n = param.Integer(bounds=(None, 1)) self.P = P def _check_defaults(self, p): assert isinstance(p.default, int) assert p.default == 0 assert p.allow_None is False assert p.bounds is None assert p.softbounds is None assert p.inclusive_bounds == (True, True) assert p.step is None def test_defaults_class(self): class A(param.Parameterized): n = param.Integer() check_defaults(A.param.n, label='N') self._check_defaults(A.param.n) def test_defaults_inst(self): class A(param.Parameterized): n = param.Integer() a = A() check_defaults(a.param.n, label='N') self._check_defaults(a.param.n) def test_defaults_unbound(self): n = param.Integer() check_defaults(n, label=None) self._check_defaults(n) def test_allow_None_class(self): self.P.c = None assert self.P.c is None self.P.d = None assert self.P.d is None exception = "Integer parameter 'P.b' must be an integer, not <(class|type) 'NoneType'>." with self.assertRaisesRegex(ValueError, exception): self.P.b = None def test_allow_None_inst(self): p = self.P() p.c = None assert p.c is None p.d = None assert p.d is None exception = "Integer parameter 'P.b' must be an integer, not <(class|type) 'NoneType'>." with self.assertRaisesRegex(ValueError, exception): p.b = None def test_initialization_without_step_class(self): class Q(param.Parameterized): q = param.Integer(default=1) self.assertEqual(Q.param['q'].step, None) def test_initialization_without_step_class2(self): self.assertEqual(self.P.param['e'].step, None) def test_initialization_with_step_class(self): self.assertEqual(self.P.param['f'].step, 1) def test_initialization_without_step_instance(self): p = self.P() self.assertEqual(p.param['e'].step, None) def test_initialization_with_step_instance(self): p = self.P() self.assertEqual(p.param['f'].step, 1) def test_step_invalid_type_number_parameter(self): exception = "Attribute 'step' of Integer parameter can only be None or an integer value" with self.assertRaisesRegex(ValueError, exception): param.Integer(step='invalid value') def test_step_invalid_type_integer_parameter(self): exception = "Attribute 'step' of Integer parameter can only be None or an integer value" with self.assertRaisesRegex(ValueError, exception): param.Integer(step=3.4) def test_outside_bounds(self): exception = "Integer parameter 'P.h' must be at most 2, not 10." with self.assertRaisesRegex(ValueError, exception): self.P.h = 10 p = self.P() with self.assertRaisesRegex(ValueError, exception): p.h = 10 def test_unbounded_side_class(self): self.P.m = 10 assert self.P.m == 10 exception = "Integer parameter 'P.m' must be at least -1, not -10." with self.assertRaisesRegex(ValueError, exception): self.P.m = -10 self.P.n = -10 assert self.P.n == -10 exception = "Integer parameter 'P.n' must be at most 1, not 10." with self.assertRaisesRegex(ValueError, exception): self.P.n = 10 def test_unbounded_side_inst(self): p = self.P() p.m = 10 assert p.m == 10 exception = "Integer parameter 'P.m' must be at least -1, not -10." with self.assertRaisesRegex(ValueError, exception): p.m = -10 p.n = -10 assert p.n == -10 exception = "Integer parameter 'P.n' must be at most 1, not 10." with self.assertRaisesRegex(ValueError, exception): p.n = 10 def test_inclusive_bounds_no_error_class(self): self.P.i = -1 assert self.P.i == -1 self.P.i = 1 assert self.P.i == 1 self.P.j = 1 assert self.P.j == 1 self.P.k = -1 assert self.P.k == -1 def test_inclusive_bounds_no_error_inst(self): p = self.P() p.i = -1 assert p.i == -1 p.i = 1 assert p.i == 1 p.j = 1 assert p.j == 1 p.k = -1 assert p.k == -1 def test_inclusive_bounds_error_on_bounds(self): p = self.P() exception = "Integer parameter 'P.j' must be greater than -1, not -1." with self.assertRaisesRegex(ValueError, exception): self.P.j = -1 with self.assertRaisesRegex(ValueError, exception): p.j = -1 exception = "Integer parameter 'P.k' must be less than 1, not 1." with self.assertRaisesRegex(ValueError, exception): self.P.k = 1 with self.assertRaisesRegex(ValueError, exception): p.k = 1 exception = "Integer parameter 'P.l' must be greater than -1, not -1." with self.assertRaisesRegex(ValueError, exception): self.P.l = -1 with self.assertRaisesRegex(ValueError, exception): p.l = -1 exception = "Integer parameter 'P.l' must be less than 1, not 1." with self.assertRaisesRegex(ValueError, exception): self.P.l = 1 with self.assertRaisesRegex(ValueError, exception): p.l = 1 def test_inclusive_bounds_error_on_bounds_post(self): exception = "Integer parameter 'j' must be greater than -1, not -1." with self.assertRaisesRegex(ValueError, exception): class P(param.Parameterized): j = param.Integer(default=-1, bounds=(-1, 1), inclusive_bounds=(False, True)) exception = "Integer parameter 'j' must be less than 1, not 1" with self.assertRaisesRegex(ValueError, exception): class Q(param.Parameterized): j = param.Integer(default=1, bounds=(-1, 1), inclusive_bounds=(True, False)) exception = "Integer parameter 'j' must be greater than -1, not -1." with self.assertRaisesRegex(ValueError, exception): class R(param.Parameterized): j = param.Integer(default=-1, bounds=(-1, 1), inclusive_bounds=(False, False)) exception = "Integer parameter 'j' must be less than 1, not 1." with self.assertRaisesRegex(ValueError, exception): class S(param.Parameterized): j = param.Integer(default=1, bounds=(-1, 1), inclusive_bounds=(False, False)) def test_invalid_default_for_bounds(self): exception = "Integer parameter 'n' must be at least 10, not 0." with self.assertRaisesRegex(ValueError, exception): class P(param.Parameterized): n = param.Integer(bounds=(10, 20)) def test_callable(self): assert self.P.g == 1 p = self.P() assert p.g == 1 def test_callable_wrong_type(self): class Q(param.Parameterized): q = param.Integer(default=lambda: 'test') exception = "Integer parameter 'Q.q' must be an integer, not <(class|type) 'str'>." with self.assertRaisesRegex(ValueError, exception): Q.q q = Q() with self.assertRaisesRegex(ValueError, exception): q.q def test_callable_outside_bounds(self): class Q(param.Parameterized): q = param.Integer(default=lambda: 2, bounds=(0, 1)) exception = "Integer parameter 'Q.q' must be at most 1, not 2." with self.assertRaisesRegex(ValueError, exception): Q.q q = Q() with self.assertRaisesRegex(ValueError, exception): q.q def test_crop_to_bounds(self): p = self.P() # when allow_None is True assert p.param.d.crop_to_bounds(None) is None # no bounds assert p.param.e.crop_to_bounds(10000) == 10000 # with concrete bounds assert p.param.h.crop_to_bounds(10) == 2 assert p.param.h.crop_to_bounds(-10) == 0 # return default if non numerical assert p.param.e.crop_to_bounds('test') == 1 # Unbound assert p.param.m.crop_to_bounds(10) == 10 assert p.param.n.crop_to_bounds(-10) == -10 @pytest.mark.skipif(np is None, reason='NumPy is not available') def test_numpy_default(self): class Q(param.Parameterized): a = param.Integer(default=np.int64(2)) assert isinstance(Q.a, np.integer) @pytest.mark.skipif(np is None, reason='NumPy is not available') def test_numpy_set(self): class Q(param.Parameterized): a = param.Integer() q = Q() q.a = np.int64(2) assert isinstance(q.a, np.integer) @pytest.mark.skipif(np is None, reason='NumPy is not available') def test_numpy_init(self): class Q(param.Parameterized): a = param.Integer() q = Q(a=np.int64(2)) assert isinstance(q.a, np.integer) class TestMagnitudeParameters(unittest.TestCase): def _check_defaults(self, p): assert p.default == 1.0 assert p.allow_None is False assert p.bounds == (0.0, 1.0) assert p.softbounds is None assert p.inclusive_bounds == (True, True) assert p.step is None def test_defaults_class(self): class A(param.Parameterized): n = param.Magnitude() check_defaults(A.param.n, label='N') self._check_defaults(A.param.n) def test_defaults_inst(self): class A(param.Parameterized): n = param.Magnitude() a = A() check_defaults(a.param.n, label='N') self._check_defaults(a.param.n) def test_defaults_unbound(self): n = param.Magnitude() check_defaults(n, label=None) self._check_defaults(n) param-2.1.1/tests/testnumpy.py000066400000000000000000000040221463636336300164120ustar00rootroot00000000000000""" If numpy's present, is numpy stuff ok? """ import os import unittest import param from .utils import check_defaults 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 _check_defaults(self, p): assert p.default is None assert p.allow_None is True assert p.instantiate is True assert p.is_instance is True assert p.class_ == numpy.ndarray def test_defaults_class(self): class P(param.Parameterized): s = param.Array() check_defaults(P.param.s, label='S', skip=['instantiate']) self._check_defaults(P.param.s) def test_defaults_inst(self): class P(param.Parameterized): s = param.Array() p = P() check_defaults(p.param.s, label='S', skip=['instantiate']) self._check_defaults(p.param.s) def test_defaults_unbound(self): s = param.Array() check_defaults(s, label=None, skip=['instantiate']) self._check_defaults(s) 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]) def test_array_pprint(self): class MatParam(param.Parameterized): mat = param.Array(numpy.zeros((2, 2))) mp = MatParam() mp.param.pprint() param-2.1.1/tests/testobjectselector.py000066400000000000000000000465021463636336300202620ustar00rootroot00000000000000""" Unit test for object selector parameters. Originally implemented as doctests in Topographica in the file testEnumerationParameter.txt """ import re import unittest from collections import OrderedDict import param import pytest from .utils import check_defaults opts=dict(A=[1,2],B=[3,4],C=dict(a=1,b=2)) class TestObjectSelectorParameters(unittest.TestCase): def setUp(self): super().setUp() 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) s = param.ObjectSelector(default=3,objects=OrderedDict(one=1,two=2,three=3)) d = param.ObjectSelector(default=opts['B'],objects=opts) changes = [] @param.depends('e:objects', watch=True) def track_e_objects(self): self.changes.append(('e', list(self.param.e.objects))) @param.depends('s:objects', watch=True) def track_s_objects(self): self.changes.append(('s', list(self.param.s.objects))) self.P = P def _check_defaults(self, p): assert p.default is None assert p.allow_None is None assert p.objects == [] assert p.compute_default_fn is None assert p.check_on_set is False assert p.names == {} def test_defaults_class(self): class P(param.Parameterized): s = param.ObjectSelector() check_defaults(P.param.s, label='S') self._check_defaults(P.param.s) def test_defaults_inst(self): class P(param.Parameterized): s = param.ObjectSelector() p = P() check_defaults(p.param.s, label='S') self._check_defaults(p.param.s) def test_defaults_unbound(self): s = param.ObjectSelector() check_defaults(s, label=None) self._check_defaults(s) def test_unbound_default_inferred(self): s = param.ObjectSelector(objects=[0, 1, 2]) assert s.default is None def test_unbound_default_explicit(self): s = param.ObjectSelector(default=1, objects=[0, 1, 2]) assert s.default == 1 def test_unbound_default_check_on_set_inferred(self): s1 = param.ObjectSelector(objects=[0, 1, 2]) s2 = param.ObjectSelector(objects=[]) s3 = param.ObjectSelector(objects={}) s4 = param.ObjectSelector() assert s1.check_on_set is True assert s2.check_on_set is False assert s3.check_on_set is False assert s4.check_on_set is False def test_unbound_default_check_on_set_explicit(self): s1 = param.ObjectSelector(check_on_set=True) s2 = param.ObjectSelector(check_on_set=False) assert s1.check_on_set is True assert s2.check_on_set is False def test_unbound_allow_None_not_dynamic(self): s = param.ObjectSelector(objects=[0, 1, 2]) assert s.allow_None is None def test_allow_None_set_and_behavior_class(self): class P(param.Parameterized): a = param.ObjectSelector(objects=dict(a=1), allow_None=True) b = param.ObjectSelector(objects=dict(a=1), allow_None=False) c = param.ObjectSelector(default=1, objects=dict(a=1), allow_None=True) d = param.ObjectSelector(default=1, objects=dict(a=1), allow_None=False) assert P.param.a.allow_None is True assert P.param.b.allow_None is False assert P.param.c.allow_None is True assert P.param.d.allow_None is False P.a = None assert P.a is None with pytest.raises(ValueError): P.b = None P.c = None assert P.c is None with pytest.raises(ValueError): P.d = None def test_allow_None_set_and_behavior_instance(self): class P(param.Parameterized): a = param.ObjectSelector(objects=dict(a=1), allow_None=True) b = param.ObjectSelector(objects=dict(a=1), allow_None=False) c = param.ObjectSelector(default=1, objects=dict(a=1), allow_None=True) d = param.ObjectSelector(default=1, objects=dict(a=1), allow_None=False) p = P() assert p.param.a.allow_None is True assert p.param.b.allow_None is False assert p.param.c.allow_None is True assert p.param.d.allow_None is False p.a = None assert p.a is None with pytest.raises(ValueError): p.b = None p.c = None assert p.c is None with pytest.raises(ValueError): p.d = None def test_set_object_constructor(self): p = self.P(e=6) self.assertEqual(p.e, 6) def test_allow_None_is_None(self): p = self.P() assert p.param.e.allow_None is None assert p.param.f.allow_None is None assert p.param.g.allow_None is None assert p.param.h.allow_None is None assert p.param.i.allow_None is None assert p.param.s.allow_None is None assert p.param.d.allow_None is None def test_get_range_list(self): r = self.P.param['g'].get_range() self.assertEqual(r['7'],7) self.assertEqual(r['8'],8) def test_get_range_dict(self): r = self.P.param['s'].get_range() self.assertEqual(r['one'],1) self.assertEqual(r['two'],2) def test_get_range_mutable(self): r = self.P.param['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_change_objects_list(self): p = self.P() p.param.e.objects = [8, 9] with pytest.raises( ValueError, match=re.escape(r"ObjectSelector parameter 'P.e' does not accept 7; valid options include: '[8, 9]'") ): p.e = 7 self.assertEqual(p.param.e.objects, [8, 9]) self.assertEqual(p.changes, [('e', [8, 9])]) def test_copy_objects_list(self): p = self.P() eobjs = p.param.e.objects.copy() self.assertIsInstance(eobjs, list) self.assertFalse(eobjs is p.param.e.objects) self.assertEqual(eobjs, [5, 6, 7]) def test_append_objects_list(self): p = self.P() p.param.e.objects.append(8) p.e = 8 self.assertEqual(p.param.e.objects, [5, 6, 7, 8]) self.assertEqual(p.changes, [('e', [5, 6, 7, 8])]) def test_extend_objects_list(self): p = self.P() p.param.e.objects.extend([8, 9]) p.e = 8 self.assertEqual(p.param.e.objects, [5, 6, 7, 8, 9]) self.assertEqual(p.changes, [('e', [5, 6, 7, 8, 9])]) def test_get_objects_list(self): p = self.P() self.assertEqual(p.param.e.objects.get('5'), 5) self.assertEqual(p.param.e.objects.get(5, 'five'), 'five') def test_insert_objects_list(self): p = self.P() p.param.e.objects.insert(0, 8) p.e = 8 self.assertEqual(p.param.e.objects, [8, 5, 6, 7]) self.assertEqual(p.changes, [('e', [8, 5, 6, 7])]) def test_pop_objects_list(self): p = self.P() p.param.e.objects.pop(-1) with self.assertRaises(ValueError): p.e = 7 self.assertEqual(p.param.e.objects, [5, 6]) self.assertEqual(p.changes, [('e', [5, 6])]) def test_remove_objects_list(self): p = self.P() p.param.e.objects.remove(7) with self.assertRaises(ValueError): p.e = 7 self.assertEqual(p.param.e.objects, [5, 6]) self.assertEqual(p.changes, [('e', [5, 6])]) def test_clear_objects_list(self): p = self.P() p.param.e.objects.clear() with self.assertRaises(ValueError): p.e = 5 self.assertEqual(p.param.e.objects, []) self.assertEqual(p.changes, [('e', [])]) def test_clear_setitem_objects_list(self): p = self.P() p.param.e.objects[:] = [] with self.assertRaises(ValueError): p.e = 5 self.assertEqual(p.param.e.objects, []) self.assertEqual(p.changes, [('e', [])]) def test_override_setitem_objects_list(self): p = self.P() p.param.e.objects[0] = 8 with self.assertRaises(ValueError): p.e = 5 p.e = 8 self.assertEqual(p.param.e.objects, [8, 6, 7]) self.assertEqual(p.changes, [('e', [8, 6, 7])]) def test_setitem_name_objects_list(self): p = self.P() p.param.e.objects['A'] = 8 self.assertEqual(p.param.e.objects, {'5': 5, '6': 6, '7': 7, 'A': 8}) self.assertEqual(len(p.changes), 1) def test_update_objects_list(self): p = self.P() p.param.e.objects.update({'A': 8}) self.assertEqual(p.param.e.objects, {'5': 5, '6': 6, '7': 7, 'A': 8}) self.assertEqual(len(p.changes), 1) def test_int_getitem_objects_list(self): p = self.P() self.assertEqual(p.param.e.objects[0], 5) def test_slice_getitem_objects_list(self): p = self.P() self.assertEqual(p.param.e.objects[1:3], [6, 7]) def test_items_objects_list(self): p = self.P() self.assertEqual(list(p.param.e.objects.items()), [('5', 5), ('6', 6), ('7', 7)]) def test_keys_objects_list(self): p = self.P() self.assertEqual(list(p.param.e.objects.keys()), ['5', '6', '7']) def test_values_objects_list(self): p = self.P() self.assertEqual(list(p.param.e.objects.values()), list(p.param.e.objects)) def test_change_objects_dict(self): p = self.P() p.param.s.objects = {'seven': 7, 'eight': 8} with pytest.raises( ValueError, match=re.escape(r"ObjectSelector parameter 'P.s' does not accept 1; valid options include: '[7, 8]'") ): p.s = 1 self.assertEqual(p.param.s.objects, [7, 8]) self.assertEqual(p.changes, [('s', [7, 8])]) def test_getitem_int_objects_dict(self): p = self.P() with self.assertRaises(KeyError): p.param.s.objects[2] def test_getitem_objects_dict(self): p = self.P() self.assertEqual(p.param.s.objects['two'], 2) def test_keys_objects_dict(self): p = self.P() self.assertEqual(list(p.param.s.objects.keys()), ['one', 'two', 'three']) def test_items_objects_dict(self): p = self.P() self.assertEqual(list(p.param.s.objects.items()), [('one', 1), ('two', 2), ('three', 3)]) def test_cast_to_dict_objects_dict(self): p = self.P() self.assertEqual(dict(p.param.s.objects), {'one': 1, 'two': 2, 'three': 3}) def test_cast_to_list_objects_dict(self): p = self.P() self.assertEqual(list(p.param.s.objects), [1, 2, 3]) def test_setitem_key_objects_dict(self): p = self.P() p.param.s.objects['seven'] = 7 p.s = 7 self.assertEqual(p.param.s.objects, [1, 2, 3, 7]) self.assertEqual(p.changes, [('s', [1, 2, 3, 7])]) def test_objects_dict_equality(self): p = self.P() p.param.s.objects = {'seven': 7, 'eight': 8} self.assertEqual(p.param.s.objects, {'seven': 7, 'eight': 8}) self.assertNotEqual(p.param.s.objects, {'seven': 7, 'eight': 8, 'nine': 9}) def test_clear_objects_dict(self): p = self.P() p.param.s.objects.clear() with self.assertRaises(ValueError): p.s = 1 self.assertEqual(p.param.s.objects, []) self.assertEqual(p.changes, [('s', [])]) def test_copy_objects_dict(self): p = self.P() sobjs = p.param.s.objects.copy() self.assertIsInstance(sobjs, dict) self.assertEqual(sobjs, {'one': 1, 'two': 2, 'three': 3}) def test_get_objects_dict(self): p = self.P() self.assertEqual(p.param.s.objects.get('two'), 2) def test_get_default_objects_dict(self): p = self.P() self.assertEqual(p.param.s.objects.get('four', 'four'), 'four') def test_pop_objects_dict(self): p = self.P() p.param.s.objects.pop('one') with self.assertRaises(ValueError): p.s = 1 self.assertEqual(p.param.s.objects, [2, 3]) self.assertEqual(p.changes, [('s', [2, 3])]) def test_remove_objects_dict(self): p = self.P() p.param.s.objects.remove(1) with self.assertRaises(ValueError): p.s = 1 self.assertEqual(p.param.s.objects, [2, 3]) self.assertEqual(p.param.s.names, {'two': 2, 'three': 3}) self.assertEqual(p.changes, [('s', [2, 3])]) def test_update_objects_dict(self): p = self.P() p.param.s.objects.update({'one': '1', 'three': '3'}) with self.assertRaises(ValueError): p.s = 1 p.s = '3' self.assertEqual(p.param.s.objects, ['1', 2, '3']) self.assertEqual(p.changes, [('s', ['1', 2, '3'])]) def test_update_with_list_objects_dict(self): p = self.P() p.param.s.objects.update([('one', '1'), ('three', '3')]) with self.assertRaises(ValueError): p.s = 1 p.s = '3' self.assertEqual(p.param.s.objects, ['1', 2, '3']) self.assertEqual(p.changes, [('s', ['1', 2, '3'])]) def test_update_with_invalid_list_objects_dict(self): p = self.P() with self.assertRaises(TypeError): p.param.s.objects.update([1, 3]) with self.assertRaises(ValueError): p.param.s.objects.update(['a', 'b']) def test_values_objects_dict(self): p = self.P() self.assertEqual(list(p.param.s.objects.values()), [1, 2, 3]) 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.") def test_compute_default_fn_in_objects(self): class P(param.Parameterized): o = param.ObjectSelector(objects=[0, 1], compute_default_fn=lambda: 1) assert P.param.o.default is None P.param.o.compute_default() assert P.param.o.default == 1 p = P() assert p.o == 1 def test_compute_default_fn_not_in_objects(self): class P(param.Parameterized): o = param.ObjectSelector(objects=[0, 1], compute_default_fn=lambda: 2) assert P.param.o.default is None P.param.o.compute_default() assert P.param.o.default == 2 p = P() assert p.o == 2 def test_inheritance_behavior1(self): class A(param.Parameterized): p = param.ObjectSelector() class B(A): p = param.ObjectSelector() assert B.param.p.default is None assert B.param.p.objects == [] assert B.param.p.check_on_set is False b = B() assert b.param.p.default is None assert b.param.p.objects == [] assert b.param.p.check_on_set is False def test_inheritance_behavior2(self): class A(param.Parameterized): p = param.ObjectSelector(objects=[0, 1]) class B(A): p = param.ObjectSelector() assert B.param.p.objects == [0, 1] assert B.param.p.default is None assert B.param.p.check_on_set is True b = B() assert b.param.p.objects == [0, 1] assert b.param.p.default is None assert b.param.p.check_on_set is True def test_inheritance_behavior3(self): class A(param.Parameterized): p = param.ObjectSelector(default=1, objects=[0, 1]) class B(A): p = param.ObjectSelector() assert B.param.p.objects == [0, 1] assert B.param.p.default == 1 assert B.param.p.check_on_set is True b = B() assert b.param.p.objects == [0, 1] assert b.param.p.default == 1 assert b.param.p.check_on_set is True def test_inheritance_behavior4(self): class A(param.Parameterized): p = param.ObjectSelector(objects=[0, 1], check_on_set=False) class B(A): p = param.ObjectSelector() assert B.param.p.objects == [0, 1] assert B.param.p.default is None assert B.param.p.check_on_set is False b = B() assert b.param.p.objects == [0, 1] assert b.param.p.default is None assert b.param.p.check_on_set is False def test_inheritance_behavior5(self): class A(param.Parameterized): p = param.ObjectSelector(objects=[0, 1], check_on_set=True) class B(A): p = param.ObjectSelector() assert B.param.p.objects == [0, 1] assert B.param.p.default is None assert B.param.p.check_on_set is True b = B() assert b.param.p.objects == [0, 1] assert b.param.p.default is None assert b.param.p.check_on_set is True def test_inheritance_behavior6(self): class A(param.Parameterized): p = param.ObjectSelector(default=0, objects=[0, 1]) class B(A): p = param.ObjectSelector(default=1) assert B.param.p.objects == [0, 1] assert B.param.p.default == 1 assert B.param.p.check_on_set is True b = B() assert b.param.p.objects == [0, 1] assert b.param.p.default == 1 assert b.param.p.check_on_set is True param-2.1.1/tests/testpandas.py000066400000000000000000000254521463636336300165220ustar00rootroot00000000000000""" Test Parameters based on pandas """ import os import re import unittest import param import pytest from .utils import check_defaults 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(unittest.TestCase): def _check_defaults(self, p): assert p.default is None assert p.allow_None is True assert p.instantiate is True assert p.is_instance is True assert p.rows is None assert p.columns is None assert p.ordered is None assert p.class_ == pandas.DataFrame def test_defaults_class(self): class P(param.Parameterized): s = param.DataFrame() check_defaults(P.param.s, label='S', skip=['instantiate']) self._check_defaults(P.param.s) def test_defaults_inst(self): class P(param.Parameterized): s = param.DataFrame() p = P() check_defaults(p.param.s, label='S', skip=['instantiate']) self._check_defaults(p.param.s) def test_defaults_unbound(self): s = param.DataFrame() check_defaults(s, label=None, skip=['instantiate']) self._check_defaults(s) 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 'Test.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['df'].ordered, False) exception = re.escape("DataFrame parameter 'Test.df': provided 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['df'].ordered, True) exception = re.escape("DataFrame parameter 'Test.df': provided 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['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 = re.escape("DataFrame parameter 'df': 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 = re.escape("DataFrame parameter 'Test.df': 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)) def test_dataframe_unordered_columns_as_set_error(self): valid_df = pandas.DataFrame({'a':[1,2], 'b':[2,3], 'c':[4,5]}, columns=['b', 'a', 'c']) with pytest.raises( ValueError, match=re.escape("DataFrame parameter 'df': columns cannot be ordered when specified as a set"), ): df = param.DataFrame(default=valid_df, columns=set(['a', 'b']), ordered=True) # noqa class TestSeries(unittest.TestCase): def _check_defaults(self, p): assert p.default is None assert p.allow_None is True assert p.instantiate is True assert p.is_instance is True assert p.rows is None assert p.class_ == pandas.Series def test_defaults_class(self): class P(param.Parameterized): s = param.Series() check_defaults(P.param.s, label='S', skip=['instantiate']) self._check_defaults(P.param.s) def test_defaults_inst(self): class P(param.Parameterized): s = param.Series() p = P() check_defaults(p.param.s, label='S', skip=['instantiate']) self._check_defaults(p.param.s) def test_defaults_unbound(self): s = param.Series() check_defaults(s, label=None, skip=['instantiate']) self._check_defaults(s) 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 = re.escape("Series parameter 'Test.series': 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 = re.escape("Series parameter 'series': 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-2.1.1/tests/testparamdepends.py000066400000000000000000001173741463636336300177240ustar00rootroot00000000000000""" Unit test for param.depends. """ import asyncio import param import pytest from param.parameterized import _parse_dependency_spec @pytest.fixture def class_name(request): try: yield request.param finally: pass class TestDependencyParser: 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: 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.method_dependencies('test')) == 2 class B(A): @param.depends('b') def test(self): pass b = B() assert len(b.param.method_dependencies('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.method_dependencies('test')) == 2 class B(A): @param.depends('b', watch=True) def test(self): pass b = B() assert len(b.param.method_dependencies('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.method_dependencies('test')) == 2 class B(A): def test(self): pass b = B() assert len(b.param.method_dependencies('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: def setup_method(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 AP(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) async def single_parameter(self): self.single_count += 1 @param.depends('a:constant', watch=True) async def constant(self): self.attr_count += 1 @param.depends('b.a', watch=True) async def single_nested(self): self.single_nested_count += 1 @param.depends('b.b.a', watch=True) async def double_nested(self): self.double_nested_count += 1 @param.depends('b.a:constant', watch=True) async def nested_attribute(self): self.nested_attr_count += 1 @param.depends('b.param', watch=True) async def nested(self): self.nested_count += 1 class P2(param.Parameterized): @param.depends(P.param.a) def external_param(self, a): pass class AP2(param.Parameterized): @param.depends(AP.param.a) async def external_param(self, a): pass self.P = P self.AP = AP self.P2 = P2 self.AP2 = AP2 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 async def test_param_nested_depends_value_unchanged_async(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) async 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 async def test_param_nested_at_class_definition_async(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) async def test(self): self.test_count += 1 b = B() b.a.c = 1 await asyncio.sleep(0.01) assert b.test_count == 1 b.a.param.update(c=2, d=1) await asyncio.sleep(0.01) assert b.test_count == 2 b.a = A() await asyncio.sleep(0.01) assert b.test_count == 3 B.a.c = 5 await asyncio.sleep(0.01) 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 async def test_param_nested_depends_expands_async(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) async def test(self): self.test_count += 1 b = B(a=A(c=1, name='A')) b.a = A(c=1, name='A') await asyncio.sleep(0.01) 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 async def test_param_depends_class_default_dynamic_async(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) async def nested(self): self.nested_count += 1 b = B() b.a.c = 1 await asyncio.sleep(0.01) assert b.nested_count == 1 b.a = A() await asyncio.sleep(0.01) assert b.nested_count == 2 @pytest.mark.parametrize('class_name', ['P', 'AP'], indirect=True) async def test_param_instance_depends_dynamic_single_nested(self, class_name): inst = getattr(self, class_name)() pinfos = inst.param.method_dependencies('single_nested', intermediate=True) assert len(pinfos) == 0 inst.b = getattr(self, class_name)() pinfos = inst.param.method_dependencies('single_nested', intermediate=True) assert len(pinfos) == 2 pinfos = {(pi.inst, pi.name): pi for pi in pinfos} pinfo = pinfos[(inst, 'b')] assert pinfo.cls is getattr(self, class_name) assert pinfo.inst is inst assert pinfo.name == 'b' assert pinfo.what == 'value' pinfo2 = pinfos[(inst.b, 'a')] assert pinfo2.cls is getattr(self, class_name) assert pinfo2.inst is inst.b assert pinfo2.name == 'a' assert pinfo2.what == 'value' if class_name == 'AP': await asyncio.sleep(0.01) assert inst.single_nested_count == 1 inst.b.a = 1 if class_name == 'AP': await asyncio.sleep(0.01) assert inst.single_nested_count == 2 @pytest.mark.parametrize('class_name', ['P', 'AP'], indirect=True) async def test_param_instance_depends_dynamic_single_nested_initialized_no_intermediates(self, class_name): init_b = getattr(self, class_name)() inst = getattr(self, class_name)(b=init_b) pinfos = inst.param.method_dependencies('single_nested', intermediate=False) assert len(pinfos) == 1 assert pinfos[0].inst is init_b assert pinfos[0].name == 'a' new_b = getattr(self, class_name)() inst.b = new_b if class_name == 'AP': await asyncio.sleep(0.01) pinfos = inst.param.method_dependencies('single_nested', intermediate=False) assert len(pinfos) == 1 assert pinfos[0].inst is new_b assert pinfos[0].name == 'a' @pytest.mark.parametrize('class_name', ['P', 'AP'], indirect=True) async def test_param_instance_depends_dynamic_single_nested_initialized_only_intermediates(self, class_name): init_b = getattr(self, class_name)() inst = getattr(self, class_name)(b=init_b) pinfos = inst.param.method_dependencies('single_nested', intermediate='only') assert len(pinfos) == 1 assert pinfos[0].inst is inst assert pinfos[0].name == 'b' @pytest.mark.parametrize('class_name', ['P', 'AP'], indirect=True) async def test_param_instance_depends_dynamic_single_nested_initialized(self, class_name): init_b = getattr(self, class_name)() inst = getattr(self, class_name)(b=init_b) pinfos = inst.param.method_dependencies('single_nested', intermediate=True) assert len(pinfos) == 2 inst.b = getattr(self, class_name)() pinfos = inst.param.method_dependencies('single_nested', intermediate=True) assert len(pinfos) == 2 pinfos = {(pi.inst, pi.name): pi for pi in pinfos} pinfo = pinfos[(inst, 'b')] pinfo.cls is getattr(self, class_name) pinfo.inst is inst assert pinfo.name == 'b' assert pinfo.what == 'value' pinfo2 = pinfos[(inst.b, 'a')] pinfo2.cls is getattr(self, class_name) pinfo2.inst is inst.b assert pinfo2.name == 'a' assert pinfo2.what == 'value' if class_name == 'AP': await asyncio.sleep(0.01) assert inst.single_nested_count == 0 inst.b.a = 1 if class_name == 'AP': await asyncio.sleep(0.01) assert inst.single_nested_count == 1 # Ensure watcher on initial value does not trigger event init_b.a = 2 if class_name == 'AP': await asyncio.sleep(0.01) assert inst.single_nested_count == 1 @pytest.mark.parametrize('class_name', ['P', 'AP'], indirect=True) async def test_param_instance_depends_dynamic_double_nested(self, class_name): inst = getattr(self, class_name)() pinfos = inst.param.method_dependencies('double_nested', intermediate=True) assert len(pinfos) == 0 inst.b = getattr(self, class_name)(b=getattr(self, class_name)()) pinfos = inst.param.method_dependencies('double_nested', intermediate=True) assert len(pinfos) == 3 pinfos = {(pi.inst, pi.name): pi for pi in pinfos} pinfo = pinfos[(inst, 'b')] assert pinfo.cls is getattr(self, class_name) assert pinfo.inst is inst assert pinfo.name == 'b' assert pinfo.what == 'value' pinfo2 = pinfos[(inst.b, 'b')] assert pinfo2.cls is getattr(self, class_name) assert pinfo2.inst is inst.b assert pinfo2.name == 'b' assert pinfo2.what == 'value' pinfo3 = pinfos[(inst.b.b, 'a')] assert pinfo3.cls is getattr(self, class_name) assert pinfo3.inst is inst.b.b assert pinfo3.name == 'a' assert pinfo3.what == 'value' if class_name == 'AP': await asyncio.sleep(0.01) assert inst.double_nested_count == 1 inst.b.b.a = 1 if class_name == 'AP': await asyncio.sleep(0.01) assert inst.double_nested_count == 2 old_subobj = inst.b.b inst.b.b = getattr(self, class_name)(a=3) if class_name == 'AP': await asyncio.sleep(0.01) assert inst.double_nested_count == 3 old_subobj.a = 4 if class_name == 'AP': await asyncio.sleep(0.01) assert inst.double_nested_count == 3 inst.b.b = getattr(self, class_name)(a=3) if class_name == 'AP': await asyncio.sleep(0.01) assert inst.double_nested_count == 3 inst.b.b.a = 4 if class_name == 'AP': await asyncio.sleep(0.01) assert inst.double_nested_count == 4 inst.b.b = getattr(self, class_name)(a=3) if class_name == 'AP': await asyncio.sleep(0.01) assert inst.double_nested_count == 5 @pytest.mark.parametrize('class_name', ['P', 'AP'], indirect=True) async def test_param_instance_depends_dynamic_double_nested_partially_initialized(self, class_name): inst = getattr(self, class_name)(b=getattr(self, class_name)()) pinfos = inst.param.method_dependencies('double_nested', intermediate=True) assert len(pinfos) == 2 pinfos = {(pi.inst, pi.name): pi for pi in pinfos} pinfo = pinfos[(inst, 'b')] assert pinfo.cls == getattr(self, class_name) assert pinfo.inst == inst assert pinfo.name == 'b' assert pinfo.what == 'value' pinfo = pinfos[(inst.b, 'b')] assert pinfo.cls == getattr(self, class_name) assert pinfo.inst == inst.b assert pinfo.name == 'b' assert pinfo.what == 'value' inst.b.b = getattr(self, class_name)() if class_name == 'AP': await asyncio.sleep(0.01) assert inst.double_nested_count == 1 inst.b.b.a = 1 if class_name == 'AP': await asyncio.sleep(0.01) assert inst.double_nested_count == 2 @pytest.mark.parametrize('class_name', ['P', 'AP'], indirect=True) async def test_param_instance_depends_dynamic_nested_attribute(self, class_name): inst = getattr(self, class_name)() pinfos = inst.param.method_dependencies('nested_attribute', intermediate=True) assert len(pinfos) == 0 inst.b = getattr(self, class_name)() pinfos = inst.param.method_dependencies('nested_attribute', intermediate=True) assert len(pinfos) == 2 pinfos = {(pi.inst, pi.name): pi for pi in pinfos} pinfo = pinfos[(inst, 'b')] assert pinfo.cls == getattr(self, class_name) assert pinfo.inst == inst assert pinfo.name == 'b' assert pinfo.what == 'value' pinfo2 = pinfos[(inst.b, 'a')] assert pinfo2.cls == getattr(self, class_name) assert pinfo2.inst == inst.b assert pinfo2.name == 'a' assert pinfo2.what == 'constant' if class_name == 'AP': await asyncio.sleep(0.01) assert inst.nested_attr_count == 1 inst.b.param.a.constant = True if class_name == 'AP': await asyncio.sleep(0.01) assert inst.nested_attr_count == 2 new_b = getattr(self, class_name)() new_b.param.a.constant = True inst.b = new_b if class_name == 'AP': await asyncio.sleep(0.01) assert inst.nested_attr_count == 2 @pytest.mark.parametrize('class_name', ['P', 'AP'], indirect=True) async def test_param_instance_depends_dynamic_nested_attribute_initialized(self, class_name): inst = getattr(self, class_name)(b=getattr(self, class_name)()) pinfos = inst.param.method_dependencies('nested_attribute', intermediate=True) assert len(pinfos) == 2 inst.b = getattr(self, class_name)() pinfos = inst.param.method_dependencies('nested_attribute', intermediate=True) assert len(pinfos) == 2 pinfos = {(pi.inst, pi.name): pi for pi in pinfos} pinfo = pinfos[(inst, 'b')] assert pinfo.cls == getattr(self, class_name) assert pinfo.inst == inst assert pinfo.name == 'b' assert pinfo.what == 'value' pinfo2 = pinfos[(inst.b, 'a')] assert pinfo2.cls == getattr(self, class_name) assert pinfo2.inst == inst.b assert pinfo2.name == 'a' assert pinfo2.what == 'constant' if class_name == 'AP': await asyncio.sleep(0.01) assert inst.nested_attr_count == 0 inst.b.param.a.constant = True if class_name == 'AP': await asyncio.sleep(0.01) assert inst.nested_attr_count == 1 @pytest.mark.parametrize('class_name', ['P', 'AP'], indirect=True) async def test_param_instance_depends_dynamic_nested(self, class_name): inst = getattr(self, class_name)() pinfos = inst.param.method_dependencies('nested') assert len(pinfos) == 0 inst.b = getattr(self, class_name)() pinfos = inst.param.method_dependencies('nested') assert len(pinfos) == 10 pinfos = {(pi.inst, pi.name): pi for pi in pinfos} pinfo = pinfos[(inst, 'b')] assert pinfo.cls is getattr(self, class_name) assert pinfo.inst is inst assert pinfo.name == 'b' assert pinfo.what == 'value' for p in ['a', 'b', 'name', 'nested_count', 'single_count', 'attr_count']: pinfo2 = pinfos[(inst.b, p)] assert pinfo2.cls is getattr(self, class_name) assert pinfo2.inst is inst.b assert pinfo2.name == p assert pinfo2.what == 'value' if class_name == 'AP': await asyncio.sleep(0.01) assert inst.nested_count == 1 inst.b.a = 1 if class_name == 'AP': await asyncio.sleep(0.01) assert inst.nested_count == 3 @pytest.mark.parametrize('class_name', ['P', 'AP'], indirect=True) async def test_param_instance_depends_dynamic_nested_initialized(self, class_name): init_b = getattr(self, class_name)() inst = getattr(self, class_name)(b=init_b) pinfos = inst.param.method_dependencies('nested') assert len(pinfos) == 10 inst.b = getattr(self, class_name)() pinfos = inst.param.method_dependencies('nested') assert len(pinfos) == 10 pinfos = {(pi.inst, pi.name): pi for pi in pinfos} pinfo = pinfos[(inst, 'b')] assert pinfo.cls is getattr(self, class_name) assert pinfo.inst is inst assert pinfo.name == 'b' assert pinfo.what == 'value' for p in ['a', 'b', 'name', 'nested_count', 'single_count', 'attr_count']: pinfo2 = pinfos[(inst.b, p)] assert pinfo2.cls is getattr(self, class_name) assert pinfo2.inst is inst.b assert pinfo2.name == p assert pinfo2.what == 'value' if class_name == 'AP': await asyncio.sleep(0.01) assert inst.single_nested_count == 0 inst.b.a = 1 if class_name == 'AP': await asyncio.sleep(0.01) assert inst.single_nested_count == 1 # Ensure watcher on initial value does not trigger event init_b.a = 2 if class_name == 'AP': await asyncio.sleep(0.01) assert inst.single_nested_count == 1 @pytest.mark.parametrize('class_name', ['P', 'AP'], indirect=True) async def test_param_instance_depends_dynamic_nested_changed_value(self, class_name): init_b = getattr(self, class_name)(a=1) inst = getattr(self, class_name)(b=init_b) pinfos = inst.param.method_dependencies('nested') assert len(pinfos) == 10 inst.b = getattr(self, class_name)(a=2) pinfos = inst.param.method_dependencies('nested') assert len(pinfos) == 10 pinfos = {(pi.inst, pi.name): pi for pi in pinfos} pinfo = pinfos[(inst, 'b')] assert pinfo.cls is getattr(self, class_name) assert pinfo.inst is inst assert pinfo.name == 'b' assert pinfo.what == 'value' for p in ['a', 'b', 'name', 'nested_count', 'single_count', 'attr_count']: pinfo2 = pinfos[(inst.b, p)] assert pinfo2.cls is getattr(self, class_name) assert pinfo2.inst is inst.b assert pinfo2.name == p assert pinfo2.what == 'value' if class_name == 'AP': await asyncio.sleep(0.01) assert inst.single_nested_count == 1 inst.b.a = 1 if class_name == 'AP': await asyncio.sleep(0.01) assert inst.single_nested_count == 2 # Ensure watcher on initial value does not trigger event init_b.a = 2 if class_name == 'AP': await asyncio.sleep(0.01) assert inst.single_nested_count == 2 @pytest.mark.parametrize('class_name', ['P', 'AP'], indirect=True) async def test_param_instance_depends(self, class_name): p = getattr(self, class_name)() pinfos = p.param.method_dependencies('single_parameter') assert len(pinfos) == 1 pinfo = pinfos[0] assert pinfo.cls is getattr(self, class_name) assert pinfo.inst is p assert pinfo.name == 'a' assert pinfo.what == 'value' p.a = 1 if class_name == 'AP': await asyncio.sleep(0.01) assert p.single_count == 1 @pytest.mark.parametrize('class_name', ['P', 'AP'], indirect=True) async def test_param_class_depends(self, class_name): pinfos = getattr(self, class_name).param.method_dependencies('single_parameter') assert len(pinfos) == 1 pinfo = pinfos[0] assert pinfo.cls is getattr(self, class_name) assert pinfo.inst is None assert pinfo.name == 'a' assert pinfo.what == 'value' @pytest.mark.parametrize('class_name', ['P', 'AP'], indirect=True) async def test_param_class_depends_constant(self, class_name): pinfos = getattr(self, class_name).param.method_dependencies('constant') assert len(pinfos) == 1 pinfo = pinfos[0] assert pinfo.cls is getattr(self, class_name) assert pinfo.inst is None assert pinfo.name == 'a' assert pinfo.what == 'constant' @pytest.mark.parametrize('class_name', ['P', 'AP'], indirect=True) async def test_param_inst_depends_nested(self, class_name): inst = getattr(self, class_name)(b=getattr(self, class_name)()) pinfos = inst.param.method_dependencies('nested') assert len(pinfos) == 10 pinfos = {(pi.inst, pi.name): pi for pi in pinfos} pinfo = pinfos[(inst, 'b')] assert pinfo.cls is getattr(self, class_name) assert pinfo.inst is inst assert pinfo.name == 'b' assert pinfo.what == 'value' for p in ['name', 'a', 'b']: info = pinfos[(inst.b, p)] assert info.name == p assert info.inst is inst.b @pytest.mark.parametrize('class_name', ['P2', 'AP2'], indirect=True) async def test_param_external_param_instance(self, class_name): inst = getattr(self, class_name)() pinfos = inst.param.method_dependencies('external_param') pinfo = pinfos[0] assert pinfo.cls is getattr(self, class_name[:-1]) assert pinfo.inst is None assert pinfo.name == 'a' assert pinfo.what == 'value' async def test_async(self): 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' await asyncio.sleep(0.01) assert inst.single_count == 1 def test_param_depends_on_parameterized_attribute(self): # Issue https://github.com/holoviz/param/issues/635 called = [] class Sub(param.Parameterized): s = param.String() class P(param.Parameterized): test_param = param.Parameter() def __init__(self, **params): self._sub = Sub() super().__init__(**params) @param.depends('_sub.s', watch=True) def cb(self): called.append(1) p = P() p.test_param = 'modified' assert not called def test_param_depends_on_parameterized_attribute_async(self): # Issue https://github.com/holoviz/param/issues/635 called = [] class Sub(param.Parameterized): s = param.String() class P(param.Parameterized): test_param = param.Parameter() def __init__(self, **params): self._sub = Sub() super().__init__(**params) @param.depends('_sub.s', watch=True) async def cb(self): called.append(1) p = P() p.test_param = 'modified' assert not called def test_param_depends_on_method(self): method_count = 0 class A(param.Parameterized): a = param.Integer() @param.depends('a', watch=True) def method1(self): pass @param.depends('method1', watch=True) def method2(self): nonlocal method_count method_count += 1 inst = A() pinfos = inst.param.method_dependencies('method2') assert len(pinfos) == 1 pinfo = pinfos[0] assert pinfo.cls is A assert pinfo.inst is inst assert pinfo.name == 'a' assert pinfo.what == 'value' inst.a = 2 assert method_count == 1 async def test_param_depends_on_method_async(self): method_count = 0 class A(param.Parameterized): a = param.Integer() @param.depends('a', watch=True) async def method1(self): pass @param.depends('method1', watch=True) async def method2(self): nonlocal method_count method_count += 1 inst = A() pinfos = inst.param.method_dependencies('method2') assert len(pinfos) == 1 pinfo = pinfos[0] assert pinfo.cls is A assert pinfo.inst is inst assert pinfo.name == 'a' assert pinfo.what == 'value' inst.a = 2 await asyncio.sleep(0.01) assert method_count == 1 def test_param_depends_on_method_subparameter(self): method1_count = 0 method2_count = 0 class Sub(param.Parameterized): a = param.Integer() @param.depends('a') def method1(self): nonlocal method1_count method1_count += 1 class Main(param.Parameterized): sub = param.Parameter() @param.depends('sub.method1', watch=True) def method2(self): nonlocal method2_count method2_count += 1 sub = Sub() main = Main(sub=sub) pinfos = main.param.method_dependencies('method2') assert len(pinfos) == 1 pinfo = pinfos[0] assert pinfo.cls is Sub assert pinfo.inst is sub assert pinfo.name == 'a' assert pinfo.what == 'value' sub.a = 2 assert method1_count == 0 assert method2_count == 1 async def test_param_depends_on_method_subparameter_async(self): method1_count = 0 method2_count = 0 class Sub(param.Parameterized): a = param.Integer() @param.depends('a') async def method1(self): nonlocal method1_count method1_count += 1 class Main(param.Parameterized): sub = param.Parameter() @param.depends('sub.method1', watch=True) async def method2(self): nonlocal method2_count method2_count += 1 sub = Sub() main = Main(sub=sub) pinfos = main.param.method_dependencies('method2') assert len(pinfos) == 1 pinfo = pinfos[0] assert pinfo.cls is Sub assert pinfo.inst is sub assert pinfo.name == 'a' assert pinfo.what == 'value' sub.a = 2 await asyncio.sleep(0.01) assert method1_count == 0 assert method2_count == 1 def test_param_depends_on_method_subparameter_after_init(self): # Setup inspired from https://github.com/holoviz/param/issues/764 method1_count = 0 method2_count = 0 class Controls(param.Parameterized): explorer = param.Parameter() @param.depends('explorer.method1', watch=True) def method2(self): nonlocal method2_count method2_count += 1 class Explorer(param.Parameterized): controls = param.Parameter() x = param.Selector(objects=['a', 'b']) def __init__(self, **params): super().__init__(**params) self.controls = Controls(explorer=self) @param.depends('x') def method1(self): nonlocal method1_count method1_count += 1 explorer = Explorer() pinfos = explorer.controls.param.method_dependencies('method2') assert len(pinfos) == 1 pinfo = pinfos[0] assert pinfo.cls is Explorer assert pinfo.inst is explorer assert pinfo.name == 'x' assert pinfo.what == 'value' explorer.x = 'b' assert method1_count == 0 assert method2_count == 1 async def test_param_depends_on_method_subparameter_after_init_async(self): # Setup inspired from https://github.com/holoviz/param/issues/764 method1_count = 0 method2_count = 0 class Controls(param.Parameterized): explorer = param.Parameter() @param.depends('explorer.method1', watch=True) async def method2(self): nonlocal method2_count method2_count += 1 class Explorer(param.Parameterized): controls = param.Parameter() x = param.Selector(objects=['a', 'b']) def __init__(self, **params): super().__init__(**params) self.controls = Controls(explorer=self) @param.depends('x') async def method1(self): nonlocal method1_count method1_count += 1 explorer = Explorer() pinfos = explorer.controls.param.method_dependencies('method2') assert len(pinfos) == 1 pinfo = pinfos[0] assert pinfo.cls is Explorer assert pinfo.inst is explorer assert pinfo.name == 'x' assert pinfo.what == 'value' explorer.x = 'b' await asyncio.sleep(0.01) assert method1_count == 0 assert method2_count == 1 def test_param_depends_class_with_len(self): # https://github.com/holoviz/param/issues/747 count = 0 class P(param.Parameterized): x = param.Parameter() @param.depends('x', watch=True) def debug(self): nonlocal count count += 1 # bool(P()) evaluates to False def __len__(self): return 0 p = P() p.x = 1 assert count == 1 def test_param_depends_subobject_before_super_init(self): count = 0 class X(param.Parameterized): p = param.Parameter() class Y(param.Parameterized): def __init__(self, **params): self.x = X() super().__init__(**params) # Check that creating this class doesn't error when resolving x.p @param.depends('x.p') def cb(self): nonlocal count count += 1 y = Y() pinfos = y.param.method_dependencies('cb') assert len(pinfos) == 1 pinfo = pinfos[0] assert pinfo.inst == y.x assert pinfo.cls == X assert pinfo.name == 'p' class TestParamDependsFunction: def setup_method(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 } assert 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 } assert 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 assert d == [4] p.b = 3 assert 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 assert d == [4] p.b = 3 assert d == [4, 5] async def test_async(self): p = self.P(a=1) d = [] @param.depends(p.param.a, watch=True) async def function(value): d.append(value) p.a = 2 await asyncio.sleep(0.01) assert d == [2] def test_misspelled_parameter_in_depends(): 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") # <- Misspelled xlim def test(self): return True def test_misspelled_parameter_in_depends_non_Parameter(): with pytest.raises(AttributeError, match="Attribute 'foo' could not be resolved on"): class Example(param.Parameterized): foo = 1 @param.depends("foo") # <- Not a Parameter def test(self): return True 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 def test_param_depends_on_undefined_attribute(): class P2(param.Parameterized): value = param.String() class P1(param.Parameterized): def __init__(self, **params): super().__init__(**params) self.p2 = P2() @param.depends('p2.value', watch=True) def cb(self): print('fired') with pytest.raises(AttributeError) as excinfo: P1() assert "Dependency 'p2' could not be resolved" in str(excinfo.value) param-2.1.1/tests/testparameterizedobject.py000066400000000000000000001327121463636336300212750ustar00rootroot00000000000000""" Unit test for Parameterized. """ import inspect import re import unittest import param import numbergen from .utils import MockLoggingHandler # CEBALERT: not anything like a complete test of Parameterized! import pytest import random from param import parameterized, Parameter from param._utils import _dict_update from param.parameterized import ( ParamOverrides, Undefined, default_label_formatter, no_instance_params, shared_parameters, ) class _SomeRandomNumbers: 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(unittest.TestCase): @classmethod def setUpClass(cls): super().setUpClass() log = param.parameterized.get_logger() cls.log_handler = MockLoggingHandler(level='DEBUG') log.addHandler(cls.log_handler) def test_name_default_to_class_name(self): assert TestPO.name == 'TestPO' def test_name_default_to_subclass_name(self): class Foo(TestPO): pass assert Foo.name == 'Foo' def test_name_as_argument(self): testpo = TestPO(name='custom') assert testpo.name == 'custom' def test_name_instance_generated(self): testpo = TestPO() match = re.fullmatch(r'TestPO\w{5}', testpo.name) assert match is not None def test_name_instance_generated_subclass(self): class Foo(TestPO): pass foo = Foo() match = re.fullmatch(r'Foo\w{5}', foo.name) assert match is not None def test_name_instance_generated_class_name_reset(self): class P(param.Parameterized): pass P.name = 'Other' assert P.name == 'Other' p = P() assert p.name == 'Other' def test_parameter_name_fixed(self): testpo = TestPO() with pytest.raises(AttributeError): testpo.param.const.name = 'notconst' def test_name_overriden(self): class P(param.Parameterized): name = param.String(default='other') assert P.name == 'other' p = P() assert p.name == 'other' def test_name_overriden_without_default(self): class A(param.Parameterized): pass class B(param.Parameterized): name = param.String(doc='some help') class C(B): pass assert B.name == 'B' assert B.param.name.doc == 'some help' assert C.name == 'C' assert C.param.name.doc == 'some help' def test_name_overriden_constructor(self): class P(param.Parameterized): name = param.String(default='other') p = P(name='another') assert p.name == 'another' def test_name_overriden_subclasses(self): class P(param.Parameterized): name = param.String(default='other') class Q(P): pass class R(Q): name = param.String(default='yetanother') assert Q.name == 'other' q1 = Q() assert q1.name == 'other' q2 = Q(name='another') assert q2.name == 'another' assert R.name == 'yetanother' r1 = R() assert r1.name == 'yetanother' r2 = R(name='last') assert r2.name == 'last' def test_name_overriden_subclasses_name_set(self): class P(param.Parameterized): name = param.String(default='other') class Q(P): pass P.name = 'another' assert Q.name == 'another' Q.name = 'yetanother' assert Q.name == 'yetanother' q = Q() assert q.name == 'yetanother' def test_name_overriden_error_not_String(self): msg = "Parameterized class 'P' cannot override the 'name' Parameter " \ "with type . Overriding 'name' is only allowed with " \ "a 'String' Parameter." with pytest.raises(TypeError, match=msg): class P(param.Parameterized): name = 'other' msg = "Parameterized class 'P' cannot override the 'name' Parameter " \ "with type . Overriding 'name' " \ "is only allowed with a 'String' Parameter." with pytest.raises(TypeError, match=msg): class P(param.Parameterized): # noqa name = param.Parameter(default='other') def test_name_complex_hierarchy(self): class Mixin1: pass class Mixin2: pass class Mixin3(param.Parameterized): pass class A(param.Parameterized, Mixin1): pass class B(A): pass class C(B, Mixin2): pass class D(C, Mixin3): pass assert A.name == 'A' assert B.name == 'B' assert C.name == 'C' assert D.name == 'D' def test_name_overriden_complex_hierarchy(self): class Mixin1: pass class Mixin2: pass class Mixin3(param.Parameterized): pass class A(param.Parameterized, Mixin1): pass class B(A): name = param.String(default='other') class C(B, Mixin2): name = param.String(default='another') class D(C, Mixin3): pass assert A.name == 'A' assert B.name == 'other' assert C.name == 'another' assert D.name == 'another' def test_name_overriden_multiple(self): class A(param.Parameterized): name = param.String(default='AA') class B(param.Parameterized): name = param.String(default='BB') class C(A, B): pass assert C.name == 'AA' def test_constant_parameter_modify_class_before(self): """Test you can set on class and the new default is picked up by new instances""" TestPO.const=9 testpo = TestPO() self.assertEqual(testpo.const,9) def test_constant_parameter_modify_class_after_init(self): """Test that setting the value on the class doesn't update the instance value even when the instance value hasn't yet been set""" oobj = [] class P(param.Parameterized): x = param.Parameter(default=oobj, constant=True) p1 = P() P.x = nobj = [0] assert P.x is nobj assert p1.x == oobj assert p1.x is oobj p2 = P() assert p2.x == nobj assert p2.x is nobj def test_constant_parameter_after_init(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) 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['ro'].constant,True) # check that instantiate was ignored for readonly self.assertEqual(testpo.param['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['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_instantiation_set_before_super(self): count = 0 class P(param.Parameterized): x = param.Parameter(0) def __init__(self, x=1): self.x = x super().__init__() @param.depends('x', watch=True) def cb(self): nonlocal count count += 1 p = P() assert p.x == 1 assert count == 0 def test_instantiation_set_before_super_contrived(self): # https://github.com/holoviz/param/pull/790#discussion_r1263483293 class P(param.Parameterized): value = param.String(default="A") def __init__(self, depth=0): self.value = 'B' if depth < 2: self.sub = P(depth+1) super().__init__() p = P() assert p.value == 'B' assert p.sub.value == 'B' def test_instantiation_set_before_super_subclass(self): # Inspired by a HoloViews use case (GenericElementPlot, GenericOverlayPlot) class A(param.Parameterized): def __init__(self, batched=False, **params): self.batched = batched super().__init__(**params) class B(A): batched = param.Boolean() def __init__(self, batched=True, **params): super().__init__(batched=batched, **params) a = A() assert a.batched is False # When b is instantiated the `batched` Parameter of B is set before # Parameterized.__init__ is called. b = B() assert b.batched is True def test_instantiation_param_objects_before_super_subclass(self): # Testing https://github.com/holoviz/param/pull/420 class P(param.Parameterized): x = param.Parameter() def __init__(self): objs = self.param.objects(instance='existing') assert isinstance(objs, dict) super().__init__() P() @pytest.mark.xfail( raises=AttributeError, reason='Behavior not defined when setting a constant parameter before calling super()', ) def test_instantiation_set_before_super_constant(self): count = 0 class P(param.Parameterized): x = param.Parameter(0, constant=True) def __init__(self, x=1): self.x = x super().__init__() @param.depends('x', watch=True) def cb(self): nonlocal count count += 1 p = P() assert p.x == 1 assert count == 0 def test_instantiation_set_before_super_readonly(self): class P(param.Parameterized): x = param.Parameter(0, readonly=True) def __init__(self, x=1): self.x = x super().__init__() with pytest.raises(TypeError, match="Read-only parameter 'x' cannot be modified"): P() def test_parameter_constant_iadd_allowed(self): # Testing https://github.com/holoviz/param/pull/400 class P(param.Parameterized): list = param.List([], constant=True) p = P() p.list += [1, 2, 3] # Just to make sure that normal setting is still forbidden with pytest.raises(TypeError, match="Constant parameter 'list' cannot be modified"): p.list = [0] def test_parameter_constant_same_notallowed(self): L = [0, 1] class P(param.Parameterized): list = param.List(L, constant=True) p = P() # instantiate is set to true internally so a deepcopy is made of L, # it's no longer the same object with pytest.raises(TypeError, match="Constant parameter 'list' cannot be modified"): p.list = L def test_values(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.values() assert len(param.Parameterized.param.values()) in [1,2] ## check for bug where subclass Parameters were not showing up ## if values() already called on a super class. assert 'inst' in TestPO.param.values() assert 'notinst' in TestPO.param.values() def test_values_name_ignored_for_instances_and_onlychanged(self): default_inst = param.Parameterized() assert 'Parameterized' in default_inst.name # name ignored when automatically computed (behavior inherited from all_equal) assert 'name' not in default_inst.param.values(onlychanged=True) # name not ignored when set assert param.Parameterized(name='foo').param.values(onlychanged=True)['name'] == 'foo' 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_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 .param.pprint does not make instance parameter copies test = TestPO() test.param.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_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.param._state_push() t.dyn assert t.param.inspect_value('dyn')!=orig t.param._state_pop() assert t.param.inspect_value('dyn')==orig def test_label(self): t = TestPO() assert t.param['ro_label'].label == 'Ro Label' def test_label_set(self): t = TestPO() assert t.param['ro_label'].label == 'Ro Label' t.param['ro_label'].label = 'Ro relabeled' assert t.param['ro_label'].label == 'Ro relabeled' def test_label_default_format(self): t = TestPO() assert t.param['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['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['ro_format'].label == 'Foo' param.parameterized.label_formatter = default_label_formatter def test_error_if_non_param_in_constructor(self): msg = "TestPO.__init__() got an unexpected keyword argument 'not_a_param'" with pytest.raises(TypeError, match=re.escape(msg)): TestPO(not_a_param=2) def test_update_class(self): class P(param.Parameterized): x = param.Parameter() p = P() P.param.update(x=10) assert P.x == p.x == 10 def test_update_context_class(self): class P(param.Parameterized): x = param.Parameter(10) p = P() with P.param.update(x=20): assert P.x == p.x == 20 assert P.x == p.x == 10 def test_update_class_watcher(self): class P(param.Parameterized): x = param.Parameter() events = [] P.param.watch(events.append, 'x') P.param.update(x=10) assert len(events) == 1 assert events[0].name == 'x' and events[0].new == 10 def test_update_context_class_watcher(self): class P(param.Parameterized): x = param.Parameter(0) events = [] P.param.watch(events.append, 'x') with P.param.update(x=20): pass assert len(events) == 2 assert events[0].name == 'x' and events[0].new == 20 assert events[1].name == 'x' and events[1].new == 0 def test_update_instance_watcher(self): class P(param.Parameterized): x = param.Parameter() p = P() events = [] p.param.watch(events.append, 'x') p.param.update(x=10) assert len(events) == 1 assert events[0].name == 'x' and events[0].new == 10 def test_update_context_instance_watcher(self): class P(param.Parameterized): x = param.Parameter(0) p = P() events = [] p.param.watch(events.append, 'x') with p.param.update(x=20): pass assert len(events) == 2 assert events[0].name == 'x' and events[0].new == 20 assert events[1].name == 'x' and events[1].new == 0 def test_update_error_not_param_class(self): with pytest.raises(ValueError, match="'not_a_param' is not a parameter of TestPO"): TestPO.param.update(not_a_param=1) def test_update_error_not_param_instance(self): t = TestPO(inst='foo') with pytest.raises(ValueError, match="'not_a_param' is not a parameter of TestPO"): t.param.update(not_a_param=1) def test_update_context_error_not_param_class(self): with pytest.raises(ValueError, match="'not_a_param' is not a parameter of TestPO"): with TestPO.param.update(not_a_param=1): pass def test_update_context_error_not_param_instance(self): t = TestPO(inst='foo') with pytest.raises(ValueError, match="'not_a_param' is not a parameter of TestPO"): with t.param.update(not_a_param=1): pass def test_update_error_while_updating(self): class P(param.Parameterized): x = param.Parameter(0, readonly=True) with pytest.raises(TypeError): P.param.update(x=1) assert P.x == 0 with pytest.raises(TypeError): with P.param.update(x=1): pass assert P.x == 0 p = P() with pytest.raises(TypeError): p.param.update(x=1) assert p.x == 0 with pytest.raises(TypeError): with p.param.update(x=1): pass assert p.x == 0 def test_update_dict_and_kwargs_instance(self): t = TestPO(inst='foo', notinst=0) t.param.update(dict(notinst=1, inst='bar'), notinst=2) assert t.notinst == 2 assert t.inst == 'bar' def test_update_context_dict_and_kwargs_instance(self): t = TestPO(inst='foo', notinst=0) with t.param.update(dict(notinst=1, inst='bar'), notinst=2): assert t.notinst == 2 assert t.inst == 'bar' assert t.notinst == 0 assert t.inst == 'foo' def test_update_context_single_parameter(self): t = TestPO(inst='foo') with t.param.update(inst='bar'): assert t.inst == 'bar' assert t.inst == 'foo' def test_update_context_does_not_set_other_params(self): t = TestPO(inst='foo') events = [] t.param.watch(events.append, list(t.param), onlychanged=False) with t.param.update(inst='bar'): pass assert len(events) == 2 assert all(e.name == 'inst' for e in events) def test_update_context_multi_parameter(self): t = TestPO(inst='foo', notinst=1) with t.param.update(inst='bar', notinst=2): assert t.inst == 'bar' assert t.notinst == 2 assert t.inst == 'foo' assert t.notinst == 1 def test_constant_readonly_parameterized(self): class ParamClass(param.Parameterized): x = param.Number() pc1 = ParamClass(name="FOO1") pc2 = ParamClass(name="FOO2") class P(param.Parameterized): ro = param.Parameter(pc1, constant=True) const = param.Parameter(pc2, readonly=True) ro_i = param.Parameter(pc1, constant=True, instantiate=True) const_i = param.Parameter(pc2, readonly=True, instantiate=True) p = P() assert p.ro.name == 'FOO1' assert p.const.name == 'FOO2' assert p.ro_i.name.startswith('ParamClass0') assert p.const_i.name == 'FOO2' 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().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(unittest.TestCase): def setUp(self): super().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(unittest.TestCase): def setUp(self): super().setUp() self.po = param.Parameterized(name='A') 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') # 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): super().setUp() with shared_parameters(): self.p1 = TestPO(name='A') self.p2 = TestPO(name='B') self.ap1 = AnotherTestPO(name='A') self.ap2 = AnotherTestPO(name='B') def test_shared_object(self): self.assertTrue(self.ap1.instPO is self.ap2.instPO) self.assertTrue(self.ap1.param['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['inst'].default is not self.p2.inst) def test_inheritance_None_is_not_special_cased_default(): class A(param.Parameterized): p = param.String(default='test') class B(A): p = param.String(default=None) b = B() assert b.p is None @pytest.mark.parametrize('attribute', [ 'default', 'doc', 'precedence', 'readonly', 'pickle_default_value', 'per_instance', # These 3 parameters of Parameter are handled dynamically, instantiating # Parameter with their value to None doesn't lead to the attribute value # on Parameter being None. # 'instantiate', # 'constant', # 'allow_None', ]) def test_inheritance_None_is_not_special_cased(attribute): """ Somewhat strange test as it's setting attributes of Parameter to None while it's not necessarily an allowed value. It's to test that it's no longer considered as a sentinel allowing inheritance. """ class A(param.Parameterized): p = param.Parameter(**{attribute: 'test'}) class B(A): p = param.Parameter(**{attribute: None}) b = B() assert getattr(b.param.p, attribute) is None def test_inheritance_no_default_declared_in_subclass(): default = 5.0 class A(param.Parameterized): p = param.Number(default=default) class B(A): p = param.Number() b = B() assert b.p == 5.0 def test_inheritance_attribute_from_non_subclass_not_inherited(): class A(param.Parameterized): p = param.String(doc='1') class B(A): p = param.Number(default=0.1) b = B() assert b.param.p.doc == '1' def test_inheritance_sub_attribute_is_used(): class A(param.Parameterized): p = param.String(doc='1') class B(A): p = param.String(doc='2') b = B() assert b.param.p.doc == '2' def test_inheritance_default_is_not_None_in_sub(): class A(param.Parameterized): p = param.String(default='1') class B(A): p = param.Number(default=0.1) b = B() assert b.p == 0.1 def test_inheritance_default_is_None_in_sub(): class A(param.Parameterized): p = param.Tuple(default=(0, 1)) class B(A): p = param.NumericTuple() b = B() assert b.p == (0, 1) def test_inheritance_diamond_not_supported(): """ In regular Python, the value of the class attribute p on D is resolved to 2: class A: p = 1 class B(A): pass class C(A): p = 2 class D(B, C): pass assert D.p == 2 This is not supported by Param (https://github.com/holoviz/param/issues/715). """ class A(param.Parameterized): p = param.Parameter(default=1, doc='11') class B(A): p = param.Parameter() class C(A): p = param.Parameter(default=2, doc='22') class D(B, C): p = param.Parameter() assert D.p != 2 assert D.param.p.doc != '22' assert D.p == 1 assert D.param.p.doc == '11' d = D() assert d.p != 2 assert d.param.p.doc != '22' assert d.p == 1 assert d.param.p.doc == '11' def test_inheritance_from_multiple_params_class(): class A(param.Parameterized): p = param.Parameter(doc='foo') class B(A): p = param.Parameter(default=2) class C(B): p = param.Parameter(instantiate=True) assert A.param.p.instantiate is False assert A.param.p.default is None assert A.param.p.doc == 'foo' assert B.param.p.instantiate is False assert B.param.p.default == 2 assert B.param.p.doc == 'foo' assert C.param.p.instantiate is True assert C.param.p.default == 2 assert C.param.p.doc == 'foo' def test_inheritance_from_multiple_params_inst(): # Picked Parameters whose default value is None class A(param.Parameterized): p = param.Parameter(doc='foo') class B(A): p = param.Dict(default={'foo': 'bar'}) class C(B): p = param.ClassSelector(class_=object, allow_None=True) a = A() b = B() c = C() assert a.param.p.allow_None is True assert a.param.p.default is None assert a.param.p.doc == 'foo' assert b.param.p.allow_None is False assert b.param.p.default == {'foo': 'bar'} assert b.param.p.doc == 'foo' assert c.param.p.allow_None is True assert c.param.p.default == {'foo': 'bar'} assert c.param.p.doc == 'foo' def test_inheritance_from_multiple_params_intermediate_setting(): class A(param.Parameterized): p = param.Parameter(doc='foo') A.param.p.default = 1 A.param.p.doc = 'bar' class B(A): p = param.Dict(default={'foo': 'bar'}) assert A.param.p.default == 1 assert A.param.p.doc == 'bar' assert B.param.p.default == {'foo': 'bar'} assert B.param.p.doc == 'bar' a = A() b = B() assert a.param.p.default == 1 assert a.param.p.doc == 'bar' assert b.param.p.default == {'foo': 'bar'} assert b.param.p.doc == 'bar' def test_inheritance_instantiate_behavior(): class A(param.Parameterized): p = param.Parameter(instantiate=True) class B(A): p = param.Parameter(readonly=True) # Normally, param.Parameter(readonly=True, instantiate=True) ends up with # instantiate being False. assert B.param.p.instantiate is True b = B() assert b.param.p.instantiate is True def test_inheritance_constant_behavior(): class A(param.Parameterized): p = param.Parameter(readonly=True) class B(A): p = param.Parameter() # Normally, param.Parameter(readonly=True) ends up with constant being # True. assert B.param.p.constant is False b = B() assert b.param.p.constant is False def test_inheritance_set_Parameter_instantiate_constant_before_instantation(): # https://github.com/holoviz/param/issues/760 class A(param.Parameterized): p0 = param.Parameter() p1 = param.Parameter(instantiate=True) p2 = param.Parameter(constant=True) class B(A): pass B.p0 = B.p1 = B.p2 = 2 b = B() assert b.p0 == 2 assert b.p1 == 2 assert b.p2 == 2 def test_inheritance_allow_None_behavior(): class A(param.Parameterized): p = param.Parameter(default=1) class B(A): p = param.Parameter() # A computes allow_None to False, B sets it to True. assert A.param.p.allow_None != B.param.p.allow_None assert B.param.p.allow_None is True a = A() b = B() assert a.param.p.allow_None != b.param.p.allow_None assert b.param.p.allow_None is True def test_inheritance_allow_None_behavior2(): class A(param.Parameterized): p = param.Parameter(allow_None=False) class B(A): p = param.Parameter(default=None) # A says None is not allowed, B sets the default to None and recomputes # allow_None. assert B.param.p.allow_None is True b = B() assert b.param.p.allow_None is True def test_inheritance_class_attribute_behavior(): class A(param.Parameterized): p = param.Parameter(1) class B(A): p = param.Parameter() assert B.p == 1 A.p = 2 # Should be 2? # https://github.com/holoviz/param/issues/718 assert B.p == 1 class TestShallowCopyMutableAttributes: @pytest.fixture def foo(self): class Foo: def __init__(self, val): self.val = val return Foo @pytest.fixture def custom_param(self): class CustomParameter(Parameter): __slots__ = ['container'] _slot_defaults = _dict_update(Parameter._slot_defaults, container=None) def __init__(self, default=Undefined, *, container=Undefined, **kwargs): super().__init__(default=default, **kwargs) self.container = container return CustomParameter def test_shallow_copy_on_class_creation(self, custom_param, foo): clist = [foo(1), foo(2)] class P(param.Parameterized): cp = custom_param(container=clist) # the mutable container has been shallow-copied assert P.param.cp.container is not clist assert all(cval is val for cval, val in zip(P.param.cp.container, clist)) def test_shallow_copy_inheritance_each_level(self, custom_param): clist = [1, 2] class A(param.Parameterized): p = custom_param(container=clist) class B(A): p = custom_param(default=1) clist.append(3) assert A.param.p.container == [1, 2] assert B.param.p.container == [1, 2] B.param.p.container.append(4) assert A.param.p.container == [1, 2] assert B.param.p.container == [1, 2, 4] def test_shallow_copy_on_instance_getitem(self, custom_param, foo): clist = [foo(1), foo(2)] class P(param.Parameterized): cp = custom_param(container=clist) p = P() assert 'cp' not in p._param__private.params p.param['cp'] # the mutable container has been shallow-copied assert 'cp' in p._param__private.params assert P.param.cp.container == p._param__private.params['cp'].container assert P.param.cp.container is not p._param__private.params['cp'].container assert all(cval is val for cval, val in zip(p.param.cp.container, clist)) def test_shallow_copy_on_instance_set(self, custom_param, foo): clist = [foo(1), foo(2)] class P(param.Parameterized): cp = custom_param(container=clist) p = P() assert 'cp' not in p._param__private.params p.cp = 'value' # the mutable container has been shallow-copied assert 'cp' in p._param__private.params assert P.param.cp.container == p._param__private.params['cp'].container assert P.param.cp.container is not p._param__private.params['cp'].container assert all(cval is val for cval, val in zip(p.param.cp.container, clist)) def test_modify_class_container_before_shallow_copy(self, custom_param, foo): clist = [foo(1), foo(2)] clist2 = [foo(3), foo(4)] class P(param.Parameterized): cp = custom_param(container=clist) p1 = P() p2 = P() # Setting the class container will affect instances are the shallow copy # is lazy and has not yet been made. P.param.cp.container = clist2 assert p1.param.cp.container == clist2 assert p2.param.cp.container == clist2 @pytest.fixture def custom_parameter1(): class CustomParameter(param.Parameter): __slots__ = ['foo', 'bar'] # foo has no default value defined in _slot_defaults def __init__(self, foo=param.Undefined, **params): super().__init__(**params) self.foo = foo return CustomParameter def test_inheritance_parameter_attribute_without_default(): class CustomParameter(param.Parameter): __slots__ = ['foo'] # foo has no default value defined in _slot_defaults def __init__(self, foo=param.Undefined, **params): super().__init__(**params) self.foo = foo with pytest.raises(KeyError, match="Slot 'foo' of parameter 'c' has no default value defined in `_slot_defaults`"): class A(param.Parameterized): c = CustomParameter() def _dir(obj): return [attr for attr in dir(obj) if not attr.startswith('__')] def test_namespace_class(): class P(param.Parameterized): x = param.Parameter() @param.depends('x', watch=True) def foo(self): pass P.x = 1 P.param.x assert _dir(P) == [ '_param__parameters', '_param__private', '_param_watchers', 'foo', 'name', 'param', 'x' ] def test_namespace_inst(): class P(param.Parameterized): x = param.Parameter() @param.depends('x', watch=True) def foo(self): pass p = P(x=2) p.param.x assert _dir(p) == [ '_param__parameters', '_param__private', '_param_watchers', 'foo', 'name', 'param', 'x' ] def test_parameterized_access_param_before_super(): class P(param.Parameterized): x = param.Parameter(1) def __init__(self, **params): # Reaching out to a Parameter default before calling super assert self.x == 1 super().__init__(**params) P() def check_signature(parameterized_obj, parameters): assert parameterized_obj.__signature__ is not None sig = inspect.signature(parameterized_obj) assert len(parameters) == len(sig.parameters) for sparam, pname in zip(sig.parameters.values(), parameters): assert sparam.name == pname assert sparam.kind == inspect.Parameter.KEYWORD_ONLY def test_parameterized_signature_base(): check_signature(param.Parameterized, ['name']) def test_parameterized_signature_simple(): class P(param.Parameterized): x = param.Parameter() check_signature(P, ['x', 'name']) def test_parameterized_signature_subclass_noparams(): class A(param.Parameterized): x = param.Parameter() class B(A): pass check_signature(B, ['x', 'name']) def test_parameterized_signature_subclass_with_params(): class A(param.Parameterized): a1 = param.Parameter() a2 = param.Parameter() class B(A): b1 = param.Parameter() b2 = param.Parameter() class C(B): c1 = param.Parameter() c2 = param.Parameter() check_signature(A, ['a1', 'a2', 'name']) check_signature(B, ['b1', 'b2', 'a1', 'a2', 'name']) check_signature(C, ['c1', 'c2', 'b1', 'b2', 'a1', 'a2', 'name']) def test_parameterized_signature_subclass_multiple_inheritance(): class A(param.Parameterized): a1 = param.Parameter() a2 = param.Parameter() class B(param.Parameterized): b1 = param.Parameter() b2 = param.Parameter() class C(A, B): c1 = param.Parameter() c2 = param.Parameter() check_signature(C, ['c1', 'c2', 'a1', 'a2', 'b1', 'b2', 'name']) def test_parameterized_signature_simple_init_same_as_parameterized(): class P(param.Parameterized): x = param.Parameter() def __init__(self, **params): super().__init__(**params) check_signature(P, ['x', 'name']) def test_parameterized_signature_simple_init_different(): class P(param.Parameterized): x = param.Parameter() def __init__(self, x=1, **params): super().__init__(x=x, **params) assert P.__signature__ is None def test_parameterized_signature_subclass_noparams_init_different(): class A(param.Parameterized): x = param.Parameter() class B(A): def __init__(self, x=1, **params): super().__init__(x=x, **params) check_signature(A, ['x', 'name']) assert B.__signature__ is None def test_parameterized_signature_subclass_with_params_init_different(): class A(param.Parameterized): a1 = param.Parameter() a2 = param.Parameter() class B(A): b1 = param.Parameter() b2 = param.Parameter() class C(B): c1 = param.Parameter() c2 = param.Parameter() def __init__(self, c1=1, **params): super().__init__(c1=1, **params) check_signature(A, ['a1', 'a2', 'name']) check_signature(B, ['b1', 'b2', 'a1', 'a2', 'name']) assert C.__signature__ is None def test_parameterized_signature_subclass_multiple_inheritance_init_different(): class A(param.Parameterized): a1 = param.Parameter() a2 = param.Parameter() class B(param.Parameterized): b1 = param.Parameter() b2 = param.Parameter() def __init__(self, b1=1, **params): super().__init__(b1=1, **params) class C(A, B): c1 = param.Parameter() c2 = param.Parameter() check_signature(A, ['a1', 'a2', 'name']) assert B.__signature__ is None assert C.__signature__ is None param-2.1.1/tests/testparameterizedrepr.py000066400000000000000000000176501463636336300210020ustar00rootroot00000000000000""" Unit test for the repr and pprint of parameterized objects, and for pprint/script_repr. """ import inspect import unittest import param import pytest class TestParameterizedRepr(unittest.TestCase): def setUp(self): super().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().__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().__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().__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().__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().__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.param.pprint(), "A(4, 'B', name='test')") def testparameterizedscriptrepr2(self): obj = self.A(4,'B', c=5, name='test') self.assertEqual(obj.param.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.param.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.param.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.param.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.param.pprint(), "B(4, 'B', c=99)") def testparameterizedscriptrepr_varags(self): obj = self.C(4,'C', c=99) self.assertEqual(obj.param.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.param.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.param.pprint(), "D(4, 'D', c=, d=)") def testparameterizedscriptrepr_nonparams(self): obj = self.E(10,q='hi', a=99) self.assertEqual(obj.param.pprint(), "E(, q=, a=99)") def testparameterizedscriptrepr_recursive(self): class Q(param.Parameterized): a = param.Number(default=39, bounds=(0,50), doc='Number a') b = param.String(default="str", doc='A string') class P(Q): c = param.ClassSelector(default=Q(), class_=Q, doc='An instance of Q') e = param.ClassSelector(default=param.Parameterized(), class_=param.Parameterized, doc='A Parameterized instance') f = param.Range(default=(0,1), doc='A range') p = P(f=(2,3), name="demo") p.c = P(c=p) assert p.param.pprint() == "P(c=P(c=..., e=Parameterized()), e=Parameterized(), f=(2,3), name='demo')" def test_exceptions(self): obj = self.E(10,q='hi',a=99) try: obj.param.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.param.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.param.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.param.pprint(qualify=False), r) self.assertEqual(obj.param.pprint(qualify=True), "tests.testparameterizedrepr."+r) @pytest.fixture def P(): class P(param.Parameterized): x = param.Parameter() y = param.Parameter() def __init__(self, x, **params): params['x'] = x super().__init__(**params) return P def test_pprint_type(P): assert param.parameterized.pprint(P) == f'{__name__}.P' def test_pprint_parameterized_instance(P): p = P(1, y=2) assert param.parameterized.pprint(p) == 'P(1,\n y=2)' def test_pprint_parameterized_other(): assert param.parameterized.pprint('2') == repr('2') def test_script_repr_type(P): assert param.script_repr(P) == f'import {__name__}\n\n{__name__}.P' def test_script_repr_parameterized_instance(P): p = P(1, y=2) sr = param.script_repr(p) assert f'import {__name__.split(".")[0]}' in sr assert f'import {__name__}' in sr assert f'{__name__}.P(1,\n\n y=2)' in sr def test_script_repr_parameterized_other(): assert param.script_repr('2') == "\n\n'2'" def test_pprint_signature_overriden(): # https://github.com/holoviz/param/issues/785 class P(param.Parameterized): def __init__(self, **params): pass class T(P): pass t = T() # This is actually setting the signature of P.__init__ # as T doesn't define __init__ # bad T.__init__.__signature__ = inspect.Signature( [ inspect.Parameter('test', inspect.Parameter.KEYWORD_ONLY), ] ) with pytest.raises(KeyError, match=r"'T\.__init__\.__signature__' must contain 'self' as its first Parameter"): t.param.pprint() # good T.__init__.__signature__ = inspect.Signature( [ inspect.Parameter('self', inspect.Parameter.POSITIONAL_OR_KEYWORD), inspect.Parameter('test', inspect.Parameter.KEYWORD_ONLY), ] ) assert t.param.pprint() == 'T()' param-2.1.1/tests/testparamoutput.py000066400000000000000000000151751463636336300176360ustar00rootroot00000000000000""" Unit test for param.output. """ import unittest import param class TestParamDepends(unittest.TestCase): 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): 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-2.1.1/tests/testparamunion.py000066400000000000000000000034551463636336300174240ustar00rootroot00000000000000""" UnitTest for param_union helper """ import logging import unittest import param class MyHandler(logging.StreamHandler): def __init__(self): super().__init__() self.records = [] def emit(self, record): self.records.append(record) class TestParamUnion(unittest.TestCase): 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-2.1.1/tests/testpathparam.py000066400000000000000000000326641463636336300172340ustar00rootroot00000000000000import os import re import shutil import tempfile import unittest import param import pytest from .utils import check_defaults class TestPathParameters(unittest.TestCase): def setUp(self): super().setUp() tmpdir1 = tempfile.mkdtemp() self.curdir = os.getcwd() # Changing the directory to tmpdir1 to test that Path resolves relative # paths to absolute paths automatically. os.chdir(tmpdir1) fa = os.path.join(tmpdir1, 'a.txt') fb = os.path.join(tmpdir1, 'b.txt') fc = 'c.txt' open(fa, 'w').close() open(fb, 'w').close() open(fc, 'w').close() self.tmpdir1 = tmpdir1 self.fa = fa self.fb = fb self.fc = fc class P(param.Parameterized): a = param.Path() b = param.Path(self.fb) c = param.Path('a.txt', search_paths=[tmpdir1]) d = param.Path(check_exists=False) e = param.Path(self.fc, check_exists=False) f = param.Path(self.fc) self.P = P def tearDown(self): os.chdir(self.curdir) def _check_defaults(self, p): assert p.default is None assert p.allow_None is True assert p.search_paths == [] assert p.check_exists is True def test_defaults_class(self): class P(param.Parameterized): p = param.Path() check_defaults(P.param.p, label='P') self._check_defaults(P.param.p) def test_defaults_inst(self): class P(param.Parameterized): p = param.Path() p = P() check_defaults(p.param.p, label='P') self._check_defaults(p.param.p) def test_defaults_unbound(self): p = param.Path() check_defaults(p, label=None) self._check_defaults(p) def test_no_path_class(self): assert self.P.a is None def test_no_path_inst(self): p = self.P() assert p.a is None def test_unsupported_type(self): with pytest.raises(ValueError): param.Path(1) def test_inst_with_path(self): p = self.P(a=self.fa) assert isinstance(p.a, str) assert os.path.isfile(p.a) assert os.path.isabs(p.a) assert p.a == self.fa def test_set_to_None_allowed(self): p = self.P() assert p.param.b.allow_None is False # This should probably raise an error (#708) with pytest.raises(ValueError, match=re.escape(r"Path parameter 'P.b' does not accept None")): p.b = None def test_relative_cwd_class(self): assert os.path.isabs(self.P.f) def test_relative_cwd_class_set(self): self.P.a = self.fc assert os.path.isabs(self.P.a) def test_relative_cwd_inst(self): assert os.path.isabs(self.P().f) def test_relative_cwd_instantiation(self): p = self.P(a=self.fc) assert os.path.isabs(p.a) def test_relative_cwd_set(self): p = self.P() p.a = self.fc assert os.path.isabs(p.a) def test_search_paths(self): p = self.P() assert isinstance(p.c, str) assert os.path.isfile(p.c) assert os.path.isabs(p.c) assert p.c == self.fa def test_inheritance_behavior(self): # Inheritance isn't working great with search_paths and this test # isn't designed to be run from the tmpdir directory. startd = os.getcwd() try: # a = param.Path() # b = param.Path(self.fb) # c = param.Path('a.txt', search_paths=[tmpdir1]) class B(self.P): a = param.Path() b = param.Path() c = param.Path() os.chdir(self.curdir) assert B.a is None assert B.b == self.fb # search_paths is empty instead of [tmpdir1] and getting c raises an error assert B.param.c.search_paths == [] with pytest.raises(OSError, match='Path a.txt was not found'): assert B.c is None b = B() assert b.a is None assert b.b == self.fb assert b.param.c.search_paths == [] with pytest.raises(OSError, match='Path a.txt was not found'): assert b.c is None finally: os.chdir(startd) def test_notfound_instantiation_raises_error(self): with pytest.raises( OSError, match=r"Path file was not found in the following place\(s\): \['\S+(\\\\|/)non(\\\\|/)existing(\\\\|/)file'\]" ): param.Path('non/existing/file') def test_set_notfound_raises_error(self): p = self.P() with pytest.raises( OSError, match=r"Path file was not found in the following place\(s\): \['\S+(\\\\|/)non(\\\\|/)existing(\\\\|/)file'\]" ): p.a = 'non/existing/file' def test_set_notfound_class_raises_error(self): with pytest.raises( OSError, match=r"Path file was not found in the following place\(s\): \['\S+(\\\\|/)non(\\\\|/)existing(\\\\|/)file'\]" ): self.P.a = 'non/existing/file' def test_nonexisting_unbound_no_error(self): p = param.Path('non/existing/file', check_exists=False) assert p.default == 'non/existing/file' def test_nonexisting_class_no_error(self): self.P.d = 'non/existing/file' assert self.P.d == 'non/existing/file' def test_nonexisting_instantiation_no_error(self): p = self.P(d='non/existing/file') assert p.d == 'non/existing/file' def test_nonexisting_set_no_error(self): p = self.P() p.d = 'non/existing/file' assert p.d == 'non/existing/file' def test_optionalexistence_unbound_no_error(self): p = param.Path(self.fa, check_exists=False) assert os.path.isabs(p.default) def test_optionalexistence_class_no_error(self): assert os.path.isabs(self.P.e) self.P.d = self.fc assert os.path.isabs(self.P.d) def test_optionalexistence_instantiation_no_error(self): p = self.P(d=self.fc) assert os.path.isabs(p.d) def test_optionalexistence_set_no_error(self): p = self.P() p.d = self.fc assert os.path.isabs(p.d) def test_existence_bad_value(self): with pytest.raises( ValueError, match="'check_exists' attribute value must be a boolean" ): param.Path(check_exists='wrong_option') class TestFilenameParameters(unittest.TestCase): def setUp(self): super().setUp() tmpdir1 = tempfile.mkdtemp() fa = os.path.join(tmpdir1, 'a.txt') fb = os.path.join(tmpdir1, 'b.txt') open(fa, 'w').close() open(fb, 'w').close() self.tmpdir1 = tmpdir1 self.fa = fa self.fb = fb class P(param.Parameterized): a = param.Filename() b = param.Filename(self.fb) c = param.Filename('a.txt', search_paths=[tmpdir1]) d = param.Filename(check_exists=False) self.P = P def tearDown(self): shutil.rmtree(self.tmpdir1) def _check_defaults(self, p): assert p.default is None assert p.allow_None is True assert p.search_paths == [] assert p.check_exists is True def test_defaults_class(self): class P(param.Parameterized): p = param.Filename() check_defaults(P.param.p, label='P') self._check_defaults(P.param.p) def test_defaults_inst(self): class P(param.Parameterized): p = param.Filename() p = P() check_defaults(p.param.p, label='P') self._check_defaults(p.param.p) def test_defaults_unbound(self): p = param.Filename() check_defaults(p, label=None) self._check_defaults(p) def test_no_path_class(self): assert self.P.a is None def test_no_path_inst(self): p = self.P() assert p.a is None def test_inst_with_path(self): p = self.P(a=self.fa) assert isinstance(p.a, str) assert os.path.isfile(p.a) assert os.path.isabs(p.a) assert p.a == self.fa def test_set_to_None_allowed(self): p = self.P() assert p.param.b.allow_None is False with pytest.raises(ValueError, match=re.escape(r"Filename parameter 'P.b' does not accept None")): p.b = None def test_search_paths(self): p = self.P() assert isinstance(p.c, str) assert os.path.isfile(p.c) assert os.path.isabs(p.c) assert p.c == self.fa def test_notfound_instantiation_raises_error(self): with pytest.raises( OSError, match=r"File file was not found in the following place\(s\): \['\S+(\\\\|/)non(\\\\|/)existing(\\\\|/)file'\]" ): param.Filename('non/existing/file') def test_set_notfound_class_raises_error(self): with pytest.raises( OSError, match=r"File file was not found in the following place\(s\): \['\S+(\\\\|/)non(\\\\|/)existing(\\\\|/)file'\]" ): self.P.a = 'non/existing/file' def test_set_notfound_raises_error(self): p = self.P() with pytest.raises( OSError, match=r"File file was not found in the following place\(s\): \['\S+(\\\\|/)non(\\\\|/)existing(\\\\|/)file'\]" ): p.a = 'non/existing/file' def test_nonexisting_unbound_no_error(self): p = param.Filename('non/existing/file', check_exists=False) assert p.default == 'non/existing/file' def test_nonexisting_class_no_error(self): self.P.d = 'non/existing/file' assert self.P.d == 'non/existing/file' def test_nonexisting_instantiation_no_error(self): p = self.P(d='non/existing/file') assert p.d == 'non/existing/file' def test_nonexisting_set_no_error(self): p = self.P() p.d = 'non/existing/file' assert p.d == 'non/existing/file' class TestFoldernameParameters(unittest.TestCase): def setUp(self): super().setUp() tmpdir1 = tempfile.mkdtemp() da = os.path.join(tmpdir1, 'da') os.mkdir(da) self.tmpdir1 = tmpdir1 self.da = da class P(param.Parameterized): a = param.Foldername() b = param.Foldername(tmpdir1) c = param.Foldername('da', search_paths=[tmpdir1]) d = param.Foldername(check_exists=False) self.P = P def tearDown(self): shutil.rmtree(self.tmpdir1) def _check_defaults(self, p): assert p.default is None assert p.allow_None is True assert p.search_paths == [] assert p.check_exists is True def test_defaults_class(self): class P(param.Parameterized): p = param.Foldername() check_defaults(P.param.p, label='P') self._check_defaults(P.param.p) def test_defaults_inst(self): class P(param.Parameterized): p = param.Foldername() p = P() check_defaults(p.param.p, label='P') self._check_defaults(p.param.p) def test_defaults_unbound(self): p = param.Foldername() check_defaults(p, label=None) self._check_defaults(p) def test_no_path_class(self): assert self.P.a is None def test_no_path_inst(self): p = self.P() assert p.a is None def test_inst_with_path(self): p = self.P(a=self.da) assert isinstance(p.a, str) assert os.path.isdir(p.a) assert os.path.isabs(p.a) assert p.a == self.da def test_set_to_None_allowed(self): p = self.P() assert p.param.b.allow_None is False with pytest.raises(ValueError, match=re.escape(r"Foldername parameter 'P.b' does not accept None")): p.b = None def test_search_paths(self): p = self.P() assert isinstance(p.c, str) assert os.path.isdir(p.c) assert os.path.isabs(p.c) assert p.c == self.da def test_notfound_instantiation_raises_error(self): with pytest.raises( OSError, match=r"Folder folder was not found in the following place\(s\): \['\S+(\\\\|/)non(\\\\|/)existing(\\\\|/)folder'\]" ): param.Foldername('non/existing/folder') def test_set_notfound_raises_error(self): p = self.P() with pytest.raises( OSError, match=r"Folder folder was not found in the following place\(s\): \['\S+(\\\\|/)non(\\\\|/)existing(\\\\|/)folder'\]" ): p.a = 'non/existing/folder' def test_set_notfound_class_raises_error(self): with pytest.raises( OSError, match=r"Folder folder was not found in the following place\(s\): \['\S+(\\\\|/)non(\\\\|/)existing(\\\\|/)folder'\]" ): self.P.a = 'non/existing/folder' def test_nonexisting_unbound_no_error(self): p = param.Foldername('non/existing/folder', check_exists=False) assert p.default == 'non/existing/folder' def test_nonexisting_class_no_error(self): self.P.d = 'non/existing/folder' assert self.P.d == 'non/existing/folder' def test_nonexisting_instantiation_no_error(self): p = self.P(d='non/existing/folder') assert p.d == 'non/existing/folder' def test_nonexisting_set_no_error(self): p = self.P() p.d = 'non/existing/folder' assert p.d == 'non/existing/folder' param-2.1.1/tests/testpickle.py000066400000000000000000000140271463636336300165170ustar00rootroot00000000000000import pickle import param import pytest try: import cloudpickle except ImportError: cloudpickle = None try: import numpy as np except: np = None try: import pandas as pd except: pd = None def eq(o1, o2): if not sorted(o1.param) == sorted(o2.param): return False for pname in o1.param: if getattr(o1, pname) != getattr(o2, pname): return False return True @pytest.fixture def pickler(request): if request.param is None: pytest.skip('cloudpickle not available') return request.param class P1(param.Parameterized): x = param.Parameter() @pytest.mark.parametrize('pickler', [cloudpickle, pickle], indirect=True) @pytest.mark.parametrize('protocol', [0, pickle.DEFAULT_PROTOCOL, pickle.HIGHEST_PROTOCOL]) def test_pickle_simple_class(pickler, protocol): s = pickler.dumps(P1, protocol=protocol) cls = pickler.loads(s) assert cls is P1 @pytest.mark.parametrize('pickler', [cloudpickle, pickle], indirect=True) @pytest.mark.parametrize('protocol', [0, pickle.DEFAULT_PROTOCOL, pickle.HIGHEST_PROTOCOL]) def test_pickle_simple_instance(pickler, protocol): p = P1() s = pickler.dumps(p, protocol=protocol) inst = pickler.loads(s) assert eq(p, inst) @pytest.mark.parametrize('pickler', [cloudpickle, pickle], indirect=True) @pytest.mark.parametrize('protocol', [0, pickle.DEFAULT_PROTOCOL, pickle.HIGHEST_PROTOCOL]) def test_pickle_simple_instance_modif_after(pickler, protocol): p = P1() s = pickler.dumps(p, protocol=protocol) p.x = 'modified' inst = pickler.loads(s) assert not eq(p, inst) assert inst.x is None class P2(param.Parameterized): a = param.Parameter() b = param.String() c = param.Dynamic() d = param.Number() e = param.Integer() f = param.Action() g = param.Event() h = param.Callable() i = param.Tuple() k = param.NumericTuple() l = param.Range() m = param.XYCoordinates() n = param.CalendarDateRange() o = param.DateRange() p = param.List() q = param.HookList() r = param.Path() s = param.Filename() t = param.Foldername() u = param.Date() v = param.CalendarDate() w = param.Selector() x = param.ObjectSelector() y = param.FileSelector() z = param.ListSelector() aa = param.MultiFileSelector() ab = param.ClassSelector(class_=type(None)) ac = None if pd is None else param.Series() ad = param.Dict() ae = None if pd is None else param.DataFrame() af = None if np is None else param.Array() @pytest.mark.parametrize('pickler', [cloudpickle, pickle], indirect=True) @pytest.mark.parametrize('protocol', [0, pickle.DEFAULT_PROTOCOL, pickle.HIGHEST_PROTOCOL]) def test_pickle_all_parameters_class(pickler, protocol): s = pickler.dumps(P2, protocol=protocol) cls = pickler.loads(s) assert cls is P2 @pytest.mark.parametrize('pickler', [cloudpickle, pickle], indirect=True) @pytest.mark.parametrize('protocol', [0, pickle.DEFAULT_PROTOCOL, pickle.HIGHEST_PROTOCOL]) def test_pickle_all_parameters_instance(pickler, protocol): p = P2() s = pickler.dumps(p, protocol=protocol) inst = pickler.loads(s) assert eq(p, inst) class P3(param.Parameterized): a = param.Integer(0) count = param.Integer(0) @param.depends("a", watch=True) def cb(self): self.count += 1 @pytest.mark.parametrize('pickler', [cloudpickle, pickle], indirect=True) @pytest.mark.parametrize('protocol', [0, pickle.DEFAULT_PROTOCOL, pickle.HIGHEST_PROTOCOL]) def test_pickle_depends_watch_class(pickler, protocol): s = pickler.dumps(P3, protocol=protocol) cls = pickler.loads(s) assert cls is P3 @pytest.mark.parametrize('pickler', [cloudpickle, pickle], indirect=True) @pytest.mark.parametrize('protocol', [0, pickle.DEFAULT_PROTOCOL, pickle.HIGHEST_PROTOCOL]) def test_pickle_depends_watch_instance(pickler, protocol): # https://github.com/holoviz/param/issues/757 p = P3() s = pickler.dumps(p, protocol=protocol) inst = pickler.loads(s) assert eq(p, inst) inst.a += 1 assert inst.count == 1 class P4(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 @pytest.mark.parametrize('pickler', [cloudpickle, pickle], indirect=True) @pytest.mark.parametrize('protocol', [0, pickle.DEFAULT_PROTOCOL, pickle.HIGHEST_PROTOCOL]) def test_pickle_complex_depends_class(pickler, protocol): s = pickler.dumps(P4, protocol=protocol) cls = pickler.loads(s) assert cls is P4 @pytest.mark.parametrize('pickler', [cloudpickle, pickle], indirect=True) @pytest.mark.parametrize('protocol', [0, pickle.DEFAULT_PROTOCOL, pickle.HIGHEST_PROTOCOL]) def test_pickle_complex_depends_instance(pickler, protocol): p = P4() s = pickler.dumps(p, protocol=protocol) inst = pickler.loads(s) assert eq(p, inst) @pytest.mark.skipif(cloudpickle is None, reason='cloudpickle not available') @pytest.mark.parametrize('protocol', [0, pickle.DEFAULT_PROTOCOL, pickle.HIGHEST_PROTOCOL]) def test_issue_757(protocol): # https://github.com/holoviz/param/issues/759 class P(param.Parameterized): a = param.Parameter() p = P() s = cloudpickle.dumps(p, protocol=protocol) inst = cloudpickle.loads(s) assert eq(p, inst) param-2.1.1/tests/testrangeparameter.py000066400000000000000000000167041463636336300202510ustar00rootroot00000000000000""" Unit test for Range parameters. """ import re import unittest import param import pytest class TestRangeParameters(unittest.TestCase): def setUp(self): super().setUp() class P(param.Parameterized): e = param.Range() f = param.Range(default=(0, 1), allow_None=True) g = param.Range(default=(0, 1)) self.P = P def _check_defaults(self, p): assert p.default is None assert p.allow_None is True assert p.length == 2 assert p.bounds is None assert p.softbounds is None assert p.inclusive_bounds == (True, True) assert p.step is None def test_defaults_class(self): class P(param.Parameterized): r = param.Range() self._check_defaults(P.param.r) def test_defaults_inst(self): class P(param.Parameterized): r = param.Range() p = P() self._check_defaults(p.param.r) def test_defaults_unbound(self): r = param.Range() self._check_defaults(r) def test_set_object_constructor(self): p = self.P(e=(0, 20)) assert p.e == (0, 20) def test_raise_not_2_tuple(self): p = self.P() msg = r"Attribute 'length' of Range parameter 'P.e' is not of the correct length \(3 instead of 2\)" with self.assertRaisesRegex(ValueError, msg): p.e = (1, 2, 3) def test_raise_if_value_bad_length_constructor(self): msg = r"Attribute 'length' of Range parameter 'P.e' is not of the correct length \(3 instead of 2\)" with self.assertRaisesRegex(ValueError, msg): self.P(e=(1, 1, 1)) def test_raise_if_value_bad_length_setattr(self): p = self.P() msg = r"Attribute 'length' of Range parameter 'P.e' is not of the correct length \(3 instead of 2\)" with self.assertRaisesRegex(ValueError, msg): p.e = (1, 1, 1) def test_raise_if_default_is_None_and_no_length(self): msg = "Attribute 'length' of NumericTuple parameter 't' must be specified if no default is supplied" with self.assertRaisesRegex(ValueError, msg): class P(param.Parameterized): t = param.NumericTuple(default=None) def test_bad_type(self): msg = r"Range parameter 'P.e' only takes a tuple value, not <(class|type) 'str'>." with self.assertRaisesRegex(ValueError, msg): self.P.e = 'test' with self.assertRaisesRegex(ValueError, msg): self.P(e='test') p = self.P() with self.assertRaisesRegex(ValueError, msg): p.e = 'test' msg = r"Range parameter 'e' only takes a tuple value, not <(class|type) 'str'>." with self.assertRaisesRegex(ValueError, msg): class P(param.Parameterized): e = param.Range(default='test') def test_support_allow_None_True(self): p = self.P() assert p.f == (0, 1) p.f = None assert p.f is None class P(param.Parameterized): f = param.Range(default=(0, 1), allow_None=True) P.f = None assert P.f is None def test_support_allow_None_False(self): p = self.P() msg = "Range parameter 'P.g' only takes a tuple value, not <(class|type) 'NoneType'>." with self.assertRaisesRegex(ValueError, msg): p.g = None with self.assertRaisesRegex(ValueError, msg): self.P.g = None def test_initialization_out_of_bounds_lower(self): with pytest.raises( ValueError, match=re.escape("Range parameter 'q' lower bound must be in range [0, 1], not -1.") ): class Q(param.Parameterized): q = param.Range((-1, 1), bounds=(0, 1)) def test_initialization_out_of_bounds_upper(self): with pytest.raises( ValueError, match=re.escape("Range parameter 'q' upper bound must be in range [0, 1], not 2.") ): class Q(param.Parameterized): q = param.Range((0, 2), bounds=(0, 1)) def test_set_exclusive_out_of_bounds_upper(self): class Q(param.Parameterized): q = param.Range(bounds=(0, 10), inclusive_bounds=(True, False)) with pytest.raises( ValueError, match=re.escape("Range parameter 'Q.q' upper bound must be in range [0, 10), not 10.") ): Q.q = (0, 10) def test_set_exclusive_out_of_bounds_lower(self): class Q(param.Parameterized): q = param.Range(bounds=(0, 10), inclusive_bounds=(False, True)) with pytest.raises( ValueError, match=re.escape("Range parameter 'Q.q' lower bound must be in range (0, 10], not 0.") ): Q.q = (0, 10) def test_set_out_of_bounds(self): class Q(param.Parameterized): q = param.Range(bounds=(0, 10)) with pytest.raises( ValueError, match=re.escape("Range parameter 'Q.q' upper bound must be in range [0, 10], not 11.") ): Q.q = (5, 11) 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)) def test_validate_step(self): msg = re.escape("Attribute 'step' of Range parameter can only be None or a numeric value, not .") p = param.Range((1, 2), bounds=(0, 10), step=1) assert p.step == 1 with self.assertRaisesRegex(ValueError, msg): param.Range((1, 2), bounds=(0, 10), step="1") def test_validate_order_on_val_with_positive_step(self): msg = re.escape("Range parameter 'Q.q' end 1 is less than its start 2 with positive step 1.") class Q(param.Parameterized): q = param.Range(bounds=(0, 10), step=1) with self.assertRaisesRegex(ValueError, msg): Q.q = (2, 1) def test_validate_order_on_val_with_negative_step(self): msg = re.escape("Range parameter 'Q.q' start -4 is less than its start -2 with negative step -1.") class Q(param.Parameterized): q = param.Range(bounds=(-5, -1), step=-1) with self.assertRaisesRegex(ValueError, msg): Q.q = (-4, -2) def test_validate_step_order_cannot_be_0(self): msg = re.escape("Attribute 'step' of Range parameter cannot be 0.") with self.assertRaisesRegex(ValueError, msg): param.Range(bounds=(0, 10), step=0) def test_validate_bounds_wrong_type_lower(self): msg = re.escape("Range parameter lower bound can only be None or a numerical value, not .") with pytest.raises(ValueError, match=msg): param.Range(bounds=('a', 1)) def test_validate_bounds_wrong_type_upper(self): msg = re.escape("Range parameter upper bound can only be None or a numerical value, not .") with pytest.raises(ValueError, match=msg): param.Range(bounds=(0, 'b')) def test_validate_softbounds_wrong_type_lower(self): msg = re.escape("Range parameter lower softbound can only be None or a numerical value, not .") with pytest.raises(ValueError, match=msg): param.Range(softbounds=('a', 1)) def test_validate_softbounds_wrong_type_upper(self): msg = re.escape("Range parameter upper softbound can only be None or a numerical value, not .") with pytest.raises(ValueError, match=msg): param.Range(softbounds=(0, 'b')) param-2.1.1/tests/testreactive.py000066400000000000000000000503401463636336300170500ustar00rootroot00000000000000import asyncio import math import operator import os import unittest import time try: import numpy as np 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") try: import pandas as pd 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") import param import pytest from param.parameterized import Skip from param.reactive import bind, rx from .utils import async_wait_until NUMERIC_BINARY_OPERATORS = ( operator.add, divmod, operator.floordiv, operator.mod, operator.mul, operator.pow, operator.sub, operator.truediv, ) LOGIC_BINARY_OPERATORS = ( operator.and_, operator.or_, operator.xor ) NUMERIC_UNARY_OPERATORS = ( abs, math.ceil, math.floor, math.trunc, operator.neg, operator.pos, round ) COMPARISON_OPERATORS = ( operator.eq, operator.ge, operator.gt, operator.le, operator.lt, operator.ne, ) LOGIC_UNARY_OPERATORS = (operator.inv,) NUMPY_UFUNCS = (np.min, np.max) @pytest.fixture(scope='module') def series(): return pd.Series(np.arange(5.0), name='A') @pytest.fixture(scope='module') def df(): return pd._testing.makeMixedDataFrame() class Parameters(param.Parameterized): string = param.String(default="string") integer = param.Integer(default=7) number = param.Number(default=3.14) function = param.Callable() boolean = param.Boolean(default=False) parameter = param.Parameter(allow_refs=False) event = param.Event() @param.depends('integer') def multiply_integer(self): return self.integer * 2 @pytest.mark.parametrize('op', NUMERIC_BINARY_OPERATORS) def test_reactive_numeric_binary_ops(op): assert op(rx(1), 2).rx.value == op(1, 2) assert op(rx(2), 2).rx.value == op(2, 2) @pytest.mark.parametrize('op', COMPARISON_OPERATORS) def test_reactive_numeric_comparison_ops(op): assert op(rx(1), 2).rx.value == op(1, 2) assert op(rx(2), 1).rx.value == op(2, 1) @pytest.mark.parametrize('op', NUMERIC_UNARY_OPERATORS) def test_reactive_numeric_unary_ops(op): assert op(rx(1)).rx.value == op(1) assert op(rx(-1)).rx.value == op(-1) assert op(rx(3.142)).rx.value == op(3.142) @pytest.mark.parametrize('op', NUMERIC_BINARY_OPERATORS) def test_reactive_numeric_binary_ops_reverse(op): assert op(2, rx(1)).rx.value == op(2, 1) assert op(2, rx(2)).rx.value == op(2, 2) @pytest.mark.parametrize('op', LOGIC_BINARY_OPERATORS) def test_reactive_logic_binary_ops(op): assert op(rx(True), True).rx.value == op(True, True) assert op(rx(True), False).rx.value == op(True, False) assert op(rx(False), True).rx.value == op(False, True) assert op(rx(False), False).rx.value == op(False, False) @pytest.mark.parametrize('op', LOGIC_UNARY_OPERATORS) def test_reactive_logic_unary_ops(op): assert op(rx(1)).rx.value == op(1) assert op(rx(0)).rx.value == op(0) @pytest.mark.parametrize('op', LOGIC_BINARY_OPERATORS) def test_reactive_logic_binary_ops_reverse(op): assert op(True, rx(True)).rx.value == op(True, True) assert op(True, rx(False)).rx.value == op(True, False) assert op(False, rx(True)).rx.value == op(False, True) assert op(False, rx(False)).rx.value == op(False, False) def test_reactive_getitem_dict(): assert rx({'A': 1})['A'].rx.value == 1 assert rx({'A': 1, 'B': 2})['B'].rx.value == 2 def test_reactive_getitem_list(): assert rx([1, 2, 3])[1].rx.value == 2 assert rx([1, 2, 3])[2].rx.value == 3 def test_reactive_getitem_list_with_slice(): i = rx(1) j = rx(5) lst = list(range(10)) lstx = rx(lst) sx = lstx[i: j] assert sx.rx.value == lst[i.rx.value: j.rx.value] i.rx.value = 2 assert sx.rx.value == lst[i.rx.value: j.rx.value] def test_reactive_getitem_numpy_with_tuple(): i = rx(0) j = rx(1) arr = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]]) arrx = rx(arr) selx = arrx[i, j] assert selx.rx.value == arr[i.rx.value, j.rx.value] i.rx.value = 1 assert selx.rx.value == arr[i.rx.value, j.rx.value] @pytest.mark.parametrize('ufunc', NUMPY_UFUNCS) def test_numpy_ufunc(ufunc): l = [1, 2, 3] assert ufunc(rx(l)).rx.value == ufunc(l) array = np.ndarray([1, 2, 3]) assert ufunc(rx(array)).rx.value == ufunc(array) def test_reactive_empty_construct(): i = rx() assert i.rx.value is None i.rx.value = 2 assert i.rx.value == 2 def test_reactive_set_new_value(): i = rx(1) assert i.rx.value == 1 i.rx.value = 2 assert i.rx.value == 2 def test_reactive_increment_value(): i = rx(1) assert i.rx.value == 1 i.rx.value += 2 assert i.rx.value == 3 def test_reactive_multiply_value_inplace(): i = rx(3) assert i.rx.value == 3 i.rx.value *= 2 assert i.rx.value == 6 def test_reactive_pipeline_set_new_value(): i = rx(1) j = i + 2 assert j.rx.value == 3 i.rx.value = 2 assert j.rx.value == 4 def test_reactive_reflect_param_value(): P = Parameters(integer=1) i = rx(P.param.integer) assert i.rx.value == 1 P.integer = 2 assert i.rx.value == 2 def test_reactive_skip_value(): P = Parameters(integer=1) def skip_values(v): if v > 2: raise Skip() else: return v+1 i = rx(P.param.integer).rx.pipe(skip_values) assert i.rx.value == 2 P.integer = 2 assert i.rx.value == 3 P.integer = 3 assert i.rx.value == 3 def test_reactive_skip_value_return(): P = Parameters(integer=1) def skip_values(v): if v > 2: return Skip else: return v+1 i = rx(P.param.integer).rx.pipe(skip_values) assert i.rx.value == 2 P.integer = 2 assert i.rx.value == 3 P.integer = 3 assert i.rx.value == 3 def test_reactive_pipeline_reflect_param_value(): P = Parameters(integer=1) i = rx(P.param.integer) + 2 assert i.rx.value == 3 P.integer = 2 assert i.rx.value == 4 def test_reactive_reactive_reflect_other_rx(): i = rx(1) j = rx(i) assert j.rx.value == 1 i.rx.value = 2 assert j.rx.value == 2 def test_reactive_pipeline_reflect_other_reactive_expr(): i = rx(1) j = i + 2 k = rx(j) assert k.rx.value == 3 i.rx.value = 2 assert k.rx.value == 4 def test_reactive_reflect_bound_method(): P = Parameters(integer=1) i = rx(P.multiply_integer) assert i.rx.value == 2 P.integer = 2 assert i.rx.value == 4 def test_reactive_pipeline_reflect_bound_method(): P = Parameters(integer=1) i = rx(P.multiply_integer) + 2 assert i.rx.value == 4 P.integer = 2 assert i.rx.value == 6 def test_reactive_reflect_bound_function(): P = Parameters(integer=1) i = rx(bind(lambda v: v * 2, P.param.integer)) assert i.rx.value == 2 P.integer = 2 assert i.rx.value == 4 def test_reactive_pipeline_reflect_bound_function(): P = Parameters(integer=1) i = rx(bind(lambda v: v * 2, P.param.integer)) + 2 assert i.rx.value == 4 P.integer = 2 assert i.rx.value == 6 def test_reactive_dataframe_method_chain(dataframe): dfi = rx(dataframe).groupby('str')[['float']].mean().reset_index() pd.testing.assert_frame_equal(dfi.rx.value, dataframe.groupby('str')[['float']].mean().reset_index()) def test_reactive_dataframe_attribute_chain(dataframe): array = rx(dataframe).str.values.rx.value np.testing.assert_array_equal(array, dataframe.str.values) def test_reactive_dataframe_param_value_method_chain(dataframe): P = Parameters(string='str') dfi = rx(dataframe).groupby(P.param.string)[['float']].mean().reset_index() pd.testing.assert_frame_equal(dfi.rx.value, dataframe.groupby('str')[['float']].mean().reset_index()) P.string = 'int' pd.testing.assert_frame_equal(dfi.rx.value, dataframe.groupby('int')[['float']].mean().reset_index()) def test_reactive_len(): i = rx([1, 2, 3]) l = i.rx.len() assert l.rx.value == 3 i.rx.value = [1, 2] assert l == 2 def test_reactive_bool(): i = rx(1) b = i.rx.bool() assert b.rx.value is True i.rx.value = 0 assert b.rx.value is False def test_reactive_not_(): i = rx(1) b = i.rx.not_() assert b.rx.value is False i.rx.value = 0 assert b.rx.value is True def test_reactive_and_(): i = rx('') b = i.rx.and_('foo') assert b.rx.value == '' i.rx.value = 'bar' assert b.rx.value == 'foo' def test_reactive_or_(): i = rx('') b = i.rx.or_('') assert b.rx.value == '' i.rx.value = 'foo' assert b.rx.value == 'foo' def test_reactive_map(): i = rx(range(3)) b = i.rx.map(lambda x: x*2) assert b.rx.value == [0, 2, 4] i.rx.value = range(1, 4) assert b.rx.value == [2, 4, 6] async def test_reactive_async_map(): i = rx(range(3)) async def mul(x): await asyncio.sleep(0.05) return x*2 b = i.rx.map(mul) assert b.rx.value is param.Undefined await async_wait_until(lambda: b.rx.value == [0, 2, 4]) i.rx.value = range(1, 4) assert b.rx.value == [0, 2, 4] await async_wait_until(lambda: b.rx.value == [2, 4, 6]) assert b.rx.value == [2, 4, 6] def test_reactive_map_args(): i = rx(range(3)) j = rx(2) b = i.rx.map(lambda x, m: x*m, j) assert b.rx.value == [0, 2, 4] j.rx.value = 3 assert b.rx.value == [0, 3, 6] def test_reactive_iter(): i = rx(('a', 'b')) a, b = i assert a.rx.value == 'a' assert b.rx.value == 'b' i.rx.value = ('b', 'a') assert a.rx.value == 'b' assert b.rx.value == 'a' def test_reactive_multi_iter(): i = rx(('a', 'b')) a1, b1 = i a2, b2 = i assert a1.rx.value == 'a' assert b1.rx.value == 'b' assert a2.rx.value == 'a' assert b2.rx.value == 'b' i.rx.value = ('b', 'a') assert a1.rx.value == 'b' assert b1.rx.value == 'a' assert a2.rx.value == 'b' assert b2.rx.value == 'a' def test_reactive_is(): i = rx(None) is_ = i.rx.is_(None) assert is_.rx.value i.rx.value = False assert not is_.rx.value def test_reactive_in(): i = rx(2) in_ = i.rx.in_([1, 2, 3]) assert in_.rx.value i.rx.value = 4 assert not in_.rx.value def test_reactive_is_not(): i = rx(None) is_ = i.rx.is_not(None) assert not is_.rx.value i.rx.value = False assert is_.rx.value def test_reactive_where_expr(): p = Parameters() r = p.param.boolean.rx.where('A', 'B') assert r.rx.value == 'B' p.boolean = True assert r.rx.value == 'A' def test_reactive_where_expr_refs(): p = Parameters() results = [] r = p.param.boolean.rx.where(p.param.string, p.param.number) r.rx.watch(results.append) assert r.rx.value == 3.14 p.boolean = True assert results == ['string'] p.string = 'foo' assert results == ['string', 'foo'] p.number = 2.1 assert results == ['string', 'foo'] p.boolean = False assert results == ['string', 'foo', 2.1] def test_reactive_watch_on_set_input(): string = rx('string') new_string = string + '!' items = [] new_string.rx.watch(items.append) string.rx.value = 'new string' assert items == ['new string!'] async def test_reactive_watch_async_on_event(): p = Parameters() event = p.param.event.rx() items = [] event.rx.watch(items.append) p.param.trigger('event') await async_wait_until(lambda: items == [True]) def test_reactive_set_value_non_root_raises(): rx_val = rx(1) + 1 with pytest.raises(AttributeError): rx_val.rx.value = 3 def test_reactive_clone_evaluates_once(): namex = rx('bob') items = [] def debug(value): items.append(value) return value assert namex.rx.pipe(debug).title().rx.value == 'Bob' assert len(items) == 1 def test_reactive_when(): p = Parameters(integer=3) integer = p.param.integer.rx().rx.when(p.param.event) assert integer.rx.value == 3 p.integer = 4 assert integer.rx.value == 3 p.param.trigger('event') assert integer.rx.value == 4 def test_reactive_when_initial(): p = Parameters(integer=3) integer = p.param.integer.rx().rx.when(p.param.event, initial=None) assert integer.rx.value is None p.integer = 4 assert integer.rx.value is None p.param.trigger('event') assert integer.rx.value == 4 def test_reactive_resolve(): p = Parameters(integer=3) p2 = Parameters(parameter=p.param.integer) prx = p2.param.parameter.rx() assert prx.rx.value is p.param.integer resolved_prx = prx.rx.resolve() assert resolved_prx.rx.value == 3 changes = [] resolved_prx.rx.watch(changes.append) # Test changing referenced value p.integer = 4 assert resolved_prx.rx.value == 4 assert changes == [4] # Test changing reference itself p2.parameter = p.param.number assert resolved_prx.rx.value == 3.14 assert changes == [4, 3.14] # Ensure no updates triggered when old reference is updated p.integer = 5 assert resolved_prx.rx.value == 3.14 assert changes == [4, 3.14] def test_reactive_resolve_nested(): p = Parameters(integer=3) p2 = Parameters(parameter=[p.param.integer]) prx = p2.param.parameter.rx() assert prx.rx.value == [p.param.integer] resolved_prx = prx.rx.resolve(nested=True) assert resolved_prx.rx.value == [3] changes = [] resolved_prx.rx.watch(changes.append) # Test changing referenced value p.integer = 4 assert resolved_prx.rx.value == [4] assert changes == [[4]] # Test changing reference itself p2.parameter = [p.param.number] assert resolved_prx.rx.value == [3.14] assert changes == [[4], [3.14]] # Ensure no updates triggered when old reference is updated p.integer = 5 assert resolved_prx.rx.value == [3.14] assert changes == [[4], [3.14]] def test_reactive_resolve_recursive(): p = Parameters(integer=3) p2 = Parameters(parameter=p.param.integer) p3 = Parameters(parameter=p2.param.parameter) prx = p3.param.parameter.rx() assert prx.rx.value is p2.param.parameter resolved_prx = prx.rx.resolve(recursive=True) assert resolved_prx.rx.value == 3 changes = [] resolved_prx.rx.watch(changes.append) # Test changing referenced value p.integer = 4 assert resolved_prx.rx.value == 4 assert changes == [4] # Test changing recursive reference p2.parameter = p.param.number assert resolved_prx.rx.value == 3.14 assert changes == [4, 3.14] # Ensure no updates triggered when old reference is updated p.integer = 5 assert resolved_prx.rx.value == 3.14 assert changes == [4, 3.14] # Test changing reference itself p3.parameter = p.param.string assert resolved_prx.rx.value == 'string' assert changes == [4, 3.14, 'string'] async def test_reactive_async_func(): async def async_func(): await asyncio.sleep(0.02) return 2 async_rx = rx(async_func) + 2 assert async_rx.rx.value is param.Undefined await async_wait_until(lambda: async_rx.rx.value == 4) async def test_reactive_pipe_async_func(): async def async_func(value): await asyncio.sleep(0.02) return value+2 async_rx = rx(0).rx.pipe(async_func) async_rx.rx.watch() assert async_rx.rx.value is param.Undefined await async_wait_until(lambda: async_rx.rx.value == 2) async def test_reactive_gen(): def gen(): yield 1 time.sleep(0.05) yield 2 rxgen = rx(gen) assert rxgen.rx.value is param.Undefined await asyncio.sleep(0.04) assert rxgen.rx.value == 1 await async_wait_until(lambda: rxgen.rx.value == 2) async def test_reactive_gen_pipe(): def gen(val): yield val+1 time.sleep(0.05) yield val+2 rxv = rx(0) rxgen = rxv.rx.pipe(gen) rxgen.rx.watch() assert rxgen.rx.value is param.Undefined await asyncio.sleep(0.04) assert rxgen.rx.value == 1 await async_wait_until(lambda: rxgen.rx.value == 2) rxv.rx.value = 2 await asyncio.sleep(0.04) assert rxgen.rx.value == 3 await async_wait_until(lambda: rxgen.rx.value == 4) async def test_reactive_gen_with_dep(): def gen(i): yield i+1 time.sleep(0.05) yield i+2 irx = rx(0) rxgen = rx(bind(gen, irx)) assert rxgen.rx.value is param.Undefined await asyncio.sleep(0.04) assert rxgen.rx.value == 1 irx.rx.value = 3 await asyncio.sleep(0.04) assert rxgen.rx.value == 4 await async_wait_until(lambda: rxgen.rx.value == 5) async def test_reactive_gen_pipe_with_dep(): def gen(value, i): yield value+i+1 time.sleep(0.05) yield value+i+2 irx = rx(0) rxv = rx(0) rxgen = rxv.rx.pipe(bind(gen, irx)) rxgen.rx.watch() assert rxgen.rx.value is param.Undefined await asyncio.sleep(0.04) assert rxgen.rx.value == 1 irx.rx.value = 3 await asyncio.sleep(0.04) assert rxgen.rx.value == 4 await async_wait_until(lambda: rxgen.rx.value == 5) rxv.rx.value = 5 await asyncio.sleep(0.04) assert rxgen.rx.value == 9 await async_wait_until(lambda: rxgen.rx.value == 10) async def test_reactive_async_gen(): async def gen(): yield 1 await asyncio.sleep(0.05) yield 2 rxgen = rx(gen) assert rxgen.rx.value is param.Undefined await asyncio.sleep(0.04) assert rxgen.rx.value == 1 await async_wait_until(lambda: rxgen.rx.value == 2) async def test_reactive_async_gen_pipe(): async def gen(value): yield value + 1 await asyncio.sleep(0.05) yield value + 2 rxgen = rx(0).rx.pipe(gen) assert rxgen.rx.value is param.Undefined await asyncio.sleep(0.04) assert rxgen.rx.value == 1 await async_wait_until(lambda: rxgen.rx.value == 2) async def test_reactive_async_gen_with_dep(): async def gen(i): yield i+1 await asyncio.sleep(0.1) yield i+2 irx = rx(0) rxgen = rx(bind(gen, irx)) assert rxgen.rx.value is param.Undefined await asyncio.sleep(0.05) assert rxgen.rx.value == 1 irx.rx.value = 3 await asyncio.sleep(0.05) irx.rx.value = 4 await asyncio.sleep(0.1) assert rxgen.rx.value == 5 async def test_reactive_async_gen_pipe_with_dep(): async def gen(value, i): yield value+i+1 await asyncio.sleep(0.05) yield value+i+2 irx = rx(0) rxv = rx(0) rxgen = rxv.rx.pipe(bind(gen, i=irx)) rxgen.rx.watch() assert rxgen.rx.value is param.Undefined await asyncio.sleep(0.04) assert rxgen.rx.value == 1 irx.rx.value = 3 await asyncio.sleep(0.04) irx.rx.value = 4 await asyncio.sleep(0.04) assert rxgen.rx.value == 5 rxv.rx.value = 5 await asyncio.sleep(0.04) assert rxgen.rx.value == 10 await async_wait_until(lambda: rxgen.rx.value == 11) def test_root_invalidation(): arx = rx('a') brx = rx('b') computed = [] def debug(value, info): computed.append(info) return value expr = arx.title().rx.pipe(debug, '1') + brx.title().rx.pipe(debug, '2') assert expr.rx.value == 'AB' assert computed == ['1', '2'] brx.rx.value = 'c' assert expr.rx.value == 'AC' assert computed == ['1', '2', '2'] arx.rx.value = 'd' assert expr.rx.value == 'DC' assert computed == ['1', '2', '2', '1'] def test_ensure_ref_can_update_by_watcher_of_same_parameter(): # https://github.com/holoviz/param/pull/929 class W(param.Parameterized): value = param.String() class T(param.Parameterized): lst = param.List(allow_refs=True, allow_None=True) @param.depends("lst", watch=True) def test(self): lst = self.lst or range(5) items = [W(value=str(i)) for i in lst] with param.discard_events(self): self.lst = param.rx(items).rx.resolve() self.items = items def transform(obj): if isinstance(obj, W): return obj.param.value return obj param.reactive.register_reference_transform(transform) t = T() t.lst = list("ABCDE") t.items[1].value = "TEST" assert t.lst[1] == "TEST" def test_reactive_callback_resolve_accessor(): df = pd.DataFrame({"name": ["Bill", "Bob"]}) dfx = rx(df) out = dfx["name"].str._callback() assert out is df["name"].str param-2.1.1/tests/testrefs.py000066400000000000000000000222601463636336300162050ustar00rootroot00000000000000import asyncio import threading import time import param import pytest from param.parameterized import Skip, resolve_ref from param.reactive import bind, rx class Parameters(param.Parameterized): string = param.String(default="string", allow_refs=True) dictionary = param.Dict(default={}, allow_refs=True, nested_refs=True) string_list = param.List(default=[], item_type=str, allow_refs=True, nested_refs=True) no_refs = param.Parameter(allow_refs=False) @param.depends('string') def formatted_string(self): if self.string.endswith('?'): raise Skip() return self.string + '!' @param.depends('string') def formatted_string_skip_return(self): if self.string.endswith('?'): return Skip return self.string + '!' class Subclass(Parameters): no_refs = param.Parameter() class SubclassOverride(Parameters): no_refs = param.Parameter(allow_refs=True) class Nested(param.Parameterized): subobject = param.ClassSelector(class_=Parameters) @param.depends('subobject.string') def string(self): return self.subobject.string + '!' def test_class_explicit_no_refs(): assert Parameters._param__private.explicit_no_refs == ['no_refs'] def test_subclass_explicit_no_refs(): assert Subclass._param__private.explicit_no_refs == ['no_refs'] def test_subclass_explicit_no_refs_override(): assert SubclassOverride._param__private.explicit_no_refs == [] def test_parameterized_warns_explicit_no_ref(): class ImplicitRefsParameters(param.Parameterized): parameter = param.Parameter(default="string") p = Parameters() with pytest.raises(Exception) as e: ImplicitRefsParameters(parameter=p.param.string) assert f"Parameter 'parameter' on {ImplicitRefsParameters} is being given a valid parameter reference \n \n Sub' in html def test_html_repr_inst_sub(self, P): html = P().param._repr_html_() assert '
\n \n Sub' in html def test_html_repr_ClassSelector_tuple(self): class P(param.Parameterized): c = param.ClassSelector(class_=(str, int)) rhtml = _parameterized_repr_html(P, True) assert 'str | int' in rhtml def test_html_repr_title_class(self, P): html = P.param._repr_html_() assert 'P' in html def test_html_repr_title_instance(self, P): html = P().param._repr_html_() assert 'P()' in html param-2.1.1/tests/testselector.py000066400000000000000000000333361463636336300170740ustar00rootroot00000000000000""" Unit test for object selector parameters. Originally implemented as doctests in Topographica in the file testEnumerationParameter.txt """ import re import unittest from collections import OrderedDict import param import pytest from .utils import check_defaults opts=dict(A=[1,2],B=[3,4],C=dict(a=1,b=2)) class TestSelectorParameters(unittest.TestCase): def setUp(self): super().setUp() class P(param.Parameterized): e = param.Selector(objects=[5,6,7]) f = param.Selector(default=10) h = param.Selector(default=None) g = param.Selector(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)) p = param.Selector(default=3, objects=dict(one=1,two=2,three=3)) d = param.Selector(default=opts['B'], objects=opts) self.P = P def _check_defaults(self, p): assert p.default is None assert p.allow_None is None assert p.objects == [] assert p.compute_default_fn is None assert p.check_on_set is False assert p.names == {} def test_defaults_class(self): class P(param.Parameterized): s = param.Selector() check_defaults(P.param.s, label='S') self._check_defaults(P.param.s) def test_defaults_inst(self): class P(param.Parameterized): s = param.Selector() p = P() check_defaults(p.param.s, label='S') self._check_defaults(p.param.s) def test_defaults_unbound(self): s = param.Selector() check_defaults(s, label=None) self._check_defaults(s) def test_unbound_default_inferred(self): s = param.Selector(objects=[0, 1, 2]) assert s.default == 0 def test_unbound_default_explicit(self): s = param.Selector(default=1, objects=[0, 1, 2]) assert s.default == 1 def test_unbound_default_check_on_set_inferred(self): s1 = param.Selector(objects=[0, 1, 2]) s2 = param.Selector(objects=[]) s3 = param.Selector(objects={}) s4 = param.Selector() assert s1.check_on_set is True assert s2.check_on_set is False assert s3.check_on_set is False assert s4.check_on_set is False def test_unbound_default_check_on_set_explicit(self): s1 = param.Selector(check_on_set=True) s2 = param.Selector(check_on_set=False) assert s1.check_on_set is True assert s2.check_on_set is False def test_set_object_constructor(self): p = self.P(e=6) self.assertEqual(p.e, 6) def test_allow_None_is_None(self): p = self.P() assert p.param.e.allow_None is None assert p.param.f.allow_None is None assert p.param.g.allow_None is None assert p.param.h.allow_None is None assert p.param.i.allow_None is None assert p.param.s.allow_None is None assert p.param.d.allow_None is None def test_allow_None_set_and_behavior_class(self): class P(param.Parameterized): a = param.Selector(objects=dict(a=1), allow_None=True) b = param.Selector(objects=dict(a=1), allow_None=False) c = param.Selector(default=1, objects=dict(a=1), allow_None=True) d = param.Selector(default=1, objects=dict(a=1), allow_None=False) assert P.param.a.allow_None is True assert P.param.b.allow_None is False assert P.param.c.allow_None is True assert P.param.d.allow_None is False P.a = None assert P.a is None with pytest.raises( ValueError, match=re.escape(r"Selector parameter 'P.b' does not accept None; valid options include: '[1]'") ): P.b = None P.c = None assert P.c is None with pytest.raises(ValueError): P.d = None def test_allow_None_set_and_behavior_instance(self): class P(param.Parameterized): a = param.Selector(objects=dict(a=1), allow_None=True) b = param.Selector(objects=dict(a=1), allow_None=False) c = param.Selector(default=1, objects=dict(a=1), allow_None=True) d = param.Selector(default=1, objects=dict(a=1), allow_None=False) p = P() assert p.param.a.allow_None is True assert p.param.b.allow_None is False assert p.param.c.allow_None is True assert p.param.d.allow_None is False p.a = None assert p.a is None with pytest.raises(ValueError): p.b = None p.c = None assert p.c is None with pytest.raises(ValueError): p.d = None def test_autodefault(self): class P(param.Parameterized): o1 = param.Selector(objects=[6, 7]) o2 = param.Selector(objects={'a': 1, 'b': 2}) assert P.o1 == 6 assert P.o2 == 1 p = P() assert p.o1 == 6 assert p.o2 == 1 def test_get_range_list(self): r = self.P.param['g'].get_range() self.assertEqual(r['7'],7) self.assertEqual(r['8'],8) def test_get_range_ordereddict(self): r = self.P.param['s'].get_range() self.assertEqual(r['one'],1) self.assertEqual(r['two'],2) def test_get_range_dict(self): r = self.P.param['p'].get_range() self.assertEqual(r['one'],1) self.assertEqual(r['two'],2) def test_get_range_mutable(self): r = self.P.param['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("Selector 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("Selector created without range.") def test_check_on_set_on_init_unbound(self): i = param.Selector(default=7, objects=[9], check_on_set=False) h = param.Selector(default=None) assert i.objects == [9, 7] assert h.objects == [] @pytest.mark.xfail(raises=AssertionError) def test_check_on_set_on_init_unbound_unsupported(self): # Tricky to update the objects to contain the default on an unbound # Selector as in that case the objects is always an empty list, that # is returned by the objects factory. f = param.Selector(default=10) assert f.objects == [10] def test_check_on_set_on_init_class(self): assert self.P.param.i.objects == [9, 7] assert self.P.param.f.objects == [10] assert self.P.param.h.objects == [] def test_check_on_set_on_init_instance(self): p = self.P() assert p.param.i.objects == [9, 7] assert p.param.f.objects == [10] assert p.param.h.objects == [] def test_check_on_set_defined(self): class P(param.Parameterized): o1 = param.Selector(check_on_set=True) o2 = param.Selector(check_on_set=False) assert P.param.o1.check_on_set is True assert P.param.o2.check_on_set is False p = P() assert p.param.o1.check_on_set is True assert p.param.o2.check_on_set is False def test_check_on_set_empty_objects(self): class P(param.Parameterized): o = param.Selector() assert P.param.o.check_on_set is False p = P() assert p.param.o.check_on_set is False def test_check_on_set_else(self): class P(param.Parameterized): o = param.Selector(objects=[0, 1]) assert P.param.o.check_on_set is True p = P() assert p.param.o.check_on_set is True def test_inheritance_behavior1(self): class A(param.Parameterized): p = param.Selector() class B(A): p = param.Selector() assert B.param.p.default is None assert B.param.p.objects == [] assert B.param.p.check_on_set is False b = B() assert b.param.p.default is None assert b.param.p.objects == [] assert b.param.p.check_on_set is False def test_inheritance_behavior2(self): class A(param.Parameterized): p = param.Selector(objects=[0, 1]) class B(A): p = param.Selector() assert B.param.p.objects == [0, 1] assert B.param.p.default == 0 assert B.param.p.check_on_set is True b = B() assert b.param.p.objects == [0, 1] assert b.param.p.default == 0 assert b.param.p.check_on_set is True def test_inheritance_behavior3(self): class A(param.Parameterized): p = param.Selector(default=1, objects=[0, 1]) class B(A): p = param.Selector() assert B.param.p.objects == [0, 1] assert B.param.p.default == 1 assert B.param.p.check_on_set is True b = B() assert b.param.p.objects == [0, 1] assert b.param.p.default == 1 assert b.param.p.check_on_set is True def test_inheritance_behavior4(self): class A(param.Parameterized): p = param.Selector(objects=[0, 1], check_on_set=False) class B(A): p = param.Selector() assert B.param.p.objects == [0, 1] assert B.param.p.default == 0 assert B.param.p.check_on_set is False b = B() assert b.param.p.objects == [0, 1] assert b.param.p.default == 0 assert b.param.p.check_on_set is False def test_inheritance_behavior5(self): class A(param.Parameterized): p = param.Selector(objects=[0, 1], check_on_set=True) class B(A): p = param.Selector() assert B.param.p.objects == [0, 1] assert B.param.p.default == 0 assert B.param.p.check_on_set is True b = B() assert b.param.p.objects == [0, 1] assert b.param.p.default == 0 assert b.param.p.check_on_set is True def test_inheritance_behavior6(self): class A(param.Parameterized): p = param.Selector(default=0, objects=[0, 1]) class B(A): p = param.Selector(default=1) assert B.param.p.objects == [0, 1] assert B.param.p.default == 1 assert B.param.p.check_on_set is True b = B() assert b.param.p.objects == [0, 1] assert b.param.p.default == 1 assert b.param.p.check_on_set is True def test_inheritance_behavior7(self): class A(param.Parameterized): p = param.Selector(default=0) class B(A): p = param.Selector(default=1) assert B.param.p.objects == [0, 1] assert B.param.p.default == 1 assert B.param.p.check_on_set is False b = B() assert b.param.p.objects == [0, 1] assert b.param.p.default == 1 assert b.param.p.check_on_set is False def test_no_instantiate_when_constant(self): # https://github.com/holoviz/param/issues/287 objs = [object(), object()] class A(param.Parameterized): p = param.Selector(default=objs[0], objects=objs, constant=True) a = A() assert a.p is objs[0] def test_objects_not_shared_in_class_hierarchy_1(self): # https://github.com/holoviz/param/issues/793 class A(param.Parameterized): p = param.Selector(objects=[1, 2], check_on_set=False) class B(A): p = param.Selector(default=2) b = B() b.p = 3 assert A.param.p.objects == [1, 2] assert B.param.p.objects == [1, 2] assert b.param.p.objects == [1, 2, 3] def test_objects_not_shared_in_class_hierarchy_2(self): # https://github.com/holoviz/param/issues/793 class A(param.Parameterized): p = param.Selector() class B(A): p = param.Selector() b = B() b.p = 2 assert A.param.p.objects == [] def test_objects_not_shared_across_instance_class(self): # https://github.com/holoviz/param/issues/746 class P(param.Parameterized): s = param.Selector(objects=[1, 2], check_on_set=False) p = P() p.s = 3 assert P.param.s.objects == [1, 2] assert p.param.s.objects == [1, 2, 3] P.s = 4 assert P.param.s.objects == [1, 2, 4] assert p.param.s.objects == [1, 2 ,3] param-2.1.1/tests/testsignatures.py000066400000000000000000000104141463636336300174300ustar00rootroot00000000000000import inspect import sys import typing import param import pytest from param import concrete_descendents, Parameter SKIP_UPDATED = [ # Not sure how to handle attribs yet param.Composite, # Not sure how to handle search_paths param.Path, param.Filename, param.Foldername, ] def custom_concrete_descendents(kls): return { pname: ptype for pname, ptype in concrete_descendents(kls).items() if ptype.__module__.startswith('param') } @pytest.mark.skipif(sys.version_info <= (3, 11), reason='typing.get_overloads available from Python 3.11') def test_signature_parameters_constructors_overloaded(): for _, p_type in custom_concrete_descendents(Parameter).items(): init_overloads = typing.get_overloads(p_type.__init__) assert len(init_overloads) == 1 def test_signature_parameters_constructors_updated(): base_args = list(inspect.signature(Parameter).parameters.keys()) for _, p_type in custom_concrete_descendents(Parameter).items(): if p_type in SKIP_UPDATED: continue sig = inspect.signature(p_type) for parameter in sig.parameters.values(): assert parameter.kind not in (inspect.Parameter.VAR_KEYWORD, inspect.Parameter.VAR_POSITIONAL) assert all(arg in sig.parameters for arg in base_args) @pytest.mark.skipif(sys.version_info <= (3, 11), reason='typing.get_overloads available from Python 3.11') def test_signature_parameters_constructors_overloaded_updated_match(): for _, p_type in custom_concrete_descendents(Parameter).items(): if p_type.__name__.startswith('_') or p_type in SKIP_UPDATED: continue init_overloads = typing.get_overloads(p_type.__init__) osig = inspect.signature(init_overloads[0]) osig = osig.replace(parameters=[parameter for pname, parameter in osig.parameters.items() if pname != 'self']) usig = inspect.signature(p_type) assert osig == usig, _ def test_signature_position_keywords(): NO_POSITIONAL = [ # class_ is first param.ClassSelector, # objects is first param.Selector, # attribs is first param.Composite, ] for ptype in custom_concrete_descendents(Parameter).values(): if ptype.__name__.startswith('_'): continue sig = inspect.signature(ptype) parameters = dict(sig.parameters) parameters.pop('self', None) if ptype in NO_POSITIONAL: assert all( p.kind in (inspect.Parameter.KEYWORD_ONLY, inspect.Parameter.VAR_KEYWORD) for pname, p in parameters.items() if pname != 'self' ) else: positional_or_kw = [ p for p in parameters.values() if p.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD ] assert len(positional_or_kw) == 1 assert positional_or_kw[0].name == 'default' del parameters['default'] assert all( p.kind in (inspect.Parameter.KEYWORD_ONLY, inspect.Parameter.VAR_KEYWORD) for p in parameters.values() ) def test_signature_warning_by_position(): # Simple test as it's tricky to automatically test all the Parameters with pytest.warns( param._utils.ParamDeprecationWarning, match=r"Passing 'objects' as positional argument\(s\) to 'param.Selector' has been deprecated since Param 2.0.0 and will raise an error in a future version, please pass them as keyword arguments" ): param.Selector([0, 1]) # objects with pytest.warns( param._utils.ParamDeprecationWarning, match=r"Passing 'class_' as positional argument\(s\) to 'param.ClassSelector' has been deprecated since Param 2.0.0 and will raise an error in a future version, please pass them as keyword arguments" ): param.ClassSelector(int) # class_ with pytest.warns( param._utils.ParamDeprecationWarning, match=r"Passing 'bounds, softbounds' as positional argument\(s\) to 'param.Number' has been deprecated since Param 2.0.0 and will raise an error in a future version, please pass them as keyword arguments" ): param.Number(1, (0, 2), (0, 2)) # default (OK), bounds (not OK), softbounds (not OK) param-2.1.1/tests/teststringparam.py000066400000000000000000000047371463636336300176060ustar00rootroot00000000000000""" Unit test for String parameters """ import unittest import param from .utils import check_defaults 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(unittest.TestCase): def _check_defaults(self, p): assert p.default == '' assert p.allow_None is False assert p.regex is None def test_defaults_class(self): class A(param.Parameterized): s = param.String() check_defaults(A.param.s, label='S') self._check_defaults(A.param.s) def test_defaults_inst(self): class A(param.Parameterized): s = param.String() a = A() check_defaults(a.param.s, label='S') self._check_defaults(a.param.s) def test_defaults_unbound(self): s = param.String() check_defaults(s, label=None) self._check_defaults(s) def test_regex_ok(self): class A(param.Parameterized): s = param.String('0.0.0.0', regex=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', regex=ip_regex) a = A() exception = "String parameter 'A.s' only takes a string value, not value of ." 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, regex=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 'A.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 's' 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-2.1.1/tests/testtimedependent.py000066400000000000000000000255361463636336300201040ustar00rootroot00000000000000""" Unit tests for the param.Time class, time dependent parameters and time-dependent numbergenerators. """ import copy import fractions import unittest import pytest import param import numbergen 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 from .utils import warnings_as_excepts 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) 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(unittest.TestCase): def setUp(self): super().setUp() param.Dynamic.time_dependent=None self.time_fn= param.Time(time_type=int) class Incrementer: 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))) with warnings_as_excepts(match="Casting type 'str' to Fraction.fraction"): self.assertEqual(hash_1, hashfn("1")) self.assertEqual(hash_42, hashfn(fractions.Fraction(42))) with warnings_as_excepts(match="Casting type 'str' to Fraction.fraction"): self.assertEqual(hash_42, hashfn("42")) self.assertEqual(hash_200001, hashfn(fractions.Fraction(200001))) with warnings_as_excepts(match="Casting type 'str' to Fraction.fraction"): 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) with warnings_as_excepts(match="Casting type 'float' to Fraction.fraction"): self.assertEqual(hashfn(0.5), hashfn(half)) with warnings_as_excepts(match="Casting type 'str' to Fraction.fraction"): 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) with warnings_as_excepts(match="Casting type 'float' to Fraction.fraction"): self.assertEqual(hashfn(0.5), hashfn(gmpy.mpq(0.5))) with warnings_as_excepts(match="Casting type 'str' to Fraction.fraction"): self.assertEqual(hashfn(pi), hashfn(gmpy.mpq(3.141592))) param-2.1.1/tests/testtupleparam.py000066400000000000000000000335171463636336300174270ustar00rootroot00000000000000import unittest import param import pytest from .utils import check_defaults try: import numpy as np except: np = None class TestTupleParameters(unittest.TestCase): def setUp(self): super().setUp() class P(param.Parameterized): e = param.Tuple(default=(1, 1)) f = param.Tuple(default=(0, 0, 0)) g = param.Tuple(default=None, length=3) h = param.Tuple(length=2, allow_None=True) self.P = P def _check_defaults(self, p): assert p.default == (0, 0) assert p.length == 2 assert p.allow_None is False def test_defaults_class(self): class P(param.Parameterized): t = param.Tuple() check_defaults(P.param.t, label='T') self._check_defaults(P.param.t) def test_defaults_inst(self): class P(param.Parameterized): t = param.Tuple() p = P() check_defaults(p.param.t, label='T') self._check_defaults(p.param.t) def test_defaults_unbound(self): t = param.Tuple() check_defaults(t, label=None) self._check_defaults(t) def test_unbound_length_inferred(self): t = param.Tuple((0, 1, 2)) assert t.length == 3 def test_unbound_length_set(self): t = param.Tuple(default=None, length=3) assert t.length == 3 def test_set_object_constructor(self): p = self.P(e=(2, 2)) self.assertEqual(p.e, (2, 2)) def test_length_inferred_from_default(self): p = self.P() assert p.param.f.length == 3 def test_raise_if_value_bad_length_constructor(self): msg = r"Attribute 'length' of Tuple parameter 'P.e' is not of the correct length \(3 instead of 2\)" with self.assertRaisesRegex(ValueError, msg): self.P(e=(1, 1, 'extra')) def test_raise_if_value_bad_length_setattr(self): p = self.P() msg = r"Attribute 'length' of Tuple parameter 'P.e' is not of the correct length \(3 instead of 2\)" with self.assertRaisesRegex(ValueError, msg): p.e = (1, 1, 'extra') def test_raise_if_default_is_None_and_no_length(self): msg = "Attribute 'length' of Tuple parameter 't' must be specified if no default is supplied" with self.assertRaisesRegex(ValueError, msg): class P(param.Parameterized): t = param.Tuple(default=None) def test_None_default(self): p = self.P() assert p.g is None assert p.param.g.length == 3 assert p.param.g.allow_None def test_raise_if_default_is_None_and_bad_length(self): msg = r"Attribute 'length' of Tuple parameter 'P.g' is not of the correct length \(2 instead of 3\)." with self.assertRaisesRegex(ValueError, msg): p = self.P(g=(0, 0)) p = self.P() with self.assertRaisesRegex(ValueError, msg): p.g = (0, 0) def test_bad_type(self): msg = r"Tuple parameter 'P.e' only takes a tuple value, not <(class|type) 'str'>." with self.assertRaisesRegex(ValueError, msg): self.P.e = 'test' with self.assertRaisesRegex(ValueError, msg): self.P(e='test') p = self.P() with self.assertRaisesRegex(ValueError, msg): p.e = 'test' msg = r"Tuple parameter 'e' only takes a tuple value, not <(class|type) 'str'>." with self.assertRaisesRegex(ValueError, msg): class P(param.Parameterized): e = param.Tuple(default='test') def test_support_allow_None(self): p = self.P() assert p.h == (0, 0) p.h = None p.h = (1, 1) assert p.h == (1, 1) class P(param.Parameterized): h = param.Tuple(length=2, allow_None=True) P.h = None P.h = (1, 1) assert P.h == (1, 1) def test_inheritance_length_behavior1(self): class A(param.Parameterized): p = param.Tuple(default=(0, 1, 2)) class B(A): p = param.Tuple() assert B.p == (0, 1, 2) assert B.param.p.length == 3 b = B() assert b.p == (0, 1, 2) assert b.param.p.default == (0, 1, 2) assert b.param.p.length == 3 def test_inheritance_length_behavior2(self): class A(param.Parameterized): p = param.Tuple(default=(0, 1, 2), length=3) class B(A): p = param.Tuple() assert B.p == (0, 1, 2) assert B.param.p.length == 3 b = B() assert b.p == (0, 1, 2) assert b.param.p.default == (0, 1, 2) assert b.param.p.length == 3 def test_inheritance_length_behavior3(self): class A(param.Parameterized): p = param.Tuple() class B(A): p = param.Tuple(default=(0, 1, 2, 3)) assert B.p == (0, 1, 2, 3) assert B.param.p.length == 4 b = B() assert b.p == (0, 1, 2, 3) assert b.param.p.default == (0, 1, 2, 3) assert b.param.p.length == 4 def test_inheritance_length_behavior4(self): class A(param.Parameterized): p = param.Tuple(default=(0, 1, 2)) class B(A): p = param.Tuple(default=(0, 1, 2, 3)) assert B.p == (0, 1, 2, 3) assert B.param.p.length == 4 b = B() assert b.p == (0, 1, 2, 3) assert b.param.p.default == (0, 1, 2, 3) assert b.param.p.length == 4 def test_inheritance_length_behavior5(self): class A(param.Parameterized): p = param.Tuple(default=(0, 1, 2, 3)) class B(A): p = param.Tuple(default=(0, 1, 2)) assert B.p == (0, 1, 2) assert B.param.p.length == 3 b = B() assert b.p == (0, 1, 2) assert b.param.p.default == (0, 1, 2) assert b.param.p.length == 3 class TestNumericTupleParameters(unittest.TestCase): def setUp(self): super().setUp() class P(param.Parameterized): e = param.NumericTuple(default=(1, 1)) f = param.NumericTuple(default=(0, 0, 0)) g = param.NumericTuple(default=None, length=3) h = param.NumericTuple(length=2, allow_None=True) self.P = P def _check_defaults(self, p): assert p.default == (0, 0) assert p.length == 2 assert p.allow_None is False def test_defaults_class(self): class P(param.Parameterized): t = param.NumericTuple() check_defaults(P.param.t, label='T') self._check_defaults(P.param.t) def test_defaults_inst(self): class P(param.Parameterized): t = param.NumericTuple() p = P() check_defaults(p.param.t, label='T') self._check_defaults(p.param.t) def test_defaults_unbound(self): t = param.NumericTuple() check_defaults(t, label=None) self._check_defaults(t) def test_set_object_constructor(self): p = self.P(e=(2, 2)) self.assertEqual(p.e, (2, 2)) def test_length_inferred_from_default(self): p = self.P() assert p.param.f.length == 3 def test_raise_if_value_bad_length_constructor(self): msg = r"Attribute 'length' of NumericTuple parameter 'P.e' is not of the correct length \(3 instead of 2\)" with self.assertRaisesRegex(ValueError, msg): self.P(e=(1, 1, 1)) def test_raise_if_value_bad_length_setattr(self): p = self.P() msg = r"Attribute 'length' of NumericTuple parameter 'P.e' is not of the correct length \(3 instead of 2\)" with self.assertRaisesRegex(ValueError, msg): p.e = (1, 1, 1) def test_raise_if_default_is_None_and_no_length(self): msg = "Attribute 'length' of NumericTuple parameter 't' must be specified if no default is supplied" with self.assertRaisesRegex(ValueError, msg): class P(param.Parameterized): t = param.NumericTuple(default=None) def test_None_default(self): p = self.P() assert p.g is None assert p.param.g.length == 3 assert p.param.g.allow_None def test_raise_if_default_is_None_and_bad_length(self): msg = r"Attribute 'length' of NumericTuple parameter 'P.g' is not of the correct length \(2 instead of 3\)." with self.assertRaisesRegex(ValueError, msg): p = self.P(g=(0, 0)) p = self.P() with self.assertRaisesRegex(ValueError, msg): p.g = (0, 0) def test_bad_type(self): msg = r"NumericTuple parameter 'P.e' only takes a tuple value, not <(class|type) 'str'>." with self.assertRaisesRegex(ValueError, msg): self.P.e = 'test' with self.assertRaisesRegex(ValueError, msg): self.P(e='test') p = self.P() with self.assertRaisesRegex(ValueError, msg): p.e = 'test' msg = r"NumericTuple parameter 'e' only takes a tuple value, not <(class|type) 'str'>." with self.assertRaisesRegex(ValueError, msg): class P(param.Parameterized): e = param.NumericTuple(default='test') def test_support_allow_None(self): p = self.P() assert p.h == (0, 0) p.h = None p.h = (1, 1) assert p.h == (1, 1) class P(param.Parameterized): h = param.NumericTuple(length=2, allow_None=True) P.h = None P.h = (1, 1) assert P.h == (1, 1) def test_raise_on_non_numeric_values(self): msg = r"NumericTuple parameter 'P.e' only takes numeric values, not <(class|type) 'str'>." with self.assertRaisesRegex(ValueError, msg): self.P.e = ('bad', 1) with self.assertRaisesRegex(ValueError, msg): self.P(e=('bad', 1)) p = self.P() with self.assertRaisesRegex(ValueError, msg): p.e = ('bad', 1) msg = r"NumericTuple parameter 'e' only takes numeric values, not <(class|type) 'str'>." with self.assertRaisesRegex(ValueError, msg): class P(param.Parameterized): e = param.NumericTuple(default=('bad', 1)) @pytest.mark.skipif(np is None, reason='NumPy is not available') def test_support_numpy_values(self): self.P(e=(np.int64(1), np.float32(2))) class TestXYCoordinatesParameters(unittest.TestCase): def setUp(self): super().setUp() class P(param.Parameterized): e = param.XYCoordinates(default=(1, 1)) f = param.XYCoordinates(default=(0, 1), allow_None=True) g = param.XYCoordinates(default=(1, 2)) self.P = P def _check_defaults(self, p): assert p.default == (0.0, 0.0) assert p.length == 2 assert p.allow_None is False def test_defaults_class(self): class P(param.Parameterized): t = param.XYCoordinates() self._check_defaults(P.param.t) def test_defaults_inst(self): class P(param.Parameterized): t = param.XYCoordinates() p = P() self._check_defaults(p.param.t) def test_defaults_unbound(self): t = param.XYCoordinates() self._check_defaults(t) def test_set_object_constructor(self): p = self.P(e=(2, 2)) self.assertEqual(p.e, (2, 2)) def test_raise_if_value_bad_length_constructor(self): msg = r"Attribute 'length' of XYCoordinates parameter 'P.e' is not of the correct length \(3 instead of 2\)" with self.assertRaisesRegex(ValueError, msg): self.P(e=(1, 1, 1)) def test_raise_if_value_bad_length_setattr(self): p = self.P() msg = r"Attribute 'length' of XYCoordinates parameter 'P.e' is not of the correct length \(3 instead of 2\)" with self.assertRaisesRegex(ValueError, msg): p.e = (1, 1, 1) def test_bad_type(self): msg = r"XYCoordinates parameter 'P.e' only takes a tuple value, not <(class|type) 'str'>." with self.assertRaisesRegex(ValueError, msg): self.P.e = 'test' with self.assertRaisesRegex(ValueError, msg): self.P(e='test') p = self.P() with self.assertRaisesRegex(ValueError, msg): p.e = 'test' msg = r"XYCoordinates parameter 'e' only takes a tuple value, not <(class|type) 'str'>." with self.assertRaisesRegex(ValueError, msg): class P(param.Parameterized): e = param.XYCoordinates(default='test') def test_support_allow_None_True(self): p = self.P() assert p.f == (0, 1) p.f = None assert p.f is None class P(param.Parameterized): f = param.Range(default=(0, 1), allow_None=True) P.f = None assert P.f is None def test_support_allow_None_False(self): p = self.P() msg = "XYCoordinates parameter 'P.g' only takes a tuple value, not <(class|type) 'NoneType'>." with self.assertRaisesRegex(ValueError, msg): p.g = None msg = "XYCoordinates parameter 'P.g' only takes a tuple value, not <(class|type) 'NoneType'>." with self.assertRaisesRegex(ValueError, msg): self.P.g = None def test_raise_on_non_numeric_values(self): msg = r"XYCoordinates parameter 'P.e' only takes numeric values, not <(class|type) 'str'>." with self.assertRaisesRegex(ValueError, msg): self.P.e = ('bad', 1) with self.assertRaisesRegex(ValueError, msg): self.P(e=('bad', 1)) p = self.P() with self.assertRaisesRegex(ValueError, msg): p.e = ('bad', 1) msg = r"XYCoordinates parameter 'e' only takes numeric values, not <(class|type) 'str'>." with self.assertRaisesRegex(ValueError, msg): class P(param.Parameterized): e = param.XYCoordinates(default=('bad', 1)) @pytest.mark.skipif(np is None, reason='NumPy is not available') def test_support_numpy_values(self): self.P(e=(np.int64(1), np.float32(2))) param-2.1.1/tests/testutils.py000066400000000000000000000264231463636336300164130ustar00rootroot00000000000000import datetime as dt import os from functools import partial import param import pytest from param import guess_param_types, resolve_path from param.parameterized import bothmethod from param._utils import _is_mutable_container, iscoroutinefunction 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 @pytest.fixture def reset_search_paths(): # The default is [os.getcwd()] which doesn't play well with the testing # framework where every test creates a new temporary directory. # This fixture sets it temporarily to []. original = resolve_path.search_paths try: resolve_path.search_paths = [] yield finally: resolve_path.search_paths = original def test_resolve_path_file_default(): assert resolve_path.path_to_file is True assert resolve_path.search_paths == [os.getcwd()] def test_resolve_path_file_not_found(): with pytest.raises(IOError, match='File surelyyoudontexist was not found in the following'): resolve_path('surelyyoudontexist') @pytest.mark.usefixtures('reset_search_paths') def test_resolve_path_file_not_found_other(tmpdir): cdir = os.getcwd() os.chdir(str(tmpdir)) try: with pytest.raises(IOError, match='File notthere was not found in the following'): resolve_path('notthere') finally: os.chdir(cdir) @pytest.mark.usefixtures('reset_search_paths') def test_resolve_path_folder_not_found(tmpdir): cdir = os.getcwd() os.chdir(str(tmpdir)) try: with pytest.raises(IOError, match='Folder notthere was not found in the following'): resolve_path('notthere', path_to_file=False) finally: os.chdir(cdir) @pytest.mark.usefixtures('reset_search_paths') def test_resolve_path_either_not_found(tmpdir): cdir = os.getcwd() os.chdir(str(tmpdir)) try: with pytest.raises(IOError, match='Path notthere was not found in the following'): resolve_path('notthere', path_to_file=None) finally: os.chdir(cdir) @pytest.mark.usefixtures('reset_search_paths') @pytest.mark.parametrize('path_to_file', [True, False, None]) def test_resolve_path_abs_not_found(tmpdir, path_to_file): cdir = os.getcwd() fp = os.path.join(str(tmpdir), 'foo') os.chdir(str(tmpdir)) try: with pytest.raises(IOError, match='not found'): resolve_path(fp, path_to_file=path_to_file) finally: os.chdir(cdir) @pytest.mark.usefixtures('reset_search_paths') def test_resolve_path_cwd_file(tmpdir): cdir = os.getcwd() fp = os.path.join(str(tmpdir), 'foo') open(fp, 'w').close() os.chdir(str(tmpdir)) try: p = resolve_path('foo') assert os.path.isfile(p) assert os.path.basename(p) == 'foo' assert os.path.isabs(p) assert p == fp finally: os.chdir(cdir) @pytest.mark.usefixtures('reset_search_paths') def test_resolve_path_cwd_folder(tmpdir): cdir = os.getcwd() fp = os.path.join(str(tmpdir), 'foo') os.mkdir(fp) os.chdir(str(tmpdir)) try: p = resolve_path('foo', path_to_file=False) assert os.path.isdir(p) assert os.path.basename(p) == 'foo' assert os.path.isabs(p) assert p == fp finally: os.chdir(cdir) @pytest.mark.usefixtures('reset_search_paths') def test_resolve_path_cwd_either_file(tmpdir): cdir = os.getcwd() fp = os.path.join(str(tmpdir), 'foo') open(fp, 'w').close() os.chdir(str(tmpdir)) try: p = resolve_path('foo', path_to_file=None) assert os.path.isfile(p) assert os.path.basename(p) == 'foo' assert os.path.isabs(p) assert p == fp finally: os.chdir(cdir) @pytest.mark.usefixtures('reset_search_paths') def test_resolve_path_cwd_either_folder(tmpdir): cdir = os.getcwd() fp = os.path.join(str(tmpdir), 'foo') os.mkdir(fp) os.chdir(str(tmpdir)) try: p = resolve_path('foo', path_to_file=None) assert os.path.isdir(p) assert os.path.basename(p) == 'foo' assert os.path.isabs(p) assert p == fp finally: os.chdir(cdir) @pytest.mark.usefixtures('reset_search_paths') def test_resolve_path_abs_file(tmpdir): cdir = os.getcwd() fp = os.path.join(str(tmpdir), 'foo') open(fp, 'w').close() os.chdir(str(tmpdir)) try: p = resolve_path(fp) assert os.path.isfile(p) assert os.path.basename(p) == 'foo' assert os.path.isabs(p) assert p == fp finally: os.chdir(cdir) @pytest.mark.usefixtures('reset_search_paths') def test_resolve_path_abs_folder(tmpdir): cdir = os.getcwd() fp = os.path.join(str(tmpdir), 'foo') os.mkdir(fp) os.chdir(str(tmpdir)) try: p = resolve_path(fp, path_to_file=False) assert os.path.isdir(p) assert os.path.basename(p) == 'foo' assert os.path.isabs(p) assert p == fp finally: os.chdir(cdir) @pytest.mark.usefixtures('reset_search_paths') def test_resolve_path_abs_either_file(tmpdir): cdir = os.getcwd() fp = os.path.join(str(tmpdir), 'foo') open(fp, 'w').close() os.chdir(str(tmpdir)) try: p = resolve_path(fp, path_to_file=None) assert os.path.isfile(p) assert os.path.basename(p) == 'foo' assert os.path.isabs(p) assert p == fp finally: os.chdir(cdir) @pytest.mark.usefixtures('reset_search_paths') def test_resolve_path_abs_either_folder(tmpdir): cdir = os.getcwd() fp = os.path.join(str(tmpdir), 'foo') os.mkdir(fp) os.chdir(str(tmpdir)) try: p = resolve_path(fp, path_to_file=None) assert os.path.isdir(p) assert os.path.basename(p) == 'foo' assert os.path.isabs(p) assert p == fp finally: os.chdir(cdir) def test_resolve_path_search_paths_file(tmpdir): fp = os.path.join(str(tmpdir), 'foo') open(fp, 'w').close() p = resolve_path('foo', search_paths=[str(tmpdir)]) assert os.path.isfile(p) assert os.path.basename(p) == 'foo' assert os.path.isabs(p) assert p == fp @pytest.mark.usefixtures('reset_search_paths') def test_resolve_path_search_paths_folder(tmpdir): fp = os.path.join(str(tmpdir), 'foo') os.mkdir(fp) p = resolve_path('foo', search_paths=[str(tmpdir)], path_to_file=False) assert os.path.isdir(p) assert os.path.basename(p) == 'foo' assert os.path.isabs(p) assert p == fp @pytest.mark.usefixtures('reset_search_paths') def test_resolve_path_search_paths_either_file(tmpdir): fp = os.path.join(str(tmpdir), 'foo') open(fp, 'w').close() p = resolve_path('foo', search_paths=[str(tmpdir)], path_to_file=None) assert os.path.isfile(p) assert os.path.basename(p) == 'foo' assert os.path.isabs(p) assert p == fp @pytest.mark.usefixtures('reset_search_paths') def test_resolve_path_search_paths_either_folder(tmpdir): fp = os.path.join(str(tmpdir), 'foo') os.mkdir(fp) p = resolve_path('foo', search_paths=[str(tmpdir)], path_to_file=None) assert os.path.isdir(p) assert os.path.basename(p) == 'foo' assert os.path.isabs(p) assert p == fp @pytest.mark.usefixtures('reset_search_paths') def test_resolve_path_search_paths_multiple_file(tmpdir): d1 = os.path.join(str(tmpdir), 'd1') d2 = os.path.join(str(tmpdir), 'd2') os.mkdir(d1) os.mkdir(d2) fp1 = os.path.join(d1, 'foo1') open(fp1, 'w').close() fp2 = os.path.join(d2, 'foo2') open(fp2, 'w').close() p = resolve_path('foo1', search_paths=[d1, d2]) assert os.path.isfile(p) assert os.path.basename(p) == 'foo1' assert os.path.isabs(p) assert p == fp1 p = resolve_path('foo2', search_paths=[d1, d2]) assert os.path.isfile(p) assert os.path.basename(p) == 'foo2' assert os.path.isabs(p) assert p == fp2 def test_both_method(): class A: @bothmethod def method(self_or_cls): return self_or_cls assert A.method() is A a = A() assert a.method() is a def test_error_prefix_unbound_defined(): with pytest.raises(ValueError, match="Number parameter 'x' only"): x = param.Number('wrong') # noqa def test_error_prefix_unbound_unexpected_pattern(): from param import Number with pytest.raises(ValueError, match="Number parameter only"): Number('wrong') def test_error_prefix_before_class_creation(): with pytest.raises(ValueError, match="Number parameter 'x' only"): class P(param.Parameterized): x = param.Number('wrong') def test_error_prefix_set_class(): class P(param.Parameterized): x = param.Number() with pytest.raises(ValueError, match="Number parameter 'P.x' only"): P.x = 'wrong' def test_error_prefix_instantiate(): class P(param.Parameterized): x = param.Number() with pytest.raises(ValueError, match="Number parameter 'P.x' only"): P(x='wrong') def test_error_prefix_set_instance(): class P(param.Parameterized): x = param.Number() p = P() with pytest.raises(ValueError, match="Number parameter 'P.x' only"): p.x = 'wrong' @pytest.mark.parametrize( ('obj,ismutable'), [ ([1, 2], True), ({1, 2}, True), ({'a': 1, 'b': 2}, True), ((1, 2), False), ('string', False), (frozenset([1, 2]), False) ] ) def test__is_mutable_container(obj, ismutable): assert _is_mutable_container(obj) is ismutable async def coro(): return def test_iscoroutinefunction_coroutine(): assert iscoroutinefunction(coro) def test_iscoroutinefunction_partial_coroutine(): pcoro = partial(partial(coro)) assert iscoroutinefunction(pcoro) async def agen(): yield def test_iscoroutinefunction_asyncgen(): assert iscoroutinefunction(agen) def test_iscoroutinefunction_partial_asyncgen(): pagen = partial(partial(agen)) assert iscoroutinefunction(pagen) param-2.1.1/tests/testversion.py000066400000000000000000000001731463636336300167320ustar00rootroot00000000000000from param.version import run_cmd def test_run_cmd(): output = run_cmd(['echo', 'test']) assert output == 'test' param-2.1.1/tests/testwatch.py000066400000000000000000000764561463636336300163740ustar00rootroot00000000000000""" Unit test for watch mechanism """ import copy import unittest import param import pytest from param.parameterized import Skip, discard_events from .utils import MockLoggingHandler, warnings_as_excepts class Accumulator: 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(unittest.TestCase): @classmethod def setUpClass(cls): super().setUpClass() log = param.parameterized.get_logger() cls.log_handler = MockLoggingHandler(level='DEBUG') log.addHandler(cls.log_handler) def setUp(self): super().setUp() self.accumulator = 0 self.list_accumulator = [] def tearDown(self): SimpleWatchExample.param.d.bounds = None 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_triggered_ignore_skip(self): def accumulator(change): if change.new > 1: raise Skip() self.accumulator += 1 obj = SimpleWatchExample() obj.param.watch(accumulator, 'a') obj.a = 1 self.assertEqual(self.accumulator, 1) obj.a = 2 self.assertEqual(self.accumulator, 1) 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 = () 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) with warnings_as_excepts(match='No such watcher'): obj.param.unwatch(watcher) try: param.parameterized.warnings_as_exceptions = False obj.param.unwatch(watcher) self.log_handler.assertEndsWith('WARNING', ' to remove.') finally: param.parameterized.warnings_as_exceptions = True 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.parameterized.batch_call_watchers(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) 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.parameterized.batch_call_watchers(obj): obj.a = 23 obj.b = 42 obj.c = 99 self.assertEqual(accumulator.call_count(), 2) 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_param_slot(self): obj = SimpleWatchExample() cls_events = [] SimpleWatchExample.param.watch(cls_events.append, 'd', what='bounds') obj.param.objects('existing')['d'].bounds = (1, 2) assert len(cls_events) == 1 assert cls_events[0].name == 'd' assert cls_events[0].what == 'bounds' assert cls_events[0].new == (1, 2) inst_events = [] obj.param.watch(inst_events.append, 'd', what='bounds') obj.param.objects('existing')['d'].bounds = (3, 4) assert len(cls_events) == 1 assert len(inst_events) == 1 assert inst_events[0].name == 'd' assert inst_events[0].what == 'bounds' assert inst_events[0].new == (3, 4) def test_param_watch_no_side_effect(self): # Example 1 of https://github.com/holoviz/param/issues/829 class P(param.Parameterized): x = param.Parameter() store = [] P.param.watch(store.append, 'x') P.x = 10 assert len(store) == 1 assert 'value' in P.param.x.watchers p = P() assert 'value' in P.param.x.watchers # Checking this does not have bad side-effects p.param.x # Watcher still on the class Parameter assert 'value' in P.param.x.watchers # Watcher not on the instance assert p.param.x.watchers == {} P.x = 20 # Watcher still triggered assert len(store) == 2 p.x = 30 # Watcher not triggerd on instance update assert len(store) == 2 def test_param_watch_multiple_instances(self): # Example 4 of https://github.com/holoviz/param/issues/829 class P(param.Parameterized): x = param.Parameter() l = param.List([]) @param.depends('x:constant', watch=True) def cb(self): self.l.append(self.param.x.constant) assert P.param.x.watchers == {} p = P() # Creating the instance ??? assert P.param.x.watchers == {} p2 = P() # Modify constant on p2.param.x p2.param.x.constant = True # The event should only trigger on p2, not p assert p2.l == [True] assert p.l == [] def test_watch_deepcopy(self): obj = SimpleWatchExample() obj.param.watch(obj.method, ['a']) obj.param.watch(lambda x: None, 'd', what='bounds') 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) def test_watch_watchers_exposed(self): obj = SimpleWatchExample() obj.param.watch(lambda: '', ['a', 'b']) with pytest.warns(param._utils.ParamFutureWarning): pw = obj._param_watchers assert isinstance(pw, dict) for pname in ('a', 'b'): assert pname in pw assert 'value' in pw[pname] assert isinstance(pw[pname]['value'], list) and len(pw[pname]['value']) == 1 assert isinstance(pw[pname]['value'][0], param.parameterized.Watcher) def test_watch_watchers_modified(self): accumulator = Accumulator() obj = SimpleWatchExample() obj.param.watch(accumulator, ['a', 'b']) with pytest.warns(param._utils.ParamFutureWarning): pw = obj._param_watchers del pw['a'] obj.param.update(a=1, b=1) assert accumulator.call_count() == 1 args = accumulator.args_for_call(0) assert len(args) == 1 assert args[0].name == 'b' def test_watch_watchers_exposed_public(self): obj = SimpleWatchExample() obj.param.watch(lambda: '', ['a', 'b']) pw = obj.param.watchers assert isinstance(pw, dict) for pname in ('a', 'b'): assert pname in pw assert 'value' in pw[pname] assert isinstance(pw[pname]['value'], list) and len(pw[pname]['value']) == 1 assert isinstance(pw[pname]['value'][0], param.parameterized.Watcher) def test_watch_watchers_modified_public(self): accumulator = Accumulator() obj = SimpleWatchExample() obj.param.watch(accumulator, ['a', 'b']) pw = obj.param.watchers del pw['a'] obj.param.update(a=1, b=1) assert accumulator.call_count() == 1 args = accumulator.args_for_call(0) assert len(args) == 1 assert args[0].name == 'b' def test_watch_watchers_setter_public(self): accumulator = Accumulator() obj = SimpleWatchExample() obj.param.watch(accumulator, ['a', 'b']) obj.param.watchers = {} obj.param.update(a=1, b=1) assert accumulator.call_count() == 0 def test_watch_watchers_class_error(self): with pytest.raises( TypeError, match=r"Accessing `\.param\.watchers` is only supported on a Parameterized instance, not class\." ): SimpleWatchExample.param.watchers def test_watch_watchers_class_set_error(self): with pytest.raises( TypeError, match=r"Setting `\.param\.watchers` is only supported on a Parameterized instance, not class\." ): SimpleWatchExample.param.watchers = {} class TestWatchMethod(unittest.TestCase): 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(unittest.TestCase): def setUp(self): super().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) 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(unittest.TestCase): def setUp(self): super().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(unittest.TestCase): def setUp(self): super().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.parameterized.batch_call_watchers(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-2.1.1/tests/utils.py000066400000000000000000000141011463636336300155010ustar00rootroot00000000000000import asyncio import logging import time from contextlib import contextmanager import param 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().__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 output: {substring}'.format( 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))) def check_defaults(parameter, label, skip=[]): # ! Not testing default and allow_None if 'doc' not in skip: assert parameter.doc is None if 'precedence' not in skip: assert parameter.precedence is None if 'instantiate' not in skip: assert parameter.instantiate is False if 'constant' not in skip: assert parameter.constant is False if 'readonly' not in skip: assert parameter.readonly is False if 'pickle_default_value' not in skip: assert parameter.pickle_default_value is True if 'per_instance' not in skip: assert parameter.per_instance is True if 'label' not in skip: assert parameter.label == label @contextmanager def warnings_as_excepts(match=None): orig = param.parameterized.warnings_as_exceptions param.parameterized.warnings_as_exceptions = True try: yield except Exception as e: if match and match not in str(e): raise ValueError(f'Exception emitted {str(e)!r} does not contain {match!r}') finally: param.parameterized.warnings_as_exceptions = orig async def async_wait_until(fn, timeout=5000, interval=100): """ Exercise a test function in a loop until it evaluates to True or times out. The function can either be a simple lambda that returns True or False: >>> await async_wait_until(lambda: x.values() == ['x']) Or a defined function with an assert: >>> async def _() >>> assert x.values() == ['x'] >>> await async_wait_until(_) Parameters ---------- fn : callable Callback timeout : int, optional Total timeout in milliseconds, by default 5000 interval : int, optional Waiting interval, by default 100 Adapted from pytest-qt. """ # Hide this function traceback from the pytest output if the test fails __tracebackhide__ = True start = time.monotonic() def timed_out(): elapsed = time.monotonic() - start elapsed_ms = elapsed * 1000 return elapsed_ms > timeout timeout_msg = f"async_wait_until timed out in {timeout} milliseconds" while True: try: result = fn() if asyncio.iscoroutine(result): result = await result except AssertionError as e: if timed_out(): raise TimeoutError(timeout_msg) from e else: if result not in (None, True, False): raise ValueError( "`async_wait_until` callback must return None, True, or " f"False, returned {result!r}" ) # None is returned when the function has an assert if result is None: return # When the function returns True or False if result: return if timed_out(): raise TimeoutError(timeout_msg) await asyncio.sleep(interval / 1000)