diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index f362e6edb9..16135a21bf 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -8,6 +8,19 @@ on: branches: - '*' workflow_dispatch: + inputs: + target: + description: "How much of the test suite to run" + type: choice + default: default + options: + - default + - full + - downstream + cache: + description: "Use cache" + type: boolean + default: true schedule: - cron: '0 19 * * SUN' @@ -21,16 +34,82 @@ jobs: runs-on: 'ubuntu-latest' steps: - uses: holoviz-dev/holoviz_tasks/pre-commit@v0.1a19 + setup: + name: Setup workflow + runs-on: ubuntu-latest + permissions: + pull-requests: read + outputs: + code_change: ${{ steps.filter.outputs.code }} + doc_change: ${{ steps.filter.outputs.doc }} + matrix: ${{ env.MATRIX }} + steps: + - uses: actions/checkout@v3 + if: github.event_name != 'pull_request' + - name: Check for code changes + uses: dorny/paths-filter@v2.11.1 + id: filter + with: + filters: | + code: + - 'panel/**' + - 'examples/**' + - 'setup.py' + - 'pyproject.toml' + - 'tox.ini' + - '.github/workflows/test.yaml' + doc: + - 'doc/getting_started/**' + - 'doc/how_to/**' + - 'scripts/**' + - name: Set matrix option + run: | + if [[ '${{ github.event_name }}' == 'workflow_dispatch' ]]; then + OPTION=${{ github.event.inputs.target }} + elif [[ '${{ github.event_name }}' == 'schedule' ]]; then + OPTION="full" + elif [[ '${{ github.event_name }}' == 'push' && '${{ github.ref_type }}' == 'tag' ]]; then + OPTION="full" + else + OPTION="default" + fi + echo "MATRIX_OPTION=$OPTION" >> $GITHUB_ENV + - name: Set test matrix with 'default' option + if: env.MATRIX_OPTION == 'default' + run: | + MATRIX=$(jq -nsc '{ + "os": ["ubuntu-latest", "macos-latest", "windows-latest"], + "python-version": ["3.9", "3.11"], + "include": [ + {"os": "ubuntu-latest", "python-version": "3.10"} + ] + }') + echo "MATRIX=$MATRIX" >> $GITHUB_ENV + - name: Set test matrix with 'full' option + if: env.MATRIX_OPTION == 'full' + run: | + MATRIX=$(jq -nsc '{ + "os": ["ubuntu-latest", "macos-latest", "windows-latest"], + "python-version": ["3.9", "3.10", "3.11"] + }') + echo "MATRIX=$MATRIX" >> $GITHUB_ENV + - name: Set test matrix with 'downstream' option + if: env.MATRIX_OPTION == 'downstream' + run: | + MATRIX=$(jq -nsc '{ + "os": ["ubuntu-latest"], + "python-version": ["3.11"] + }') + echo "MATRIX=$MATRIX" >> $GITHUB_ENV + unit_test_suite: name: Unit tests on ${{ matrix.os }} with Python ${{ matrix.python-version }} - needs: [pre_commit] + needs: [pre_commit, setup] runs-on: ${{ matrix.os }} + if: needs.setup.outputs.code_change == 'true' strategy: fail-fast: false - matrix: - os: ['ubuntu-latest', 'macos-latest', 'windows-latest'] - # Run on the full set on schedule, workflow_dispatch and push&tags events, otherwise on a subset. - python-version: ${{ ( github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || ( github.event_name == 'push' && github.ref_type == 'tag' ) ) && fromJSON('["3.9", "3.10", "3.11"]') || fromJSON('["3.9", "3.11"]') }} + matrix: ${{ fromJson(needs.setup.outputs.matrix) }} timeout-minutes: 90 defaults: run: @@ -56,7 +135,7 @@ jobs: conda-update: true nodejs: true envs: -o examples -o recommended -o tests -o build - cache: true + cache: ${{ github.event.inputs.cache || github.event.inputs.cache == '' }} opengl: true id: install - name: doit develop_install @@ -98,8 +177,9 @@ jobs: fail_ci_if_error: false # optional (default = false) ui_test_suite: name: UI tests on ${{ matrix.os }} with Python 3.9 - needs: [pre_commit] + needs: [pre_commit, setup] runs-on: ${{ matrix.os }} + if: needs.setup.outputs.code_change == 'true' || needs.setup.outputs.doc_change == 'true' strategy: fail-fast: false matrix: @@ -144,7 +224,7 @@ jobs: python-version: 3.9 channels: pyviz/label/dev,bokeh,conda-forge,nodefaults envs: "-o recommended -o tests -o build" - cache: true + cache: ${{ github.event.inputs.cache || github.event.inputs.cache == '' }} nodejs: true playwright: true id: install @@ -199,8 +279,9 @@ jobs: fail_ci_if_error: false # optional (default = false) core_test_suite: name: Core tests on Python ${{ matrix.python-version }}, ${{ matrix.os }} - needs: [pre_commit] + needs: [pre_commit, setup] runs-on: ${{ matrix.os }} + if: needs.setup.outputs.code_change == 'true' strategy: fail-fast: false matrix: @@ -223,7 +304,7 @@ jobs: # # channel-priority: strict # channels: pyviz/label/dev,conda-forge,nodefaults # envs: "-o tests_core -o tests_ci" - # cache: true + # cache: ${{ github.event.inputs.cache || github.event.inputs.cache == '' }} # conda-update: true # id: install - uses: actions/checkout@v3 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5707fe4199..3a6e21dd31 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,17 +17,17 @@ repos: exclude: \.min\.js$ - id: trailing-whitespace - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.1.6 + rev: v0.1.9 hooks: - id: ruff files: panel/ - repo: https://github.com/pycqa/isort - rev: 5.12.0 + rev: 5.13.2 hooks: - id: isort name: isort (python) - repo: https://github.com/hoxbro/clean_notebook - rev: v0.1.13 + rev: v0.1.14 hooks: - id: clean-notebook args: [-i, tags] @@ -39,7 +39,7 @@ repos: additional_dependencies: - tomli - repo: https://github.com/pre-commit/mirrors-prettier - rev: v3.0.3 + rev: v4.0.0-alpha.8 hooks: - id: prettier types_or: [css] diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b3fc15109..865b771030 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,58 @@ # Releases +## Version 1.3.7 + +Date: 2024-01-19 + +This patch release focuses on a number of fixes and minor enhancements for the chat components and various other smaller improvements and fixes including docs improvements. In particular we want to highlight the new Ploomber deployment guide contributed by @neelash23. Next we want to welcome @jz314, @fayssalelmofatiche and @neelasha23 as new contributors and welcome back @SultanOrazbayev as a returning contributor. Lastly we want to thank the core contributor team, including @MarcSkovMadsen, @ahuang11, @maximlt, @Hoxbro and @philippjfr for their continued efforts maintaining Panel. + +### Enhancements + +- Add `filter_by` to `ChatMessage.serialize` ([#6090](https://github.com/holoviz/panel/pull/6090)) +- Support using an SVG for `ToggleIcon` ([#6127](https://github.com/holoviz/panel/pull/6127)) +- Add resizable param to `TextAreaInput` ([#6126](https://github.com/holoviz/panel/pull/6126)) +- Improve date and datetime picker functionality ([#6152](https://github.com/holoviz/panel/pull/6152)) +- Add activity indicator to `ChatMessage` ([#6153](https://github.com/holoviz/panel/pull/6153)) +- Lazily import bleach HTML sanitizer ([#6179](https://github.com/holoviz/panel/pull/6179)) + +### Bug fixes + +- Fix alignment issues in chat components ([#6104](https://github.com/holoviz/panel/pull/6104), [#6135](https://github.com/holoviz/panel/pull/6135)) +- Fix generator placeholder and optimize updates in Chat components ([#6105](https://github.com/holoviz/panel/pull/6105)) +- Fix issue with callback future handling on Chat components ([#6120](https://github.com/holoviz/panel/pull/6120)) +- Fix bug in Chat interfaces related to `pn.state.browser_info` ([#6122](https://github.com/holoviz/panel/pull/6122)) +- Allow instantiating empty `Matplotlib` pane ([#6128](https://github.com/holoviz/panel/pull/6128)) +- Ensure icon displays inline with text on `FileDownload` ([#6133](https://github.com/holoviz/panel/pull/6133)) +- Fix styling of links in `Tabulator` fast theme ([#6146](https://github.com/holoviz/panel/pull/6146)) +- Fix passing of `card_params` on `ChatFeed` ([#6154](https://github.com/holoviz/panel/pull/6154)) +- Handle `Tabulator.title_formatter` if is type `dict` ([#6166](https://github.com/holoviz/panel/pull/6166)) +- Fix `per_session` caching ([#6169](https://github.com/holoviz/panel/pull/6169)) +- Correctly reshape nd-arrays in `Plotly` pane ([#6174](https://github.com/holoviz/panel/pull/6174)) +- Handle NaT values on `Perspective` pane ([#6176](https://github.com/holoviz/panel/pull/6176)) +- Do not rerender output if `ReplacementPane` object identity is unchanged ([#6183](https://github.com/holoviz/panel/pull/6183)) +- Tabulator: fix valuesLookup set up for older list-like editors ([#6192](https://github.com/holoviz/panel/pull/6192)) +- Fix pyodide loading message styling issues ([#6194](https://github.com/holoviz/panel/pull/6194)) +- More complete patch for the `TextEditor` to support being rendered in the Shadow DOM ([#6222](https://github.com/holoviz/panel/pull/6222)) +- Add guard to `Tabulator` ensuring that it does not error when it is not rendered ([#6223](https://github.com/holoviz/panel/pull/6223)) +- Fix race conditions when instantiating Comm in Jupyter causing notifications to break ([#6229](https://github.com/holoviz/panel/pull/6229)) + +### Compatibility & Security + +- Upgrade Plotly.js to 2.25.3 to address CVE-2023-46308 ([#6230](https://github.com/holoviz/panel/pull/6230)) + +### Documentation + +- Add `Design` and `Theme` explanation documentation ([#4741](https://github.com/holoviz/panel/pull/4741)) +- Fix pyodide execution in documentation +- Fix wrong and broken link ([#5988](https://github.com/holoviz/panel/pull/5988), [#6132](https://github.com/holoviz/panel/pull/6132)) +- Use GoatCounter for website analytics ([#6117](https://github.com/holoviz/panel/pull/6117)) +- Add Dask How to guide ([#4234](https://github.com/holoviz/panel/pull/4234)) +- Fix `Material` template notebook .show() call ([#6137](https://github.com/holoviz/panel/pull/6137)) +- Add missing item in docstring ([#6167](https://github.com/holoviz/panel/pull/6167)) +- Ploomber Cloud deployment documentation ([#6182](https://github.com/holoviz/panel/pull/6182)) +- Correct duplicate wording ([#6188](https://github.com/holoviz/panel/pull/6188)) +- Update JupyterLite Altair example to latest API ([#6226](https://github.com/holoviz/panel/pull/6226)) + ## Version 1.3.6 Date: 2023-12-20 diff --git a/doc/_static/images/asyncify.png b/doc/_static/images/asyncify.png new file mode 100644 index 0000000000..527976db87 Binary files /dev/null and b/doc/_static/images/asyncify.png differ diff --git a/doc/_static/images/dask-dashboard-empty.png b/doc/_static/images/dask-dashboard-empty.png new file mode 100644 index 0000000000..91473648b6 Binary files /dev/null and b/doc/_static/images/dask-dashboard-empty.png differ diff --git a/doc/_static/images/dask_fibonacci_queue.png b/doc/_static/images/dask_fibonacci_queue.png new file mode 100644 index 0000000000..6ecbca2760 Binary files /dev/null and b/doc/_static/images/dask_fibonacci_queue.png differ diff --git a/doc/_static/images/ploomber_deployment.png b/doc/_static/images/ploomber_deployment.png new file mode 100644 index 0000000000..922c46c780 Binary files /dev/null and b/doc/_static/images/ploomber_deployment.png differ diff --git a/doc/_static/logos/dask-logo.svg b/doc/_static/logos/dask-logo.svg new file mode 100644 index 0000000000..3ac766541b --- /dev/null +++ b/doc/_static/logos/dask-logo.svg @@ -0,0 +1,18 @@ + diff --git a/doc/_static/logos/ploomber.png b/doc/_static/logos/ploomber.png new file mode 100644 index 0000000000..9a18e0903f Binary files /dev/null and b/doc/_static/logos/ploomber.png differ diff --git a/doc/about/releases.md b/doc/about/releases.md index 6d1e9d54bd..d6af9fc7e0 100644 --- a/doc/about/releases.md +++ b/doc/about/releases.md @@ -2,6 +2,59 @@ See [the HoloViz blog](https://blog.holoviz.org/#category=panel) for a visual summary of the major features added in each release. +## Version 1.3.7 + +Date: 2024-01-19 + +This patch release focuses on a number of fixes and minor enhancements for the chat components and various other smaller improvements and fixes including docs improvements. In particular we want to highlight the new Ploomber deployment guide contributed by @neelash23. Next we want to welcome @jz314, @fayssalelmofatiche and @neelasha23 as new contributors and welcome back @SultanOrazbayev as a returning contributor. Lastly we want to thank the core contributor team, including @MarcSkovMadsen, @ahuang11, @maximlt, @Hoxbro and @philippjfr for their continued efforts maintaining Panel. + +### Enhancements + +- Add `filter_by` to `ChatMessage.serialize` ([#6090](https://github.com/holoviz/panel/pull/6090)) +- Support using an SVG for `ToggleIcon` ([#6127](https://github.com/holoviz/panel/pull/6127)) +- Add resizable param to `TextAreaInput` ([#6126](https://github.com/holoviz/panel/pull/6126)) +- Improve date and datetime picker functionality ([#6152](https://github.com/holoviz/panel/pull/6152)) +- Add activity indicator to `ChatMessage` ([#6153](https://github.com/holoviz/panel/pull/6153)) +- Lazily import bleach HTML sanitizer ([#6179](https://github.com/holoviz/panel/pull/6179)) + +### Bug fixes + +- Fix alignment issues in chat components ([#6104](https://github.com/holoviz/panel/pull/6104), [#6135](https://github.com/holoviz/panel/pull/6135)) +- Fix generator placeholder and optimize updates in Chat components ([#6105](https://github.com/holoviz/panel/pull/6105)) +- Fix issue with callback future handling on Chat components ([#6120](https://github.com/holoviz/panel/pull/6120)) +- Fix bug in Chat interfaces related to `pn.state.browser_info` ([#6122](https://github.com/holoviz/panel/pull/6122)) +- Allow instantiating empty `Matplotlib` pane ([#6128](https://github.com/holoviz/panel/pull/6128)) +- Ensure icon displays inline with text on `FileDownload` ([#6133](https://github.com/holoviz/panel/pull/6133)) +- Fix styling of links in `Tabulator` fast theme ([#6146](https://github.com/holoviz/panel/pull/6146)) +- Fix passing of `card_params` on `ChatFeed` ([#6154](https://github.com/holoviz/panel/pull/6154)) +- Handle `Tabulator.title_formatter` if is type `dict` ([#6166](https://github.com/holoviz/panel/pull/6166)) +- Fix `per_session` caching ([#6169](https://github.com/holoviz/panel/pull/6169)) +- Correctly reshape nd-arrays in `Plotly` pane ([#6174](https://github.com/holoviz/panel/pull/6174)) +- Handle NaT values on `Perspective` pane ([#6176](https://github.com/holoviz/panel/pull/6176)) +- Do not rerender output if `ReplacementPane` object identity is unchanged ([#6183](https://github.com/holoviz/panel/pull/6183)) +- Tabulator: fix valuesLookup set up for older list-like editors ([#6192](https://github.com/holoviz/panel/pull/6192)) +- Fix pyodide loading message styling issues ([#6194](https://github.com/holoviz/panel/pull/6194)) +- More complete patch for the `TextEditor` to support being rendered in the Shadow DOM ([#6222](https://github.com/holoviz/panel/pull/6222)) +- Add guard to `Tabulator` ensuring that it does not error when it is not rendered ([#6223](https://github.com/holoviz/panel/pull/6223)) +- Fix race conditions when instantiating Comm in Jupyter causing notifications to break ([#6229](https://github.com/holoviz/panel/pull/6229)) + +### Compatibility & Security + +- Upgrade Plotly.js to 2.25.3 to address CVE-2023-46308 ([#6230](https://github.com/holoviz/panel/pull/6230)) + +### Documentation + +- Add `Design` and `Theme` explanation documentation ([#4741](https://github.com/holoviz/panel/pull/4741)) +- Fix pyodide execution in documentation +- Fix wrong and broken link ([#5988](https://github.com/holoviz/panel/pull/5988), [#6132](https://github.com/holoviz/panel/pull/6132)) +- Use GoatCounter for website analytics ([#6117](https://github.com/holoviz/panel/pull/6117)) +- Add Dask How to guide ([#4234](https://github.com/holoviz/panel/pull/4234)) +- Fix `Material` template notebook .show() call ([#6137](https://github.com/holoviz/panel/pull/6137)) +- Add missing item in docstring ([#6167](https://github.com/holoviz/panel/pull/6167)) +- Ploomber Cloud deployment documentation ([#6182](https://github.com/holoviz/panel/pull/6182)) +- Correct duplicate wording ([#6188](https://github.com/holoviz/panel/pull/6188)) +- Update JupyterLite Altair example to latest API ([#6226](https://github.com/holoviz/panel/pull/6226)) + ## Version 1.3.6 Date: 2023-12-20 diff --git a/doc/conf.py b/doc/conf.py index a4e2a5d8f6..943fb2e00b 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -81,7 +81,8 @@ 'sphinx.ext.napoleon', 'nbsite.gallery', 'sphinx_copybutton', - 'nbsite.pyodide' + 'nbsite.pyodide', + 'nbsite.analytics', ] napoleon_numpy_docstring = True @@ -92,6 +93,10 @@ jlite_url = 'https://pyviz-dev.github.io/panelite-dev' if is_dev else 'https://panelite.holoviz.org' pyodide_url = 'https://pyviz-dev.github.io/panel/pyodide' if is_dev else 'https://panel.holoviz.org/pyodide' +nbsite_analytics = { + 'goatcounter_holoviz': True, +} + nbsite_gallery_conf = { 'github_org': 'holoviz', 'github_project': 'panel', diff --git a/doc/explanation/index.md b/doc/explanation/index.md index da6f70ff75..8c3b98d07b 100644 --- a/doc/explanation/index.md +++ b/doc/explanation/index.md @@ -58,14 +58,22 @@ Deepen your understanding about how Panel communicates between Python and Javasc ::::{grid} 1 2 2 3 :gutter: 1 1 1 2 +:::{grid-item-card} {octicon}`paintbrush;2.5em;sd-mr-1 sd-animate-grow50` Designs & Themes +:link: styling/design +:link-type: doc + +Understand how the Panel `Design` and `Theme` components work internally. +::: + :::{grid-item-card} {octicon}`repo-template;2.5em;sd-mr-1 sd-animate-grow50` Templates -:link: templates/templates_overview +:link: styling/templates_overview :link-type: doc -Deepen your understanding about Template styling in Panel. +Discover Panel templates and how to use them. ::: :::: + ## Dependencies ::::{grid} 1 2 2 3 diff --git a/doc/explanation/styling.md b/doc/explanation/styling.md deleted file mode 100644 index f89d51aba1..0000000000 --- a/doc/explanation/styling.md +++ /dev/null @@ -1,14 +0,0 @@ -.. raw:: html -
- - - -# Styling - -```{toctree} -:titlesonly: -:hidden: -:maxdepth: 1 - -Templates
+
+Panel supports `async` and `await`. This means you can easily **offload large computations to your Dask cluster asynchronously and keep your app responsive while you `await` the results**. Please note that off loading the computations to the Dask cluster can add ~250msec of overhead and thus is not suitable for all kinds of use cases.
+
+## Installation
+
+Lets start by installing *Panel*, *hvPlot* and *Dask Distributed*.
+
+```bash
+pip install panel hvplot dask[distributed]
+```
+
+## Start the Cluster
+
+For development, testing and many use cases a [`LocalCluster`](https://docs.dask.org/en/stable/deploying-python.html#localcluster) is more than fine and will allow you to leverage all the CPUs on your machine. When you want to scale out to an entire cluster will you can switch to a non-local cluster. To avoid any issues when combining Panel and Dask we recommend starting the `LocalCluster`
+separately from the Dask `Client` and your Panel app.
+
+```python
+# cluster.py
+from dask.distributed import LocalCluster
+
+DASK_SCHEDULER_PORT = 64719
+DASK_SCHEDULER_ADDRESS = f"tcp://127.0.0.1:{DASK_SCHEDULER_PORT}"
+N_WORKERS = 4
+
+if __name__ == '__main__':
+ cluster = LocalCluster(scheduler_port=DASK_SCHEDULER_PORT, n_workers=N_WORKERS)
+ print(cluster.scheduler_address)
+ input()
+```
+
+and running
+
+```bash
+$ python cluster.py
+tcp://127.0.0.1:64719
+```
+
+You can now open the [Dask Dashboard](https://docs.dask.org/en/stable/dashboard.html) at [http://localhost:8787/status](http://localhost:8787/status).
+
+So far there is not a lot to see here:
+
+
+
+The Dask `Client` will serialize any *tasks* and send them to the Dask `Cluster` for execution. This means that the `Client` and `Cluster` must able to import the same versions of all *tasks* and python package dependencies.
+
+## Dask Distributed
+
+## Fibonacci Task Queue
+
+In this section we will define a Panel app to *submit* and *monitor* Fibonacci tasks.
+
+Let's start by defining the *fibonacci* tasks in a `tasks.py` file:
+
+```python
+# tasks.py
+from datetime import datetime as dt
+
+import numpy as np
+
+
+def _fib(n):
+ if n < 2:
+ return n
+ else:
+ return _fib(n - 1) + _fib(n - 2)
+
+
+def fibonacci(n):
+ start = dt.now()
+ print(start, "start", n)
+ result = _fib(n)
+ end = dt.now()
+ print(end, "end", (end-start).seconds, n, result)
+ return result
+```
+
+Lets now define the full `app.py` file.
+
+```python
+# app.py
+from datetime import datetime as dt
+
+from dask.distributed import Client
+
+import panel as pn
+
+from cluster import DASK_SCHEDULER_ADDRESS
+from tasks import fibonacci
+
+QUEUE = []
+
+pn.extension("terminal", design="material", sizing_mode="stretch_width")
+
+@pn.cache # We use caching to share the client across all users and sessions
+async def get_client():
+ return await Client(
+ DASK_SCHEDULER_ADDRESS, asynchronous=True
+ )
+
+n_input = pn.widgets.IntInput(value=0, width=100, sizing_mode="fixed", name="n")
+submit_button = pn.widgets.Button(name="SUBMIT", button_type="primary", align="end")
+terminal_widget = pn.widgets.Terminal(
+ height=200,
+)
+
+queue = pn.rx(QUEUE)
+
+@pn.depends(submit_button, watch=True)
+async def _handle_click(_):
+ n = n_input.value
+ n_input.value += 1
+
+ start = dt.now()
+ QUEUE.append(n)
+ queue.rx.value = QUEUE
+
+ client = await get_client()
+ fib_n = await client.submit(fibonacci, n)
+
+ end = dt.now()
+
+ QUEUE.pop(QUEUE.index(n))
+ queue.rx.value = QUEUE
+
+ duration = (end - start).seconds
+ terminal_widget.write(f"fibonacci({n})={fib_n} in {duration}sec\n")
+
+
+pn.Column(
+ "# Fibonacci Tasks",
+ pn.Row(n_input, submit_button),
+ pn.rx("## Task queue: {}").format(queue),
+ "## Results",
+ terminal_widget,
+).servable()
+```
+
+You can now run `panel serve app.py` and the app will look like
+
+
+
+## Dask Dashboard Components
+
+It can be very useful to include some of the live [Dask endpoints](https://distributed.dask.org/en/stable/http_services.html) in your app. Its easy to do by embedding the specific urls in an *iframe*.
+
+In the `dashboard.py` file we define the `DaskViewer` component that can be used to explore the *individual dask plots*.
+
+```python
+# dashboard.py
+import os
+
+import param
+
+import panel as pn
+
+DASK_DASHBOARD_ADDRESS = os.getenv("DASK_DASHBOARD", "http://localhost:8787/status")
+
+VIEWS = {
+ "aggregate-time-per-action": "individual-aggregate-time-per-action",
+ "bandwidth-types": "individual-bandwidth-types",
+ "bandwidth-workers": "individual-bandwidth-workers",
+ "cluster-memory": "individual-cluster-memory",
+ "compute-time-per-key": "individual-compute-time-per-key",
+ "cpu": "individual-cpu",
+ "exceptions": "individual-exceptions",
+ "gpu-memory": "individual-gpu-memory",
+ "gpu-utilization": "individual-gpu-utilization",
+ "graph": "individual-graph",
+ "groups": "individual-groups",
+ "memory-by-key": "individual-memory-by-key",
+ "nprocessing": "individual-nprocessing",
+ "occupancy": "individual-occupancy",
+ "profile-server": "individual-profile-server",
+ "profile": "individual-profile",
+ "progress": "individual-progress",
+ "scheduler-system": "individual-scheduler-system",
+ "task-stream": "individual-task-stream",
+ "workers-cpu-timeseries": "individual-workers-cpu-timeseries",
+ "workers-disk-timeseries": "individual-workers-disk-timeseries",
+ "workers-disk": "individual-workers-disk",
+ "workers-memory-timeseries": "individual-workers-memory-timeseries",
+ "workers-memory": "individual-workers-memory",
+ "workers-network-timeseries": "individual-workers-network-timeseries",
+ "workers-network": "individual-workers-network",
+ "workers": "individual-workers",
+}
+
+VIEWER_PARAMETERS = ["url", "path"]
+
+def dask_dashboard_view(path="individual-cpu", url=DASK_DASHBOARD_ADDRESS):
+ url = url.replace("/status", "/") + path
+ return f""""""
+
+class DaskViewer(pn.viewable.Viewer):
+ url = param.String(DASK_DASHBOARD_ADDRESS, doc="The url of the Dask status dashboard")
+ path = param.Selector(default="individual-cpu", objects=VIEWS, doc="the endpoint", label="View")
+
+ def __init__(self, size=20, **params):
+ viewer_params = {k:v for k, v in params.items() if k in VIEWER_PARAMETERS}
+ layout_params = {k:v for k, v in params.items() if k not in VIEWER_PARAMETERS}
+
+ super().__init__(**viewer_params)
+
+ view = pn.bind(dask_dashboard_view, self.param.path, self.param.url)
+ self._iframe = pn.pane.HTML(view, sizing_mode="stretch_both")
+ self._select = pn.widgets.Select.from_param(self.param.path, size=size, width=300, sizing_mode="fixed", margin=(20,5,10,5))
+ self._link = pn.panel(f"""Dask Dashboard""", height=50, margin=(0,20))
+ self._panel = pn.Column(pn.Row(self._iframe, self._select, sizing_mode="stretch_both"), self._link, **layout_params)
+
+ def __panel__(self):
+ return self._panel
+
+if __name__.startswith("bokeh"):
+ pn.extension(sizing_mode="stretch_width")
+
+ DaskViewer(height=500, size=25).servable()
+```
+
+Try running `panel serve dashboard.py`. If your Dask cluster is working, you will see something like
+
+
+
+## Additional Resources
+
+- [Panel - Use Async Callbacks](../callbacks/async.md)
+- [Dask - Async/Await and Non-Blocking Execution Documentation](https://examples.dask.org/applications/async-await.html#Async/Await-and-Non-Blocking-Execution)
+- [Dask - Async Web Server](https://examples.dask.org/applications/async-web-server.html)
diff --git a/doc/how_to/concurrency/index.md b/doc/how_to/concurrency/index.md
index 03be0a31c6..7e965bb0ef 100644
--- a/doc/how_to/concurrency/index.md
+++ b/doc/how_to/concurrency/index.md
@@ -54,10 +54,33 @@ Discover how to manually set up a Thread to process an event queue.
:::
:::{grid-item-card} {octicon}`arrow-switch;2.5em;sd-mr-1 sd-animate-grow50` Use Asynchronous Processing
-:link: async
+:link: ../callbacks/async
:link-type: doc
-Discover how to make use of asynchronous callbacks to handle I/O bound operations concurrently.
+Discover how to make use of asynchronous callbacks to handle I/O and cpu bound operations concurrently.
+:::
+
+:::{grid-item-card} {octicon}`paper-airplane;2.5em;sd-mr-1 sd-animate-grow50` Sync to Async
+:link: sync_to_async
+:link-type: doc
+
+Discover how to run your sync callbacks asynchronously to handle I/O and cpu bound operations concurrently.
+:::
+
+::::
+
+## Scaling via an external compute engine
+
+You can also scale your application by offloading your compute heavy tasks to an external compute engine like [Dask](https://www.dask.org/). Please note that this may add additional overhead of several 100ms to your tasks.
+
+::::{grid} 1 2 2 2
+:gutter: 1 1 1 2
+
+:::{grid-item-card} {octicon}`versions;2.5em;sd-mr-1 sd-animate-grow50` Dask
+:link: dask
+:link-type: doc
+
+Discover how-to configure and use Dask to scale your Panel application
:::
::::
@@ -70,5 +93,8 @@ Discover how to make use of asynchronous callbacks to handle I/O bound operation
load_balancing
processes
threading
-async
+manual_threading
+../callbacks/async
+sync_to_async
+dask
```
diff --git a/doc/how_to/concurrency/sync_to_async.md b/doc/how_to/concurrency/sync_to_async.md
new file mode 100644
index 0000000000..cd80c3da17
--- /dev/null
+++ b/doc/how_to/concurrency/sync_to_async.md
@@ -0,0 +1,40 @@
+# Run synchronous functions asynchronously
+
+Running your bound, synchronous functions asynchronously can be an easy way to make your application responsive and scale to more users.
+
+## Asyncify
+
+This example will show how to make your app responsive by running a sync, cpu bound function asynchronously. We will be using [asyncer](https://asyncer.tiangolo.com) by Tiangolo. You can install the package via `pip install asyncer`.
+
+```python
+import numpy as np
+import pandas as pd
+
+from asyncer import asyncify
+import panel as pn
+
+widget = pn.widgets.IntSlider(value=5, start=0, end=10)
+
+def do_sync_work(it, n):
+ return sum(pd.DataFrame(np.random.rand(n,n)).sum().sum() for _ in range(it))
+
+async def create_result():
+ yield pn.indicators.LoadingSpinner(value=True, width=25, height=25)
+ result = await asyncify(do_sync_work)(it=5, n=10000)
+ yield f"Wow. That was slow.\n\nThe sum is **{result:.2f}**"
+
+pn.Column(widget.rx() + 1, create_result).servable()
+```
+
+
+
+Without [`asyncify`](https://asyncer.tiangolo.com/tutorial/asyncify/) the app would have been unresponsive for 5-10 seconds while loading.
+
+[`asyncify`](https://asyncer.tiangolo.com/tutorial/asyncify/) works well for IO bound functions as well as for CPU bound functions that releases the GIL.
+
+## Dask
+
+If you run many cpu bound functions you may consider offloading your functions asynchronously to an external compute engine like [Dask](https://www.dask.org/). See our [Dask how-to Guide](../performance/dask.md).
diff --git a/doc/how_to/deployment/index.md b/doc/how_to/deployment/index.md
index 536345e0ae..702d71ced1 100644
--- a/doc/how_to/deployment/index.md
+++ b/doc/how_to/deployment/index.md
@@ -42,6 +42,13 @@ For guides on running and configuring a Panel server see the [server how-to guid

:::
+:::{grid-item-card} Ploomber Cloud
+:link: ploomber
+:link-type: doc
+
+
+:::
+
::::
## Other Cloud Providers
@@ -58,4 +65,5 @@ binder
gcp
heroku
huggingface
+ploomber
```
diff --git a/doc/how_to/deployment/ploomber.md b/doc/how_to/deployment/ploomber.md
new file mode 100644
index 0000000000..61ec362aa4
--- /dev/null
+++ b/doc/how_to/deployment/ploomber.md
@@ -0,0 +1,49 @@
+# Ploomber Cloud
+
+Ploomber Cloud offers a [free deployment](https://platform.ploomber.io) option for Panel apps. Once you create an account and log in, follow these steps for deploying using the web application:
+
+1. Click on the "NEW" button. You'll find the below page:
+
+
+
+2. In the "Framework" section, click on Panel.
+3. In the "Source code" section, click on "Upload your files".
+4. Upload your `.zip` file with the `app.py` and `requirements.txt` file.
+5. Click on "CREATE"
+6. Wait until deployment finishes. To see your app, click on the `VIEW APPLICATION` button.
+
+
+Full instructions for deploying Panel apps are available [here](https://docs.cloud.ploomber.io/en/latest/apps/panel.html).
+
+You can also deploy the panel app using the Ploomber command line interface by following the below steps:
+
+1. First, install the package:
+
+```bash
+pip install ploomber-cloud
+```
+
+2. Get an [API Key](https://docs.cloud.ploomber.io/en/latest/quickstart/apikey.html) and set the key:
+
+```bash
+ploomber-cloud key YOURKEY
+```
+
+3. `cd` into the panel app folder that you want to deploy.
+4. Initialize your project:
+
+```bash
+ploomber-cloud init
+```
+
+5. This will prompt you for the project type. You need to enter `panel`.
+6. Once your project is configured it will generate a `ploomber-cloud.json` with the project info.
+7. Deploy the project by running:
+
+```bash
+ploomber-cloud deploy
+```
+
+8. To view your application, login to the web application and click on the `VIEW APPLICATION` button.
+
+Full instructions for deploying apps using the CLI are available [here](https://docs.cloud.ploomber.io/en/latest/user-guide/cli.html).
diff --git a/doc/how_to/links/jslinks.md b/doc/how_to/links/jslinks.md
index 15d2035f72..9b1837afe3 100644
--- a/doc/how_to/links/jslinks.md
+++ b/doc/how_to/links/jslinks.md
@@ -81,4 +81,4 @@ pn.Row(url, button)
## Related Resources
- See the [Explanation > APIs](../../explanation/api/index.md) for context on this and other Panel APIs
-- The [How to > Link Plot Parameters in Javascript](./links) guide addresses how to link Bokeh and HoloViews plot parameters in Javascript.
+- The [How to > Link Plot Parameters in Javascript](./link_plots.md) guide addresses how to link Bokeh and HoloViews plot parameters in Javascript.
diff --git a/doc/how_to/streamlit_migration/chat.md b/doc/how_to/streamlit_migration/chat.md
index a6e14baa95..4190c407aa 100644
--- a/doc/how_to/streamlit_migration/chat.md
+++ b/doc/how_to/streamlit_migration/chat.md
@@ -4,15 +4,15 @@ Both Streamlit and Panel provides special components to help you build conversat
| Streamlit | Panel | Description |
| -------------------- | ------------------- | -------------------------------------- |
-| [`chat_message`](https://docs.streamlit.io/library/api-reference/chat/st.chat_message) | [`ChatMessage`](../../../examples/reference/chat/ChatMessage.ipynb) | Display a chat message |
+| [`chat_message`](https://docs.streamlit.io/library/api-reference/chat/st.chat_message) | [`ChatMessage`](../../reference/chat/ChatMessage.md) | Display a chat message |
| [`chat_input`](https://docs.streamlit.io/library/api-reference/chat/st.chat_input) | [`ChatInput` example](https://holoviz-topics.github.io/panel-chat-examples/components/#chat_input) | Input a chat message |
| [`status`](https://docs.streamlit.io/library/api-reference/status/st.status) | [`Status` example](https://holoviz-topics.github.io/panel-chat-examples/components/#status) | Display the output of long-running tasks in a container |
-| | [`ChatFeed`](../../../examples/reference/chat/ChatFeed.ipynb) | Display multiple chat messages |
-| | [`ChatInterface`](../../../examples/reference/chat/ChatInterface.ipynb) | High-level, easy to use chat interface |
-| [`StreamlitCallbackHandler`](https://python.langchain.com/docs/integrations/callbacks/streamlit) | [`PanelCallbackHandler`](../../../examples/reference/chat/PanelCallbackHandler.ipynb) | Display the thoughts and actions of a [LangChain](https://python.langchain.com/docs/get_started/introduction) agent |
+| | [`ChatFeed`](../../reference/chat/ChatFeed.md) | Display multiple chat messages |
+| | [`ChatInterface`](../../reference/chat/ChatInterface.md) | High-level, easy to use chat interface |
+| [`StreamlitCallbackHandler`](https://python.langchain.com/docs/integrations/callbacks/streamlit) | [`PanelCallbackHandler`](../../reference/chat/PanelCallbackHandler.md) | Display the thoughts and actions of a [LangChain](https://python.langchain.com/docs/get_started/introduction) agent |
| [`StreamlitChatMessageHistory`](https://python.langchain.com/docs/integrations/memory/streamlit_chat_message_history) | | Persist the memory of a [LangChain](https://python.langchain.com/docs/get_started/introduction) agent |
-The starting point for most Panel users is the *high-level* [`ChatInterface`](../../../examples/reference/chat/ChatInterface.ipyn) or [`PanelCallbackHandler`](../../../examples/reference/chat/PanelCallbackHandler.ipynb), not the *low-level* [`ChatMessage`](../../../examples/reference/chat/ChatMessage.ipynb) and [`ChatFeed`](../../../examples/reference/chat/ChatFeed.ipynb) components.
+The starting point for most Panel users is the *high-level* [`ChatInterface`](../../reference/chat/ChatInterface.md) or [`PanelCallbackHandler`](../../reference/chat/PanelCallbackHandler.md), not the *low-level* [`ChatMessage`](../../reference/chat/ChatMessage.md) and [`ChatFeed`](../../reference/chat/ChatFeed.md) components.
## Chat Message
diff --git a/doc/how_to/streamlit_migration/panes.md b/doc/how_to/streamlit_migration/panes.md
index d680640869..f83a6c108c 100644
--- a/doc/how_to/streamlit_migration/panes.md
+++ b/doc/how_to/streamlit_migration/panes.md
@@ -5,7 +5,7 @@ In Panel the objects that can display your Python objects are called *panes*. Wi
- Get notifications about interactions like click events on your plots and tables and react to them.
- Use unique data visualization ecosystems like HoloViz, ipywidgets and VTK.
-Check out the [Panes Section](../../reference/index#panes) of the [Component Gallery](../../reference/index.md) for the full list of *panes*.
+Check out the [Panes Section](../../reference/index.md#panes) of the [Component Gallery](../../reference/index.md) for the full list of *panes*.
---
diff --git a/doc/how_to/styling/altair.md b/doc/how_to/styling/altair.md
index 3f67c6957f..54cfc56836 100644
--- a/doc/how_to/styling/altair.md
+++ b/doc/how_to/styling/altair.md
@@ -1,6 +1,6 @@
# Style Altair Plots
-This guide addresses how to style Altair plots displayed using the [Vega pane](../../../examples/reference/panes/Vega).
+This guide addresses how to style Altair plots displayed using the [Vega pane](../../reference/panes/Vega.md).
You can select the theme of Altair plots using [`altair.themes.enable`](https://altair-viz.github.io/user_guide/customization.html#changing-the-theme) and an accent color using the `configure_mark` method. The list of themes is available via `altair.themes.names()`.
diff --git a/examples/apps/django/requirements.txt b/examples/apps/django/requirements.txt
index 59fc2ed647..58724108f0 100644
--- a/examples/apps/django/requirements.txt
+++ b/examples/apps/django/requirements.txt
@@ -2,4 +2,4 @@ django==2.2.28
channels==2.2.0
panel==0.9.3
bokeh==2.0.2
-jinja2==3.0.1
+jinja2==3.1.3
diff --git a/examples/apps/django_multi_apps/requirements.txt b/examples/apps/django_multi_apps/requirements.txt
index dea92220fa..fd6f5c5a3d 100644
--- a/examples/apps/django_multi_apps/requirements.txt
+++ b/examples/apps/django_multi_apps/requirements.txt
@@ -16,7 +16,7 @@ hvplot==0.5.2
hyperlink==20.0.1
idna==2.8
incremental==21.3.0
-Jinja2==2.11.3
+Jinja2==3.1.3
Markdown==3.1.1
MarkupSafe==1.1.1
numpy==1.22.0
diff --git a/examples/reference/chat/ChatFeed.ipynb b/examples/reference/chat/ChatFeed.ipynb
index 3c03d464d6..0904ad03d1 100644
--- a/examples/reference/chat/ChatFeed.ipynb
+++ b/examples/reference/chat/ChatFeed.ipynb
@@ -8,7 +8,6 @@
},
"outputs": [],
"source": [
- "import time\n",
"import panel as pn\n",
"\n",
"pn.extension()"
@@ -44,7 +43,7 @@
"##### Styling\n",
"\n",
"* **`card_params`** (Dict): Parameters to pass to Card, such as `header`, `header_background`, `header_color`, etc.\n",
- "* **`message_params`** (Dict): Parameters to pass to each ChatMessage, such as `reaction_icons`, `timestamp_format`, `show_avatar`, `show_user`, and `show_timestamp`.\n",
+ "* **`message_params`** (Dict): Parameters to pass to each ChatMessage, such as `reaction_icons`, `timestamp_format`, `show_avatar`, `show_user`, and `show_timestamp` Params passed that are not ChatFeed params will be forwarded into `message_params`.\n",
"\n",
"##### Other\n",
"\n",
@@ -55,6 +54,7 @@
"* **`placeholder_threshold`** (float): Min duration in seconds of buffering before displaying the placeholder. If 0, the placeholder will be disabled. Defaults to 0.2.\n",
"* **`auto_scroll_limit`** (int): Max pixel distance from the latest object in the Column to activate automatic scrolling upon update. Setting to 0 disables auto-scrolling.\n",
"* **`scroll_button_threshold`** (int): Min pixel distance from the latest object in the Column to display the scroll button. Setting to 0 disables the scroll button.\n",
+ "* **`show_activity_dot`** (bool): Whether to show an activity dot on the ChatMessage while streaming the callback response.\n",
"* **`view_latest`** (bool): Whether to scroll to the latest object on init. If not enabled the view will be on the first object. Defaults to True.\n",
"\n",
"#### Methods\n",
@@ -604,6 +604,30 @@
")"
]
},
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "The messages can be filtered by using a custom `filter_by` function."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "def filter_by_reactions(messages):\n",
+ " return [message for message in messages if \"favorite\" in message.reactions]\n",
+ "\n",
+ "\n",
+ "chat_feed.send(\n",
+ " pn.chat.ChatMessage(\"I'm a message with a reaction!\", reactions=[\"favorite\"])\n",
+ ")\n",
+ "\n",
+ "chat_feed.serialize(filter_by=filter_by_reactions)"
+ ]
+ },
{
"cell_type": "markdown",
"metadata": {},
@@ -716,7 +740,7 @@
"cell_type": "markdown",
"metadata": {},
"source": [
- "You can pass `ChatEntry` params through `entry_params`."
+ "You can pass `ChatEntry` params through `message_params`."
]
},
{
@@ -734,6 +758,25 @@
"chat_feed"
]
},
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Alternatively, directly pass those params to the ChatFeed constructor and it'll be forwarded into `message_params` automatically."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "chat_feed = pn.chat.ChatFeed(default_avatars={\"System\": \"S\", \"User\": \"👤\"}, reaction_icons={\"like\": \"thumb-up\"})\n",
+ "chat_feed.send(user=\"System\", value=\"This is the System speaking.\")\n",
+ "chat_feed.send(user=\"User\", value=\"This is the User speaking.\")\n",
+ "chat_feed"
+ ]
+ },
{
"cell_type": "markdown",
"metadata": {},
diff --git a/examples/reference/chat/ChatMessage.ipynb b/examples/reference/chat/ChatMessage.ipynb
index 4f5b485e97..6fb9e99e73 100644
--- a/examples/reference/chat/ChatMessage.ipynb
+++ b/examples/reference/chat/ChatMessage.ipynb
@@ -59,6 +59,7 @@
"* **`show_timestamp`** (bool): Whether to display the timestamp of the message.\n",
"* **`show_reaction_icons`** (bool): Whether to display the reaction icons.\n",
"* **`show_copy_icon`** (bool): Whether to show the copy icon.\n",
+ "* **`show_activity_dot`** (bool): Whether to show the activity dot.\n",
"* **`name`** (str): The title or name of the chat message widget, if any.\n",
"\n",
"___"
diff --git a/examples/reference/templates/Material.ipynb b/examples/reference/templates/Material.ipynb
index 148bcbc5cd..81211d64d6 100644
--- a/examples/reference/templates/Material.ipynb
+++ b/examples/reference/templates/Material.ipynb
@@ -82,7 +82,7 @@
" )\n",
")\n",
"\n",
- "template.show();"
+ "template.servable();"
]
},
{
diff --git a/examples/reference/widgets/TextAreaInput.ipynb b/examples/reference/widgets/TextAreaInput.ipynb
index b9a990b568..7371245083 100644
--- a/examples/reference/widgets/TextAreaInput.ipynb
+++ b/examples/reference/widgets/TextAreaInput.ipynb
@@ -39,6 +39,7 @@
"* **`name`** (str): The title of the widget\n",
"* **`placeholder`** (str): A placeholder string displayed when no value is entered\n",
"* **`rows`** (int, default=2): The number of rows in the text input field. \n",
+ "* **`resizable`** (boolean | str): Whether the layout is interactively resizable, and if so in which dimensions: `width`, `height`, or `both`.\n",
"\n",
"___"
]
diff --git a/examples/reference/widgets/ToggleIcon.ipynb b/examples/reference/widgets/ToggleIcon.ipynb
index 839c5f908a..b8ee06a06d 100644
--- a/examples/reference/widgets/ToggleIcon.ipynb
+++ b/examples/reference/widgets/ToggleIcon.ipynb
@@ -25,7 +25,7 @@
"##### Core\n",
"\n",
"* **`active_icon`** (str): The name of the icon to display when toggled from [tabler-icons.io](https://tabler-icons.io)/\n",
- "* **`icon`** (str): The name of the icon to display from [tabler-icons.io](https://tabler-icons.io)/\n",
+ "* **`icon`** (str): The name of the icon to display from [tabler-icons.io](https://tabler-icons.io)/ or an SVG.\n",
"* **`value`** (boolean): Whether the icon is toggled on or off\n",
"\n",
"##### Display\n",
@@ -113,6 +113,29 @@
"pn.widgets.ToggleIcon(icon=\"thumb-down\", active_icon=\"thumb-up\", size='3em')"
]
},
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "You can also use SVGs for icons."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "SVG = \"\"\"\n",
+ "\n",
+ "\"\"\"\n",
+ "ACTIVE_SVG = \"\"\"\n",
+ "\n",
+ "\"\"\"\n",
+ "\n",
+ "pn.widgets.ToggleIcon(icon=SVG, active_icon=ACTIVE_SVG, size='3em')"
+ ]
+ },
{
"cell_type": "markdown",
"metadata": {},
diff --git a/lite/files/Getting_Started.ipynb b/lite/files/Getting_Started.ipynb
index 65b644444b..004e861abd 100644
--- a/lite/files/Getting_Started.ipynb
+++ b/lite/files/Getting_Started.ipynb
@@ -87,7 +87,7 @@
").properties(\n",
" width=250,\n",
" height=250\n",
- ").add_selection(\n",
+ ").add_params(\n",
" brush\n",
")\n",
"\n",
diff --git a/panel/chat/feed.py b/panel/chat/feed.py
index 2f048d01e4..0cfff58d09 100644
--- a/panel/chat/feed.py
+++ b/panel/chat/feed.py
@@ -163,7 +163,8 @@ class ChatFeed(ListPanel):
message_params = param.Dict(default={}, doc="""
Params to pass to each ChatMessage, like `reaction_icons`, `timestamp_format`,
- `show_avatar`, `show_user`, and `show_timestamp`.""")
+ `show_avatar`, `show_user`, and `show_timestamp`. Params passed
+ that are not ChatFeed params will be forwarded into `message_params`.""")
header = param.Parameter(doc="""
The header of the chat feed; commonly used for the title.
@@ -197,6 +198,10 @@ class ChatFeed(ListPanel):
display the scroll button. Setting to 0
disables the scroll button.""")
+ show_activity_dot = param.Boolean(default=True, doc="""
+ Whether to show an activity dot on the ChatMessage while
+ streaming the callback response.""")
+
view_latest = param.Boolean(default=True, doc="""
Whether to scroll to the latest object on init. If not
enabled the view will be on the first object.""")
@@ -205,9 +210,6 @@ class ChatFeed(ListPanel):
The placeholder wrapped in a ChatMessage object;
primarily to prevent recursion error in _update_placeholder.""")
- _callback_future = param.ClassSelector(class_=asyncio.Future, allow_None=True, doc="""
- The current, cancellable async task being executed.""")
-
_callback_state = param.ObjectSelector(objects=list(CallbackState), doc="""
The current state of the callback.""")
@@ -217,10 +219,20 @@ class ChatFeed(ListPanel):
_stylesheets: ClassVar[List[str]] = [f"{CDN_DIST}css/chat_feed.css"]
def __init__(self, *objects, **params):
+ self._callback_future = None
+
if params.get("renderers") and not isinstance(params["renderers"], list):
params["renderers"] = [params["renderers"]]
if params.get("width") is None and params.get("sizing_mode") is None:
params["sizing_mode"] = "stretch_width"
+
+ # forward message params to ChatMessage for convenience
+ message_params = params.get("message_params", {})
+ for param_key in list(params.keys()):
+ if param_key not in ChatFeed.param and param_key in ChatMessage.param:
+ message_params[param_key] = params.pop(param_key)
+ params["message_params"] = message_params
+
super().__init__(*objects, **params)
# instantiate the card's column) is not None)
@@ -241,10 +253,8 @@ def __init__(self, *objects, **params):
stylesheets=self._stylesheets,
**linked_params
)
- self.link(self._chat_log, objects='objects', bidirectional=True)
- # we have a card for the title
- self._card = Card(
- self._chat_log, VSpacer(),
+ card_params = linked_params.copy()
+ card_params.update(
margin=self.param.margin,
align=self.param.align,
header=self.header,
@@ -258,7 +268,14 @@ def __init__(self, *objects, **params):
title_css_classes=["chat-feed-title"],
styles={"padding": "0px"},
stylesheets=self._stylesheets + self.param.stylesheets.rx(),
- **linked_params
+ )
+ card_params.update(self.card_params)
+ self.link(self._chat_log, objects='objects', bidirectional=True)
+ # we have a card for the title
+ self._card = Card(
+ self._chat_log,
+ VSpacer(),
+ **card_params
)
# handle async callbacks using this trick
@@ -269,14 +286,25 @@ def _get_model(
self, doc: Document, root: Model | None = None,
parent: Model | None = None, comm: Comm | None = None
) -> Model:
- return self._card._get_model(doc, root, parent, comm)
+ model = self._card._get_model(doc, root, parent, comm)
+ ref = (root or model).ref['id']
+ self._models[ref] = (model, parent)
+ return model
def _cleanup(self, root: Model | None = None) -> None:
self._card._cleanup(root)
super()._cleanup(root)
+ @param.depends("card_params", watch=True)
+ def _update_card_params(self):
+ self._card.param.update(**self.card_params)
+
@param.depends("placeholder_text", watch=True, on_init=True)
def _update_placeholder(self):
+ if self._placeholder is not None:
+ self._placeholder.param.update(object=self.placeholder_text)
+ return
+
loading_avatar = SVG(
PLACEHOLDER_SVG, sizing_mode=None, css_classes=["rotating-placeholder"]
)
@@ -335,6 +363,7 @@ def _build_message(
message_params["avatar"] = avatar
if self.width:
message_params["width"] = int(self.width - 80)
+
message = ChatMessage(**message_params)
return message
@@ -402,18 +431,24 @@ async def _serialize_response(self, response: Any) -> ChatMessage | None:
updating the message's value.
"""
response_message = None
- if isasyncgen(response):
- self._callback_state = CallbackState.GENERATING
- async for token in response:
- response_message = self._upsert_message(token, response_message)
- elif isgenerator(response):
- self._callback_state = CallbackState.GENERATING
- for token in response:
- response_message = self._upsert_message(token, response_message)
- elif isawaitable(response):
- response_message = self._upsert_message(await response, response_message)
- else:
- response_message = self._upsert_message(response, response_message)
+ try:
+ if isasyncgen(response):
+ self._callback_state = CallbackState.GENERATING
+ async for token in response:
+ response_message = self._upsert_message(token, response_message)
+ response_message.show_activity_dot = self.show_activity_dot
+ elif isgenerator(response):
+ self._callback_state = CallbackState.GENERATING
+ for token in response:
+ response_message = self._upsert_message(token, response_message)
+ response_message.show_activity_dot = self.show_activity_dot
+ elif isawaitable(response):
+ response_message = self._upsert_message(await response, response_message)
+ else:
+ response_message = self._upsert_message(response, response_message)
+ finally:
+ if response_message:
+ response_message.show_activity_dot = False
return response_message
async def _schedule_placeholder(
@@ -429,7 +464,7 @@ async def _schedule_placeholder(
return
start = asyncio.get_event_loop().time()
- while not task.done() and num_entries == len(self._chat_log):
+ while not self._callback_state == CallbackState.IDLE and num_entries == len(self._chat_log):
duration = asyncio.get_event_loop().time() - start
if duration > self.placeholder_threshold:
self.append(self._placeholder)
@@ -659,6 +694,7 @@ def clear(self) -> List[Any]:
def _serialize_for_transformers(
self,
+ messages: List[ChatMessage],
role_names: Dict[str, str | List[str]] | None = None,
default_role: str | None = "assistant",
custom_serializer: Callable = None
@@ -681,8 +717,8 @@ def _serialize_for_transformers(
for name in names:
names_role[name.lower()] = role
- messages = []
- for message in self._chat_log.objects:
+ serialized_messages = []
+ for message in messages:
lowercase_name = message.user.lower()
if lowercase_name not in names_role and not default_role:
@@ -703,11 +739,12 @@ def _serialize_for_transformers(
else:
content = str(message)
- messages.append({"role": role, "content": content})
- return messages
+ serialized_messages.append({"role": role, "content": content})
+ return serialized_messages
def serialize(
self,
+ filter_by: Callable | None = None,
format: Literal["transformers"] = "transformers",
custom_serializer: Callable | None = None,
**serialize_kwargs
@@ -720,10 +757,13 @@ def serialize(
format : str
The format to export the chat log as; currently only
supports "transformers".
+ filter_by : callable
+ A function to filter the chat log by.
+ The function must accept and return a list of ChatMessage objects.
custom_serializer : callable
A custom function to format the ChatMessage's object. The function must
- accept one positional argument. If not provided,
- uses the serialize method on ChatMessage.
+ accept one positional argument, the ChatMessage object, and return a string.
+ If not provided, uses the serialize method on ChatMessage.
**serialize_kwargs
Additional keyword arguments to use for the specified format.
@@ -742,9 +782,13 @@ def serialize(
-------
The chat log serialized in the specified format.
"""
+ messages = self._chat_log.objects.copy()
+ if filter_by is not None:
+ messages = filter_by(messages)
+
if format == "transformers":
return self._serialize_for_transformers(
- custom_serializer=custom_serializer, **serialize_kwargs
+ messages, custom_serializer=custom_serializer, **serialize_kwargs
)
raise NotImplementedError(f"Format {format!r} is not supported.")
diff --git a/panel/chat/interface.py b/panel/chat/interface.py
index 3ef6e68e77..8642dc4e25 100644
--- a/panel/chat/interface.py
+++ b/panel/chat/interface.py
@@ -23,7 +23,7 @@
from ..widgets.button import Button
from ..widgets.input import FileInput, TextInput
from .feed import CallbackState, ChatFeed
-from .message import _FileInputMessage
+from .message import ChatMessage, _FileInputMessage
@dataclass
@@ -292,8 +292,8 @@ def _init_widgets(self):
sizing_mode="stretch_width",
max_width=show_expr.rx.where(90, 45),
max_height=50,
- margin=(5, 5, 5, 0),
- align="start",
+ margin=(0, 5, 0, 0),
+ align="center",
visible=visible
)
if action != "stop":
@@ -543,6 +543,7 @@ def active(self, index: int) -> None:
def _serialize_for_transformers(
self,
+ messages: List[ChatMessage],
role_names: Dict[str, str | List[str]] | None = None,
default_role: str | None = "assistant",
custom_serializer: Callable = None
@@ -552,6 +553,8 @@ def _serialize_for_transformers(
Arguments
---------
+ messages : list(ChatMessage)
+ A list of ChatMessage objects to serialize.
role_names : dict(str, str | list(str)) | None
A dictionary mapping the role to the ChatMessage's user name.
Defaults to `{"user": [self.user], "assistant": [self.callback_user]}`
@@ -563,8 +566,8 @@ def _serialize_for_transformers(
If this is set to None, raises a ValueError if the user name is not found.
custom_serializer : callable
A custom function to format the ChatMessage's object. The function must
- accept one positional argument and return a string. If not provided,
- uses the serialize method on ChatMessage.
+ accept one positional argument, the ChatMessage object, and return a string.
+ If not provided, uses the serialize method on ChatMessage.
Returns
-------
@@ -575,7 +578,7 @@ def _serialize_for_transformers(
"user": [self.user],
"assistant": [self.callback_user],
}
- return super()._serialize_for_transformers(role_names, default_role, custom_serializer)
+ return super()._serialize_for_transformers(messages, role_names, default_role, custom_serializer)
@param.depends("_callback_state", watch=True)
async def _update_input_disabled(self):
diff --git a/panel/chat/message.py b/panel/chat/message.py
index 9996c5a5b9..2b4f7c02ce 100644
--- a/panel/chat/message.py
+++ b/panel/chat/message.py
@@ -197,6 +197,9 @@ class ChatMessage(PaneBase):
show_copy_icon = param.Boolean(default=True, doc="""
Whether to display the copy icon.""")
+ show_activity_dot = param.Boolean(default=False, doc="""
+ Whether to show the activity dot.""")
+
renderers = param.HookList(doc="""
A callable or list of callables that accept the object and return a
Panel object to render the object. If a list is provided, will
@@ -222,7 +225,7 @@ def __init__(self, object=None, **params):
tz = params.get("timestamp_tz")
if tz is not None:
tz = ZoneInfo(tz)
- elif state.browser_info.timezone:
+ elif state.browser_info and state.browser_info.timezone:
tz = ZoneInfo(state.browser_info.timezone)
params["timestamp"] = datetime.datetime.now(tz=tz)
reaction_icons = params.get("reaction_icons", {"favorite": "heart"})
@@ -240,6 +243,9 @@ def __init__(self, object=None, **params):
self._build_layout()
def _build_layout(self):
+ self._activity_dot = HTML(
+ "●", css_classes=["activity-dot"], visible=self.param.show_activity_dot
+ )
self._left_col = left_col = Column(
self._render_avatar(),
max_width=60,
@@ -275,8 +281,10 @@ def _build_layout(self):
Row(
self._user_html,
self.chat_copy_icon,
+ self._activity_dot,
stylesheets=self._stylesheets,
sizing_mode="stretch_width",
+ css_classes=["header"]
),
self._center_row,
self._timestamp_html,
diff --git a/panel/command/serve.py b/panel/command/serve.py
index bcff57c5cc..de857a3c87 100644
--- a/panel/command/serve.py
+++ b/panel/command/serve.py
@@ -579,7 +579,7 @@ def customize_kwargs(self, args, server_kwargs):
raise ValueError("OAuth encryption key was not a valid base64 "
"string. Generate an encryption key with "
"`panel oauth-secret` and ensure you did not "
- "truncate the returned string.")
+ "truncate the returned string.") from None
if len(key) != 32:
raise ValueError(
"OAuth encryption key must be 32 url-safe "
@@ -605,7 +605,7 @@ def customize_kwargs(self, args, server_kwargs):
"Using OAuth2 provider with Panel requires the "
"cryptography library. Install it with `pip install "
"cryptography` or `conda install cryptography`."
- )
+ ) from None
state.encryption = Fernet(config.oauth_encryption_key)
kwargs['auth_provider'] = OAuthProvider(
diff --git a/panel/compiler.py b/panel/compiler.py
index b3cae33e17..0627047042 100644
--- a/panel/compiler.py
+++ b/panel/compiler.py
@@ -47,7 +47,7 @@ def write_bundled_files(name, files, explicit_dir=None, ext=None):
except Exception as e:
raise ConnectionError(
f"Failed to fetch {name} dependency: {bundle_file}. Errored with {e}."
- )
+ ) from e
try:
map_file = f'{bundle_file}.map'
map_response = requests.get(map_file)
diff --git a/panel/config.py b/panel/config.py
index b4b26cea0d..a1909cbf3e 100644
--- a/panel/config.py
+++ b/panel/config.py
@@ -798,7 +798,7 @@ def __call__(self, *args, **params):
backend = hv.Store.current_backend
else:
backend = 'bokeh'
- if hasattr(hv.Store, 'set_current_backend'):
+ if not loaded or (loaded and backend != hv.Store.current_backend) and hasattr(hv.Store, 'set_current_backend'):
hv.Store.set_current_backend(backend)
else:
hv.Store.current_backend = backend
@@ -911,9 +911,9 @@ def _apply_signatures(self):
parameters = sig_params[:-1]
processed_kws, keyword_groups = set(), []
- for cls in reversed(cls.mro()):
+ for scls in reversed(cls.mro()):
keyword_group = []
- for (k, v) in sorted(cls.__dict__.items()):
+ for (k, v) in sorted(scls.__dict__.items()):
if (isinstance(v, param.Parameter) and k not in processed_kws
and not v.readonly):
keyword_group.append(k)
diff --git a/panel/dist/bundled/datatabulator/tabulator-tables@5.5.0/dist/css/tabulator_fast.min.css b/panel/dist/bundled/datatabulator/tabulator-tables@5.5.0/dist/css/tabulator_fast.min.css
index 61663d9611..7287073ae0 100644
--- a/panel/dist/bundled/datatabulator/tabulator-tables@5.5.0/dist/css/tabulator_fast.min.css
+++ b/panel/dist/bundled/datatabulator/tabulator-tables@5.5.0/dist/css/tabulator_fast.min.css
@@ -566,6 +566,12 @@
background-color: var(--accent-fill-hover);
cursor: pointer;
}
+.tabulator-row.tabulator-selected a {
+ color: var(--foreground-on-accent-rest);
+}
+.tabulator-row.tabulator-selectable:hover a {
+ color: var(--foreground-on-accent-rest);
+}
.tabulator-row.tabulator-row-moving {
border: 1px solid #000;
background: #fff;
diff --git a/panel/dist/css/button.css b/panel/dist/css/button.css
index d2c902a9c5..5ae415e813 100644
--- a/panel/dist/css/button.css
+++ b/panel/dist/css/button.css
@@ -139,7 +139,7 @@
.bk-btn a {
align-items: center;
- display: flex;
+ display: inline;
height: 100%;
justify-content: center;
padding: 6px;
diff --git a/panel/dist/css/chat_message.css b/panel/dist/css/chat_message.css
index 692dcc4cfa..fe39d2ea1c 100644
--- a/panel/dist/css/chat_message.css
+++ b/panel/dist/css/chat_message.css
@@ -47,6 +47,10 @@
max-width: calc(100% - 80px);
}
+.header {
+ width: fit-content;
+}
+
.name {
font-size: 1em;
margin-bottom: 0px;
@@ -67,15 +71,19 @@
color-mix(in srgb, var(--panel-shadow-color) 15%, transparent) 0px 1px 3px
1px;
font-size: 1.25em;
- min-width: 50px;
min-height: 50px;
margin-block: 0px;
margin-left: 10px; /* Space for avatar */
margin-right: 5px; /* Space for reaction */
background-color: var(--panel-surface-color, #f1f1f1);
+ min-width: 0;
max-width: calc(100% - 40px);
padding: 5px;
width: fit-content;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ overflow-wrap: anywhere;
}
.footer {
@@ -88,7 +96,7 @@
.timestamp {
color: #a9a9a9;
display: flex;
- margin-top: 0px;
+ margin-top: 3px;
}
.markdown {
@@ -108,3 +116,23 @@
margin-block: 0px;
margin-inline: 2px;
}
+
+@keyframes fadeOut {
+ 0% {
+ opacity: 1;
+ }
+ 50% {
+ opacity: 1;
+ }
+ 100% {
+ opacity: 0;
+ }
+}
+
+.activity-dot {
+ display: inline-block;
+ animation: fadeOut 2s infinite cubic-bezier(0.68, -0.55, 0.27, 1.55);
+ color: #32cd32;
+ font-size: 1.25em;
+ margin-block: 0px;
+}
diff --git a/panel/dist/css/loading.css b/panel/dist/css/loading.css
index 80294a3bf7..6dbd45299e 100644
--- a/panel/dist/css/loading.css
+++ b/panel/dist/css/loading.css
@@ -21,6 +21,8 @@
:host(.pn-loading) .pn-loading-msg,
.pn-loading .pn-loading-msg {
+ color: var(--panel-on-background-color, black);
+ font-size: 2em;
position: absolute;
top: 72%;
font-size: 2em;
diff --git a/panel/dist/css/widgetbox.css b/panel/dist/css/widgetbox.css
index 56ec2f00fe..a20fcf02e4 100644
--- a/panel/dist/css/widgetbox.css
+++ b/panel/dist/css/widgetbox.css
@@ -4,6 +4,4 @@
border-radius: 4px;
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05);
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05);
- overflow-x: hidden;
- overflow-y: hidden;
}
diff --git a/panel/io/cache.py b/panel/io/cache.py
index 6927c7e042..5a3016dbeb 100644
--- a/panel/io/cache.py
+++ b/panel/io/cache.py
@@ -192,14 +192,14 @@ def _generate_hash_inner(obj):
raise ValueError(
f'User hash function {hash_func!r} failed for input '
f'{obj!r} with following error: {type(e).__name__}("{e}").'
- )
+ ) from e
return output
if hasattr(obj, '__reduce__'):
h = hashlib.new("md5")
try:
reduce_data = obj.__reduce__()
except BaseException:
- raise ValueError(f'Could not hash object of type {type(obj).__name__}')
+ raise ValueError(f'Could not hash object of type {type(obj).__name__}') from None
for item in reduce_data:
h.update(_generate_hash(item))
return h.digest()
@@ -319,6 +319,9 @@ def cache(
A dictionary mapping from a type to a function which returns
a hash for an object of that type. If provided this will
override the default hashing function provided by Panel.
+ max_items: int or None
+ The maximum items to keep in the cache. Default is None, which does
+ not limit number of items stored in the cache.
policy: str
A caching policy when max_items is set, must be one of:
- FIFO: First in - First out
@@ -347,7 +350,8 @@ def cache(
max_items=max_items,
ttl=ttl,
to_disk=to_disk,
- cache_path=cache_path
+ cache_path=cache_path,
+ per_session=per_session,
)
func_hash = None # noqa
@@ -431,7 +435,7 @@ def wrapped_func(*args, **kwargs):
func_cache[hash_value] = (ret, time, 0, time)
return ret
- def clear(session_context=None):
+ def clear():
global func_hash
# clear called before anything is cached.
if 'func_hash' not in globals():
@@ -445,10 +449,13 @@ def clear(session_context=None):
else:
cache = state._memoize_cache.get(func_hash, {})
cache.clear()
+
wrapped_func.clear = clear
if per_session and state.curdoc and state.curdoc.session_context:
- state.curdoc.on_session_destroyed(clear)
+ def server_clear(session_context):
+ clear()
+ state.curdoc.on_session_destroyed(server_clear)
try:
wrapped_func.__dict__.update(func.__dict__)
diff --git a/panel/io/convert.py b/panel/io/convert.py
index bf82a4f93d..c3e9fb4815 100644
--- a/panel/io/convert.py
+++ b/panel/io/convert.py
@@ -248,7 +248,7 @@ def script_to_html(
except Exception as e:
raise ValueError(
f'Requirements parser raised following error: {e}'
- )
+ ) from e
# Environment
if panel_version == 'local':
diff --git a/panel/io/jupyter_server_extension.py b/panel/io/jupyter_server_extension.py
index 9bc5d349b0..8ea0a26383 100644
--- a/panel/io/jupyter_server_extension.py
+++ b/panel/io/jupyter_server_extension.py
@@ -169,9 +169,9 @@ async def _get_info(self, msg_id, timeout=KERNEL_TIMEOUT):
raise TimeoutError('Timed out while waiting for kernel to open Comm channel to Panel application.')
try:
msg = await ensure_async(self.kernel.iopub_channel.get_msg(timeout=None))
- except Empty:
+ except Empty as e:
if not await ensure_async(self.kernel.is_alive()):
- raise RuntimeError("Kernel died before establishing Comm connection to Panel application.")
+ raise RuntimeError("Kernel died before establishing Comm connection to Panel application.") from e
continue
if msg['parent_header'].get('msg_id') != msg_id:
continue
@@ -381,9 +381,9 @@ async def _check_for_message(self):
break
try:
msg = await ensure_async(self.kernel.iopub_channel.get_msg(timeout=None))
- except Empty:
+ except Empty as e:
if not await ensure_async(self.kernel.is_alive()):
- raise RuntimeError("Kernel died before expected shutdown of Panel app.")
+ raise RuntimeError("Kernel died before expected shutdown of Panel app.") from e
continue
msg_type = msg['header']['msg_type']
diff --git a/panel/io/liveness.py b/panel/io/liveness.py
index 3f65b270a9..e0975db260 100644
--- a/panel/io/liveness.py
+++ b/panel/io/liveness.py
@@ -25,4 +25,4 @@ async def get(self):
except Exception as e:
raise web.HTTPError(
500, f"Endpoint {endpoint!r} could not be served. Application raised error: {e}"
- )
+ ) from e
diff --git a/panel/io/model.py b/panel/io/model.py
index 44e6606d35..5fbb5d1757 100644
--- a/panel/io/model.py
+++ b/panel/io/model.py
@@ -160,8 +160,8 @@ def bokeh_repr(obj: Model, depth: int = 0, ignored: Optional[Iterable[str]] = No
props_repr = ', '.join(props)
if isinstance(obj, FlexBox):
r += '{cls}(children=[\n'.format(cls=cls)
- for obj in obj.children: # type: ignore
- r += textwrap.indent(bokeh_repr(obj, depth=depth+1) + ',\n', ' ')
+ for child_obj in obj.children: # type: ignore
+ r += textwrap.indent(bokeh_repr(child_obj, depth=depth+1) + ',\n', ' ')
r += '], %s)' % props_repr
else:
r += '{cls}({props})'.format(cls=cls, props=props_repr)
diff --git a/panel/io/notebook.py b/panel/io/notebook.py
index c09ec4ce88..61c12766c3 100644
--- a/panel/io/notebook.py
+++ b/panel/io/notebook.py
@@ -8,9 +8,11 @@
import os
import sys
import uuid
+import warnings
from collections import OrderedDict
from contextlib import contextmanager
+from functools import partial
from typing import (
TYPE_CHECKING, Any, Dict, Iterator, List, Literal, Optional, Tuple,
)
@@ -63,13 +65,31 @@
def _jupyter_server_extension_paths() -> List[Dict[str, str]]:
return [{"module": "panel.io.jupyter_server_extension"}]
-def push(doc: 'Document', comm: 'Comm', binary: bool = True) -> None:
+def push(doc: Document, comm: Comm, binary: bool = True, msg: any = None) -> None:
"""
Pushes events stored on the document across the provided comm.
"""
- msg = diff(doc, binary=binary)
+ if msg is None:
+ msg = diff(doc, binary=binary)
if msg is None:
return
+ elif not comm._comm:
+ try:
+ from tornado.ioloop import IOLoop
+ IOLoop.current().call_later(0.1, partial(push, doc, comm, binary, msg=msg))
+ except Exception:
+ warnings.warn(
+ 'Attempted to send message over Jupyter Comm but it was not '
+ 'yet open and also could not be rescheduled to a later time. '
+ 'The update will not be sent.', UserWarning, stacklevel=0
+ )
+ else:
+ send(comm, msg)
+
+def send(comm: Comm, msg: any):
+ """
+ Sends a bokeh message across a pyviz_comms.Comm.
+ """
# WARNING: CommManager model assumes that either JSON content OR a buffer
# is sent. Therefore we must NEVER(!!!) send both at once.
comm.send(msg.header_json)
diff --git a/panel/io/rest.py b/panel/io/rest.py
index 0a76532ed7..0bd052315c 100644
--- a/panel/io/rest.py
+++ b/panel/io/rest.py
@@ -152,7 +152,7 @@ def param_rest_provider(files, endpoint):
try:
import nbconvert # noqa
except ImportError:
- raise ImportError("Please install nbconvert to serve Jupyter Notebooks.")
+ raise ImportError("Please install nbconvert to serve Jupyter Notebooks.") from None
from nbconvert import ScriptExporter
exporter = ScriptExporter()
source, _ = exporter.from_filename(filename)
diff --git a/panel/io/server.py b/panel/io/server.py
index 7c947f0dcf..dc9f01b9a5 100644
--- a/panel/io/server.py
+++ b/panel/io/server.py
@@ -704,11 +704,11 @@ def parse_url_path(self, path: str) -> str:
try:
module = importlib.import_module(mod)
except ModuleNotFoundError:
- raise HTTPError(404, 'Module not found')
+ raise HTTPError(404, 'Module not found') from None
try:
component = getattr(module, cls)
except AttributeError:
- raise HTTPError(404, 'Component not found')
+ raise HTTPError(404, 'Component not found') from None
# May only access resources listed in specific attributes
if rtype not in self._resource_attrs:
@@ -717,7 +717,7 @@ def parse_url_path(self, path: str) -> str:
try:
resources = getattr(component, rtype)
except AttributeError:
- raise HTTPError(404, 'Resource type not found')
+ raise HTTPError(404, 'Resource type not found') from None
# Handle template resources
if rtype == '_resources':
@@ -1174,7 +1174,7 @@ def get_server(
raise KeyError(
"Keys of the title dictionary and of the apps "
f"dictionary must match. No {slug} key found in the "
- "title dictionary.")
+ "title dictionary.") from None
else:
title_ = title
slug = slug if slug.startswith('/') else '/'+slug
diff --git a/panel/io/state.py b/panel/io/state.py
index 09b60a8e42..6336b10751 100644
--- a/panel/io/state.py
+++ b/panel/io/state.py
@@ -215,7 +215,7 @@ class _state(param.Parameterized):
def __repr__(self) -> str:
server_info = []
- for server, panel, docs in self._servers.values():
+ for server, panel, _docs in self._servers.values():
server_info.append(
"{}:{:d} - {!r}".format(server.address or "localhost", server.port, panel)
)
@@ -967,10 +967,11 @@ def curdoc(self) -> Document | None:
pyodide_session = self._is_pyodide and 'pyodide_kernel' not in sys.modules
if doc and (doc.session_context or pyodide_session):
return doc
- finally:
- curdoc = self._curdoc.get()
- if curdoc:
- return curdoc
+ except Exception:
+ pass
+ curdoc = self._curdoc.get()
+ if curdoc:
+ return curdoc
@curdoc.setter
def curdoc(self, doc: Document) -> None:
diff --git a/panel/layout/grid.py b/panel/layout/grid.py
index 728fbaf420..f8c051cce0 100644
--- a/panel/layout/grid.py
+++ b/panel/layout/grid.py
@@ -405,7 +405,7 @@ def _yoffset(self):
@property
def _object_grid(self):
grid = np.full((self.nrows, self.ncols), None, dtype=object)
- for i, ((y0, x0, y1, x1), obj) in enumerate(self.objects.items()):
+ for (y0, x0, y1, x1), obj in self.objects.items():
l = 0 if x0 is None else x0
r = self.ncols if x1 is None else x1
t = 0 if y0 is None else y0
diff --git a/panel/layout/gridstack.py b/panel/layout/gridstack.py
index a35d3606a2..bfb9a62e74 100644
--- a/panel/layout/gridstack.py
+++ b/panel/layout/gridstack.py
@@ -183,7 +183,7 @@ def _update_sizing(self):
else:
height = 0
- for i, ((y0, x0, y1, x1), obj) in enumerate(self.objects.items()):
+ for (y0, x0, y1, x1), obj in self.objects.items():
x0 = 0 if x0 is None else x0
x1 = (self.ncols) if x1 is None else x1
y0 = 0 if y0 is None else y0
diff --git a/panel/links.py b/panel/links.py
index fedf3cb49a..faf359e1aa 100644
--- a/panel/links.py
+++ b/panel/links.py
@@ -578,10 +578,10 @@ def _get_triggers(
def _get_specs(
self, link: 'Link', source: 'Reactive', target: 'JSLinkTarget'
) -> Sequence[Tuple['SourceModelSpec', 'TargetModelSpec', str | None]]:
- for src_spec, code in link.code.items():
- src_specs = src_spec.split('.')
- if src_spec.startswith('event:'):
- src_spec = (None, src_spec)
+ for spec in link.code:
+ src_specs = spec.split('.')
+ if spec.startswith('event:'):
+ src_spec = (None, spec)
elif len(src_specs) > 1:
src_spec = ('.'.join(src_specs[:-1]), src_specs[-1])
else:
@@ -589,7 +589,7 @@ def _get_specs(
if isinstance(source, Reactive):
src_prop = source._rename.get(src_prop, src_prop)
src_spec = (None, src_prop)
- return [(src_spec, (None, None), code)]
+ return [(src_spec, (None, None), link.code[spec])]
diff --git a/panel/models/card.ts b/panel/models/card.ts
index 7a546041c7..8c16f5b7c4 100644
--- a/panel/models/card.ts
+++ b/panel/models/card.ts
@@ -12,8 +12,7 @@ export class CardView extends ColumnView {
connect_signals(): void {
super.connect_signals()
- const {active_header_background, children, collapsed, header_background, header_color, hide_header} = this.model.properties
- this.on_change(children, () => this.render())
+ const {active_header_background, collapsed, header_background, header_color, hide_header} = this.model.properties
this.on_change(collapsed, () => this._collapse())
this.on_change([header_color, hide_header], () => this.render())
diff --git a/panel/models/file_download.ts b/panel/models/file_download.ts
index 486a19b529..bdb21f161a 100644
--- a/panel/models/file_download.ts
+++ b/panel/models/file_download.ts
@@ -90,8 +90,7 @@ export class FileDownloadView extends InputWidgetView {
// 3. auto=True: The widget is a button, i.e right click to "Save as..." won't work
this.anchor_el = document.createElement('a')
this.button_el = button({
- disabled: this.model.disabled,
- type: "bk_btn, bk_btn_type",
+ disabled: this.model.disabled
})
if (this.icon_view != null) {
const separator = this.model.label != "" ? nbsp() : text("")
diff --git a/panel/models/icon.py b/panel/models/icon.py
index 5878a36348..059af48a0e 100644
--- a/panel/models/icon.py
+++ b/panel/models/icon.py
@@ -12,7 +12,7 @@ class ToggleIcon(Widget):
The name of the icon to display when toggled.""")
icon = String(default="heart", help="""
- The name of the icon to display.""")
+ The name of the icon or SVG to display.""")
size = String(default="1em", help="""
The size of the icon as a valid CSS font-size.""")
diff --git a/panel/models/icon.ts b/panel/models/icon.ts
index 1b9cf847f2..911c6c80b2 100644
--- a/panel/models/icon.ts
+++ b/panel/models/icon.ts
@@ -1,13 +1,14 @@
import { TablerIcon, TablerIconView } from "@bokehjs/models/ui/icons/tabler_icon";
+import { SVGIcon, SVGIconView } from "@bokehjs/models/ui/icons/svg_icon";
import { Control, ControlView } from '@bokehjs/models/widgets/control';
import type { IterViews } from '@bokehjs/core/build_views';
import * as p from "@bokehjs/core/properties";
import { build_view } from '@bokehjs/core/build_views';
-
export class ToggleIconView extends ControlView {
model: ToggleIcon;
- icon_view: TablerIconView;
+ icon_view: TablerIconView | SVGIconView;
+ was_svg_icon: boolean
public *controls() { }
@@ -19,11 +20,8 @@ export class ToggleIconView extends ControlView {
override async lazy_initialize(): Promise