From 3396a5e6912257a645cd6506efd72e2b5b434a17 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Thu, 18 Jan 2024 14:38:21 +0100 Subject: [PATCH] Backports for 1.3.7 (#6221) --- .github/workflows/test.yaml | 101 +++++++- .pre-commit-config.yaml | 8 +- CHANGELOG.md | 53 ++++ doc/_static/images/asyncify.png | Bin 0 -> 3902 bytes doc/_static/images/dask-dashboard-empty.png | Bin 0 -> 45389 bytes doc/_static/images/dask_fibonacci_queue.png | Bin 0 -> 33876 bytes doc/_static/images/ploomber_deployment.png | Bin 0 -> 112969 bytes doc/_static/logos/dask-logo.svg | 18 ++ doc/_static/logos/ploomber.png | Bin 0 -> 36812 bytes doc/about/releases.md | 53 ++++ doc/conf.py | 7 +- doc/explanation/index.md | 12 +- doc/explanation/styling.md | 14 -- doc/explanation/styling/design.md | 110 ++++++++ doc/explanation/styling/index.md | 15 ++ .../templates_overview.md | 0 doc/getting_started/core_concepts.md | 2 +- doc/how_to/callbacks/async.md | 79 +++--- doc/how_to/concurrency/async.md | 51 ---- doc/how_to/concurrency/dask.md | 235 ++++++++++++++++++ doc/how_to/concurrency/index.md | 32 ++- doc/how_to/concurrency/sync_to_async.md | 40 +++ doc/how_to/deployment/index.md | 8 + doc/how_to/deployment/ploomber.md | 49 ++++ doc/how_to/links/jslinks.md | 2 +- doc/how_to/streamlit_migration/chat.md | 10 +- doc/how_to/streamlit_migration/panes.md | 2 +- doc/how_to/styling/altair.md | 2 +- examples/apps/django/requirements.txt | 2 +- .../apps/django_multi_apps/requirements.txt | 2 +- examples/reference/chat/ChatFeed.ipynb | 49 +++- examples/reference/chat/ChatMessage.ipynb | 1 + examples/reference/templates/Material.ipynb | 2 +- .../reference/widgets/TextAreaInput.ipynb | 1 + examples/reference/widgets/ToggleIcon.ipynb | 25 +- lite/files/Getting_Started.ipynb | 2 +- panel/chat/feed.py | 104 +++++--- panel/chat/interface.py | 15 +- panel/chat/message.py | 10 +- panel/command/serve.py | 4 +- panel/compiler.py | 2 +- panel/config.py | 6 +- .../dist/css/tabulator_fast.min.css | 6 + panel/dist/css/button.css | 2 +- panel/dist/css/chat_message.css | 32 ++- panel/dist/css/loading.css | 2 + panel/dist/css/widgetbox.css | 2 - panel/io/cache.py | 17 +- panel/io/convert.py | 2 +- panel/io/jupyter_server_extension.py | 8 +- panel/io/liveness.py | 2 +- panel/io/model.py | 4 +- panel/io/notebook.py | 24 +- panel/io/rest.py | 2 +- panel/io/server.py | 8 +- panel/io/state.py | 11 +- panel/layout/grid.py | 2 +- panel/layout/gridstack.py | 2 +- panel/links.py | 10 +- panel/models/card.ts | 3 +- panel/models/file_download.ts | 3 +- panel/models/icon.py | 2 +- panel/models/icon.ts | 53 +++- panel/models/perspective.ts | 9 +- panel/models/plotly.py | 4 +- panel/models/plotly.ts | 9 +- panel/models/quill.ts | 136 +++++----- panel/models/tabulator.ts | 2 + panel/models/util.ts | 25 ++ panel/pane/base.py | 46 ++-- panel/pane/holoviews.py | 4 +- panel/pane/plot.py | 3 + panel/pane/vtk/vtk.py | 4 +- panel/param.py | 10 +- panel/pipeline.py | 2 +- panel/reactive.py | 14 +- panel/template/base.py | 3 +- panel/tests/chat/test_feed.py | 53 +++- panel/tests/conftest.py | 2 +- panel/tests/io/test_cache.py | 26 +- panel/tests/io/test_state.py | 2 +- panel/tests/pane/test_holoviews.py | 34 +++ panel/tests/pane/test_plot.py | 13 + panel/tests/test_config.py | 10 +- panel/tests/test_interact.py | 6 +- panel/tests/test_server.py | 6 +- panel/tests/ui/io/test_location.py | 4 +- panel/tests/ui/pane/test_plotly.py | 34 +++ panel/tests/ui/widgets/test_icon.py | 73 ++++++ panel/tests/ui/widgets/test_input.py | 29 +++ panel/tests/ui/widgets/test_tabulator.py | 6 +- panel/tests/ui/widgets/test_texteditor.py | 2 +- panel/tests/util.py | 2 +- panel/tests/widgets/test_icon.py | 4 + panel/tests/widgets/test_tqdm.py | 10 +- panel/theme/base.py | 2 + panel/util/__init__.py | 27 +- panel/viewable.py | 3 +- panel/widgets/icon.py | 11 +- panel/widgets/indicators.py | 2 +- panel/widgets/input.py | 75 +++++- panel/widgets/misc.py | 7 +- panel/widgets/select.py | 2 +- panel/widgets/slider.py | 4 +- panel/widgets/tables.py | 6 +- pyproject.toml | 3 +- setup.py | 3 +- 107 files changed, 1645 insertions(+), 412 deletions(-) create mode 100644 doc/_static/images/asyncify.png create mode 100644 doc/_static/images/dask-dashboard-empty.png create mode 100644 doc/_static/images/dask_fibonacci_queue.png create mode 100644 doc/_static/images/ploomber_deployment.png create mode 100644 doc/_static/logos/dask-logo.svg create mode 100644 doc/_static/logos/ploomber.png delete mode 100644 doc/explanation/styling.md create mode 100644 doc/explanation/styling/design.md create mode 100644 doc/explanation/styling/index.md rename doc/explanation/{templates => styling}/templates_overview.md (100%) delete mode 100644 doc/how_to/concurrency/async.md create mode 100644 doc/how_to/concurrency/dask.md create mode 100644 doc/how_to/concurrency/sync_to_async.md create mode 100644 doc/how_to/deployment/ploomber.md diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index f362e6edb95..16135a21bf2 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 5707fe41993..3a6e21dd319 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 5b3fc151095..865b7710308 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 0000000000000000000000000000000000000000..527976db87fbf5a9b77221ef4b5494e5ab7d8c70 GIT binary patch literal 3902 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%0D5F!;boz9e(?NnStm z`>1ieV#(SfHA7Sb`b%64TXyEySkF~a4EnS$iGW-FxlwA*2}-#|+%nFR$d7DH#fyGK znDb5o&&Z2esIr-)Dq={0Wu=1hALOufNN*<@Mtuwgo*9RaQsy&7kpTMyal#1{FyH#D ziqg}AV8$QaKu4Pe3qhZUtafbUS+_-33fWBT1A?bxj78?5v z?dxv>zYQL2NYpE&p~d`C3vbuHoov*=rn9VLGn(DgD&kYo0tA8z=LywX13fn$elquQ zsaXJFwfn+nUyx~s!fbKrtGDH=^Q$7irIYbgnX7-hBfy$&lX#h~qP5#fH@9yX<-k)^ z_wEVabWrl7RJ~f&@j8zqUv^>Vd0kZn1>1Xx&YU0Z>`jdkgm1nEhx|}LraO9IGnn>& z*WJ8zl7aU-Zqh|pgrgqub+kIvV*$B6bb62=9^Bb|h`X?mZttWR-f%?SGMDgfw=>A& z=*|YpEQ$K9?zQQh^7*f;Z51+*d^uYR9#}~S={n2EjFr`tZ{Z8>G%a|Ml@Nrih4$$| z&69xJybqn#?h_Ua33h?2qLXdGLN)3}O2J-txjd2_pUt2LM@j*HNC?CAPd>75>FrL6 zw_-2FqZ-nOM!lrGVe%AAP0pc)flgbH@0sY&xTMVHmTGlD(4Su3f$c)fXVpj4wR2}x z=zheh7D#O3s)N4>ue*f=05ortHDTHyBonBbOXReHOdfR%Ia&CK-R)`+HBe8-TZdi< zF48uTUvY47L!5U!W7}ECW1-L63HfF0i7=^>XwRESewTOtIjiWc#x4`tWoZ@bh~9y!5CkT<;l{aQsn zbf2KepkPIyisMuHJ{r)-qxtm=pd**7|LUig_sM&*EJAH2^!H?xL8F_R#C$#>GQBlwY)X7;!{5J$IhMoWb literal 0 HcmV?d00001 diff --git a/doc/_static/images/dask_fibonacci_queue.png b/doc/_static/images/dask_fibonacci_queue.png new file mode 100644 index 0000000000000000000000000000000000000000..6ecbca2760d79819e66f8c8b70b5a707e2723159 GIT binary patch literal 33876 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 E0=>;QZ2$lO literal 0 HcmV?d00001 diff --git a/doc/_static/images/ploomber_deployment.png b/doc/_static/images/ploomber_deployment.png new file mode 100644 index 0000000000000000000000000000000000000000..922c46c7808d119b49fa759bd1ea23348ba63c02 GIT binary patch literal 112969 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

|94`KT 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 HF6w^)u<@vP literal 0 HcmV?d00001 diff --git a/doc/_static/logos/dask-logo.svg b/doc/_static/logos/dask-logo.svg new file mode 100644 index 00000000000..3ac766541b8 --- /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 0000000000000000000000000000000000000000..9a18e0903f447d1877851f0392b86f87a54791e9 GIT binary patch literal 36812 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�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 literal 0 HcmV?d00001 diff --git a/doc/about/releases.md b/doc/about/releases.md index 6d1e9d54bdf..d6af9fc7e02 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 a4e2a5d8f64..943fb2e00b4 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 da6f70ff750..8c3b98d07b9 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 f89d51aba1e..00000000000 --- a/doc/explanation/styling.md +++ /dev/null @@ -1,14 +0,0 @@ -.. raw:: html - - - - -# Styling - -```{toctree} -:titlesonly: -:hidden: -:maxdepth: 1 - -Templates -``` diff --git a/doc/explanation/styling/design.md b/doc/explanation/styling/design.md new file mode 100644 index 00000000000..af32ffa31be --- /dev/null +++ b/doc/explanation/styling/design.md @@ -0,0 +1,110 @@ +# Designs and Theming + +:::{versionadded} 1.0.0 +Designs and theming allow controlling the look and feel of individual components or your entire application. Before version 1.0 the concept of a design and a template were combined but since it is now possible to apply a `Design` separately from a template. +::: + +## `Design` class + +The `Design` class has the ability to apply styling and theming to components via three mechanisms: + +1. Loading external JS and CSS `resources`. +2. Applying `modifiers` that directly override (or combine with) component parameter settings (including CSS stylesheets). + +Additionally a `Design` class always has an associated `Theme` which determines the color palette of the design. This also has the ability to: + +1. Apply a `base_css` (usually containing CSS variable definitions and inherited from the base class). +2. Apply a `bokeh_theme` to override Bokeh component styling. +3. Apply a `css` with specific CSS overrides for that theme. +4. Apply its own set of `modifiers`. + +### Example: Bootstrap + +Let us work through the definition of the `Bootstrap` design step-by-step. + +#### Resources + +First of all, the `Bootstrap` design defines CSS and JS `resources` which load the bootstrap5 library: + +```python + _resources = { + 'css': { + 'bootstrap': CSS_URLS['bootstrap5'] + }, + 'js': { + 'bootstrap': JS_URLS['bootstrap5'] + } + } +``` + +#### Modifiers + +Next we get to the `modifiers` which is where the meat of the styling definition lives. Modifiers are applied per class and override the default parameter settings of a component. We can apply modifiers to a base class such as `Viewable`, which will affect all components or we can override a specific class, e.g. `Tabulator`: + +```python + modifiers = { + Accordion: { + 'active_header_background': 'var(--bs-surface-bg)' + } + ... + Tabulator: { + 'theme': 'bootstrap4' + }, + Viewable: { + 'stylesheets': [Inherit, f'{CDN_DIST}bundled/theme/bootstrap.css'] + } + } +``` + +Here we define specific parameters to override or combine with, e.g. we set the `active_header_background` of an `Accordion` to inherit from the `--bs-surface-bg` CSS variable. In this way we can control settings that are not controlled by CSS. However, most crucially we also declare that all `Viewable` components should inherit a bundled stylesheet: `bundled/theme/bootstrap.css`. + +The `bootstrap.css` file includes styling overrides for many different components and will be loaded by all `Viewable` components. + +#### CSS stylesheets + +All design stylesheets open with CSS variable definitions which map the global color definitions to specific variables used by a particular design system: + +```css +:host, :root { + --bs-primary-color: var(--jp-ui-font-color0, var(--panel-on-primary-color)); + --bs-primary-bg: var(--jp-brand-color0, var(--panel-primary-color)); +} +``` + +The `:host` and `:root` selectors ensure that these variable definition apply inside the shadow DOM, i.e. that they apply to the shadow `host`, and globally to the document `root` (i.e. usually the `` element). + +Note also how we map the `--bs-primary-color` color to two other variables, first the `--jp-ui-font-color0`, which allows us to automatically inherit styles in Jupyter based environments and secondly the `--panel-on-primary-color` which is the primary definition of Panel color themes and also the entrypoint for users to override the color definitions. + +Once declared these CSS variables are then used to set the style of specific components: + +```css +.bk-menu { + background-color: var(--bs-form-control-bg); + color: var(--bs-form-control-color); +} +``` + +In this way global style definitions and variables can flow down all the way to individual components. + +#### Themes + +Lastly we get to the themes, each `Design` component declares the set of supported themes by name: + +```python + _themes = { + 'dark': BootstrapDarkTheme, + 'default': BootstrapDefaultTheme, + } +``` + +Currently only `default` (light) and `dark` themes are supported. The implementation of these themes subclasses from the `DefaultTheme` and `DarkTheme` base classes and applies specific overrides. + +## Developing an internal theme + +If you want to help contribute improvements to a `Design` or `Theme` simply edit the CSS definitions in `panel/theme/css` and periodically re-bundle the resources with: + +```bash +panel bundle --themes --only-local +``` + +On UNIX you can simply use `watch -n 5 panel bundle --themes --only-local` to re-bundle these resources every few seconds. diff --git a/doc/explanation/styling/index.md b/doc/explanation/styling/index.md new file mode 100644 index 00000000000..dcb886bc077 --- /dev/null +++ b/doc/explanation/styling/index.md @@ -0,0 +1,15 @@ +.. raw:: html + + + + +# Styling + +```{toctree} +:titlesonly: +:hidden: +:maxdepth: 1 + +Design & Theme +Templates +``` diff --git a/doc/explanation/templates/templates_overview.md b/doc/explanation/styling/templates_overview.md similarity index 100% rename from doc/explanation/templates/templates_overview.md rename to doc/explanation/styling/templates_overview.md diff --git a/doc/getting_started/core_concepts.md b/doc/getting_started/core_concepts.md index e6f7140d671..dfd1431ebd5 100644 --- a/doc/getting_started/core_concepts.md +++ b/doc/getting_started/core_concepts.md @@ -93,7 +93,7 @@ Row ``` ::: -Sometimes an object has multiple possible representations to pick from. In these cases you can explicitly construct construct the desired `Pane` type, e.g. here are a few representations of a `DataFrame`: +Sometimes an object has multiple possible representations to pick from. In these cases you can explicitly construct the desired `Pane` type, e.g. here are a few representations of a `DataFrame`: ::::{tab-set} diff --git a/doc/how_to/callbacks/async.md b/doc/how_to/callbacks/async.md index 92156ed2667..d43eb3bcfe3 100644 --- a/doc/how_to/callbacks/async.md +++ b/doc/how_to/callbacks/async.md @@ -1,13 +1,22 @@ # Use Asynchronous Callbacks -This guide addresses how to leverage asynchronous callbacks to run I/O bound tasks in parallel. +This guide addresses how to leverage asynchronous callbacks to run I/O bound tasks in parallel. This technique is also beneficial for CPU bound tasks that release the GIL. -```{admonition} Prerequisites -1. Python has natively supported asynchronous functions since version 3.5, for a quick overview of some of the concepts involved see [the Python documentation](https://docs.python.org/3/library/asyncio-task.html). +You can use `async` function with event handlers like `on_click` as well as the reactive apis `.bind`, `.depends` and `.watch`. + +You can also schedule asynchronous periodic callbacks with `pn.state.add_periodic_callback` as well as run `async` functions directly with `pn.state.execute`. + +```{admonition} Asyncio +For a quick overview of the most important `asyncio` concepts see [the Python documentation](https://docs.python.org/3/library/asyncio-task.html). +``` + +```{admonition} Bokeh Models +It is important to note that asynchronous callbacks operate without locking the underlying Bokeh Document, which means Bokeh models cannot be safely modified by default. Usually this is not an issue because modifying Panel components appropriately schedules updates to underlying Bokeh models, however in cases where we want to modify a Bokeh model directly, e.g. when embedding and updating a Bokeh plot in a Panel application we explicitly have to decorate the asynchronous callback with `pn.io.with_lock` (see example below). ``` + --- -## `.param.watch` +## `on_click` One of the major benefits of leveraging async functions is that it is simple to write callbacks which will perform some longer running IO tasks in the background. Below we simulate this by creating a `Button` which will update some text when it starts and finishes running a long-running background task (here simulated using `asyncio.sleep`. If you are running this in the notebook you will note that you can start multiple tasks and it will update the text immediately but continue in the background: @@ -30,36 +39,7 @@ button.on_click(run_async) pn.Row(button, text) ``` -Note that `on_click` is simple one way of registering an asynchronous callback, but the more flexible `.param.watch` is also supported. Scheduling asynchronous periodic callbacks can be done with `pn.state.add_periodic_callback`. - -It is important to note that asynchronous callbacks operate without locking the underlying Bokeh Document, which means Bokeh models cannot be safely modified by default. Usually this is not an issue because modifying Panel components appropriately schedules updates to underlying Bokeh models, however in cases where we want to modify a Bokeh model directly, e.g. when embedding and updating a Bokeh plot in a Panel application we explicitly have to decorate the asynchronous callback with `pn.io.with_lock`. - -```{pyodide} -import numpy as np -from bokeh.plotting import figure -from bokeh.models import ColumnDataSource - -button = pn.widgets.Button(name='Click me!') - -p = figure(width=500, height=300) -cds = ColumnDataSource(data={'x': [0], 'y': [0]}) -p.line(x='x', y='y', source=cds) -pane = pn.pane.Bokeh(p) - -@pn.io.with_lock -async def stream(event): - await asyncio.sleep(1) - x, y = cds.data['x'][-1], cds.data['y'][-1] - cds.stream({'x': list(range(x+1, x+6)), 'y': y+np.random.randn(5).cumsum()}) - pane.param.trigger('object') - -# Equivalent to `.on_click` but shown -button.param.watch(stream, 'clicks') - -pn.Row(button, pane) -``` - -## `pn.bind` +## `.bind` ```{pyodide} widget = pn.widgets.IntSlider(start=0, end=10) @@ -80,7 +60,9 @@ pn.Column(widget, pn.bind(get_img, widget)) In this example Panel will invoke the function and update the output when the function returns while leaving the process unblocked for the duration of the `aiohttp` request. -The equivalent can be written using `.param.watch` as: +## `.watch` + +The app from the section above can be written using `.param.watch` as: ```{pyodide} widget = pn.widgets.IntSlider(start=0, end=10) @@ -107,6 +89,33 @@ pn.Column(widget, image) In this example Param will await the asynchronous function and the image will be updated when the request completes. +## Bokeh models with `pn.io.with_lock` + +```{pyodide} +import numpy as np +from bokeh.plotting import figure +from bokeh.models import ColumnDataSource + +button = pn.widgets.Button(name='Click me!') + +p = figure(width=500, height=300) +cds = ColumnDataSource(data={'x': [0], 'y': [0]}) +p.line(x='x', y='y', source=cds) +pane = pn.pane.Bokeh(p) + +@pn.io.with_lock +async def stream(event): + await asyncio.sleep(1) + x, y = cds.data['x'][-1], cds.data['y'][-1] + cds.stream({'x': list(range(x+1, x+6)), 'y': y+np.random.randn(5).cumsum()}) + pane.param.trigger('object') + +# Equivalent to `.on_click` but shown +button.param.watch(stream, 'clicks') + +pn.Row(button, pane) +``` + ## Related Resources - See the related [How-to > Link Parameters with Callbacks API](../links/index.md) guides, including [How to > Create Low-Level Python Links Using `.watch`](../links/watchers.md). diff --git a/doc/how_to/concurrency/async.md b/doc/how_to/concurrency/async.md deleted file mode 100644 index 7fb7494bab0..00000000000 --- a/doc/how_to/concurrency/async.md +++ /dev/null @@ -1,51 +0,0 @@ -# Use Asynchronous Processing - -When using Python you can use async callbacks wherever you would ordinarily use a regular synchronous function. For instance you can use `pn.bind` on an async function: - -```{pyodide} -import panel as pn - -widget = pn.widgets.IntSlider(start=0, end=10) - -async def get_img(index): - url = f"https://picsum.photos/800/300?image={index}" - if pn.state._is_pyodide: - from pyodide.http import pyfetch - return pn.pane.JPG(await (await pyfetch(url)).bytes()) - - import aiohttp - async with aiohttp.ClientSession() as session: - async with session.get(url) as resp: - return pn.pane.JPG(await resp.read()) - -pn.Column(widget, pn.bind(get_img, widget)) -``` - -In this example Panel will invoke the function and update the output when the function returns while leaving the process unblocked for the duration of the `aiohttp` request. - -Similarly you can attach asynchronous callbacks using `.param.watch`: - -```{pyodide} -widget = pn.widgets.IntSlider(start=0, end=10) - -image = pn.pane.JPG() - -async def update_img(event): - url = f"https://picsum.photos/800/300?image={event.new}" - if pn.state._is_pyodide: - from pyodide.http import pyfetch - image.object = await (await pyfetch(url)).bytes() - return - - import aiohttp - async with aiohttp.ClientSession() as session: - async with session.get(url) as resp: - image.object = await resp.read() - -widget.param.watch(update_img, 'value') -widget.param.trigger('value') - -pn.Column(widget, image) -``` - -In this example Param will await the asynchronous function and the image will be updated when the request completes. diff --git a/doc/how_to/concurrency/dask.md b/doc/how_to/concurrency/dask.md new file mode 100644 index 00000000000..bd015acf009 --- /dev/null +++ b/doc/how_to/concurrency/dask.md @@ -0,0 +1,235 @@ +# Scaling with Dask + +This guide demonstrates how you can *offload tasks* to [Dask](https://www.dask.org/) to **scale your apps to bigger datasets, bigger calculations and more users**. + + + +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: + +![Empty Dask Dashboard](../../_static/images/dask-dashboard-empty.png) + +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 + +![Dask Viewer](https://assets.holoviz.org/panel/how_to/concurrency/dask-dashboard.gif) + +## 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 03be0a31c68..7e965bb0ef6 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 00000000000..cd80c3da17e --- /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 536345e0ae9..702d71ced1b 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 ![Hugging Face Logo](../../_static/logos/huggingface.png) ::: +:::{grid-item-card} Ploomber Cloud +:link: ploomber +:link-type: doc + +![Ploomber Logo](../../_static/logos/ploomber.png) +::: + :::: ## 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 00000000000..61ec362aa4a --- /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 15d2035f725..9b1837afe38 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 a6e14baa959..4190c407aab 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 d6806408699..f83a6c108c1 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 3f67c6957fc..54cfc568360 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 59fc2ed647b..58724108f00 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 dea92220fa2..fd6f5c5a3df 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 3c03d464d67..0904ad03d11 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 4f5b485e97d..6fb9e99e73f 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 148bcbc5cde..81211d64d68 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 b9a990b5681..7371245083e 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 839c5f908ab..b8ee06a06d1 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 65b644444b8..004e861abd4 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 2f048d01e44..0cfff58d09c 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 3ef6e68e775..8642dc4e25b 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 9996c5a5b9f..2b4f7c02ce4 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 bcff57c5ccc..de857a3c874 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 b3cae33e17b..06270470421 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 b4b26cea0d4..a1909cbf3ef 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 61663d96117..7287073ae06 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 d2c902a9c50..5ae415e8132 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 692dcc4cfa8..fe39d2ea1cc 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 80294a3bf74..6dbd45299e8 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 56ec2f00fe4..a20fcf02e4c 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 6927c7e0428..5a3016dbeb7 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 bf82a4f93d2..c3e9fb48154 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 9bc5d349b01..8ea0a26383d 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 3f65b270a95..e0975db2605 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 44e6606d35b..5fbb5d1757f 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 c09ec4ce887..61c12766c34 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 0a76532ed77..0bd052315c7 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 7c947f0dcf0..dc9f01b9a52 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 09b60a8e429..6336b107510 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 728fbaf4202..f8c051cce0b 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 a35d3606a2f..bfb9a62e742 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 fedf3cb49a9..faf359e1aa1 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 7a546041c7f..8c16f5b7c4c 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 486a19b5291..bdb21f161a5 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 5878a36348e..059af48a0e9 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 1b9cf847f2c..911c6c80b2b 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 { await super.lazy_initialize(); - const size = this.calculate_size(); - const icon_model = new TablerIcon({ icon_name: this.model.icon, size: size }); - this.icon_view = await build_view(icon_model, { parent: this }); - - this.icon_view.el.addEventListener('click', () => this.toggle_value()); + this.was_svg_icon = this.is_svg_icon(this.model.icon) + this.icon_view = await this.build_icon_model(this.model.icon, this.was_svg_icon); } override *children(): IterViews { @@ -31,6 +29,10 @@ export class ToggleIconView extends ControlView { yield this.icon_view; } + is_svg_icon(icon: string): boolean { + return icon.trim().startsWith(' { + const size = this.calculate_size(); + let icon_model; + if (is_svg_icon) { + icon_model = new SVGIcon({ svg: icon, size: size }); + } else { + icon_model = new TablerIcon({ icon_name: icon, size: size }); + } + const icon_view = await build_view(icon_model, { parent: this }); + icon_view.el.addEventListener('click', () => this.toggle_value()); + return icon_view; + } + + async update_icon(): Promise { const icon = this.model.value ? this.get_active_icon() : this.model.icon; - this.icon_view.model.icon_name = icon; + const is_svg_icon = this.is_svg_icon(icon) + + if (this.was_svg_icon !== is_svg_icon) { + // If the icon type has changed, we need to rebuild the icon view + // and invalidate the old one. + const icon_view = await this.build_icon_model(icon, is_svg_icon); + icon_view.render(); + this.icon_view.remove(); + this.icon_view = icon_view; + this.was_svg_icon = is_svg_icon; + this.update_cursor(); + this.shadow_el.appendChild(this.icon_view.el); + } + else if (is_svg_icon) { + (this.icon_view as SVGIconView).model.svg = icon; + } else { + (this.icon_view as TablerIconView).model.icon_name = icon; + } this.icon_view.el.style.lineHeight = '0'; } @@ -106,7 +137,7 @@ export class ToggleIcon extends Control { this.define(({ Boolean, Nullable, String }) => ({ active_icon: [String, ""], icon: [String, "heart"], - size: [Nullable(String), null ], + size: [Nullable(String), null], value: [Boolean, false], })); } diff --git a/panel/models/perspective.ts b/panel/models/perspective.ts index 0ab5f0df1b6..f04fed58afc 100644 --- a/panel/models/perspective.ts +++ b/panel/models/perspective.ts @@ -219,8 +219,13 @@ export class PerspectiveView extends HTMLBoxView { get data(): any { const data: any = {} - for (const column of this.model.source.columns()) - data[column] = this.model.source.get_array(column) + for (const column of this.model.source.columns()) { + let array = this.model.source.get_array(column) + if (this.model.schema[column] == 'datetime' && array.includes(-9223372036854776)) { + array = array.map((v) => v === -9223372036854776 ? null : v) + } + data[column] = array + } return data } diff --git a/panel/models/plotly.py b/panel/models/plotly.py index 7886419d462..9e1318d9ff9 100644 --- a/panel/models/plotly.py +++ b/panel/models/plotly.py @@ -18,7 +18,7 @@ class PlotlyPlot(LayoutDOM): __javascript_raw__ = [ JS_URLS['jQuery'], - 'https://cdn.plot.ly/plotly-2.18.0.min.js' + 'https://cdn.plot.ly/plotly-2.25.2.min.js' ] @classproperty @@ -31,7 +31,7 @@ def __js_skip__(cls): __js_require__ = { 'paths': { - 'plotly': 'https://cdn.plot.ly/plotly-2.18.0.min' + 'plotly': 'https://cdn.plot.ly/plotly-2.25.2.min' }, 'exports': {'plotly': 'Plotly'} } diff --git a/panel/models/plotly.ts b/panel/models/plotly.ts index ec0fd2080fb..0c59aa13773 100644 --- a/panel/models/plotly.ts +++ b/panel/models/plotly.ts @@ -5,7 +5,7 @@ import {is_equal} from "@bokehjs/core/util/eq" import {ColumnDataSource} from "@bokehjs/models/sources/column_data_source"; import {debounce} from "debounce" -import {deepCopy, isPlainObject, get, throttle} from "./util" +import {deepCopy, isPlainObject, get, reshape, throttle} from "./util" import {HTMLBox, HTMLBoxView, set_size} from "./layout" @@ -321,12 +321,7 @@ export class PlotlyPlotView extends HTMLBoxView { for (const column of cds.columns()) { let array = cds.get_array(column)[0]; if (array.shape != null && array.shape.length > 1) { - const arrays = []; - const shape = array.shape; - for (let s = 0; s < shape[0]; s++) { - arrays.push(array.slice(s*shape[1], (s+1)*shape[1])); - } - array = arrays; + array = reshape(array, array.shape); } let prop_path = column.split("."); let prop = prop_path[prop_path.length - 1]; diff --git a/panel/models/quill.ts b/panel/models/quill.ts index d56e01f37ee..f4667725322 100644 --- a/panel/models/quill.ts +++ b/panel/models/quill.ts @@ -1,49 +1,7 @@ import * as p from "@bokehjs/core/properties" import { div } from "@bokehjs/core/dom" -import {HTMLBox, HTMLBoxView} from "./layout" - -const normalizeNative = (nativeRange: any) => { - - // document.getSelection model has properties startContainer and endContainer - // shadow.getSelection model has baseNode and focusNode - // Unify formats to always look like document.getSelection - - if (nativeRange) { - - const range = nativeRange; - - // // HACK: To allow pasting - if (range.baseNode?.classList?.value === 'ql-clipboard') { - return null - } - - if (range.baseNode) { - range.startContainer = nativeRange.baseNode; - range.endContainer = nativeRange.focusNode; - range.startOffset = nativeRange.baseOffset; - range.endOffset = nativeRange.focusOffset; - - if (range.endOffset < range.startOffset) { - range.startContainer = nativeRange.focusNode; - range.endContainer = nativeRange.baseNode; - range.startOffset = nativeRange.focusOffset; - range.endOffset = nativeRange.baseOffset; - } - } - - if (range.startContainer) { - - return { - start: { node: range.startContainer, offset: range.startOffset }, - end: { node: range.endContainer, offset: range.endOffset }, - native: range - }; - } - } - - return null -}; +import { HTMLBox, HTMLBoxView } from "./layout" export class QuillInputView extends HTMLBoxView { override model: QuillInput @@ -59,7 +17,7 @@ export class QuillInputView extends HTMLBoxView { this.connect(this.model.properties.disabled.change, () => this.quill.enable(!this.model.disabled)) this.connect(this.model.properties.visible.change, () => { if (this.model.visible) - this.container.style.visibility = 'visible'; + this.container.style.visibility = 'visible'; }) this.connect(this.model.properties.text.change, () => { if (this._editing) @@ -71,7 +29,7 @@ export class QuillInputView extends HTMLBoxView { this.quill.enable(!this.model.disabled) this._editing = false }) - const {mode, toolbar, placeholder} = this.model.properties + const { mode, toolbar, placeholder } = this.model.properties this.on_change([placeholder], () => { this.quill.root.setAttribute('data-placeholder', this.model.placeholder) }) @@ -93,7 +51,7 @@ export class QuillInputView extends HTMLBoxView { render(): void { super.render() - this.container = div({style: "visibility: hidden;"}) + this.container = div({ style: "visibility: hidden;" }) this.shadow_el.appendChild(this.container) const theme = (this.model.mode === 'bubble') ? 'bubble' : 'snow' this.watch_stylesheets() @@ -106,16 +64,82 @@ export class QuillInputView extends HTMLBoxView { theme: theme }); - // Apply only with getSelection() is defined (e.g. undefined on Firefox) - if (typeof this.quill.root.getRootNode().getSelection !== 'undefined') { - // Hack Quill and replace document.getSelection with shadow.getSelection - // see https://stackoverflow.com/questions/67914657/quill-editor-inside-shadow-dom/67944380#67944380 - this.quill.selection.getNativeRange = () => { + // Apply ShadowDOM patch found at: + // https://github.com/quilljs/quill/issues/2961#issuecomment-1775999845 + + const hasShadowRootSelection = !!((document.createElement('div').attachShadow({ mode: 'open' }) as any).getSelection); + // Each browser engine has a different implementation for retrieving the Range + const getNativeRange = (rootNode: any) => { + try { + if (hasShadowRootSelection) { + // In Chromium, the shadow root has a getSelection function which returns the range + return rootNode.getSelection().getRangeAt(0); + } else { + const selection = window.getSelection(); + if ((selection as any).getComposedRanges) { + // Webkit range retrieval is done with getComposedRanges (see: https://bugs.webkit.org/show_bug.cgi?id=163921) + return (selection as any).getComposedRanges(rootNode)[0]; + } else { + // Gecko implements the range API properly in Native Shadow: https://developer.mozilla.org/en-US/docs/Web/API/Selection/getRangeAt + return (selection as any).getRangeAt(0); + } + } + } catch { + return null; + } + } + + /** + * Original implementation uses document.active element which does not work in Native Shadow. + * Replace document.activeElement with shadowRoot.activeElement + **/ + this.quill.selection.hasFocus = () => { + const rootNode = (this.quill.root.getRootNode() as ShadowRoot); + return rootNode.activeElement === this.quill.root; + } - const selection = (this.shadow_el as any).getSelection(); - const range = normalizeNative(selection); - return range; - }; + /** + * Original implementation uses document.getSelection which does not work in Native Shadow. + * Replace document.getSelection with shadow dom equivalent (different for each browser) + **/ + this.quill.selection.getNativeRange = () => { + const rootNode = (this.quill.root.getRootNode() as ShadowRoot); + const nativeRange = getNativeRange(rootNode); + return !!nativeRange ? this.quill.selection.normalizeNative(nativeRange) : null; + }; + + /** + * Original implementation relies on Selection.addRange to programmatically set the range, which does not work + * in Webkit with Native Shadow. Selection.addRange works fine in Chromium and Gecko. + **/ + this.quill.selection.setNativeRange = (startNode: any, startOffset: any) => { + var endNode = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : startNode; + var endOffset = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : startOffset; + var force = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : false; + if (startNode != null && (this.quill.selection.root.parentNode == null || startNode.parentNode == null || endNode.parentNode == null)) { + return; + } + var selection = document.getSelection(); + if (selection == null) return; + if (startNode != null) { + if (!this.quill.selection.hasFocus()) this.quill.selection.root.focus(); + var native = (this.quill.selection.getNativeRange() || {}).native; + if (native == null || force || startNode !== native.startContainer || startOffset !== native.startOffset || endNode !== native.endContainer || endOffset !== native.endOffset) { + if (startNode.tagName == "BR") { + startOffset = [].indexOf.call(startNode.parentNode.childNodes, startNode); + startNode = startNode.parentNode; + } + if (endNode.tagName == "BR") { + endOffset = [].indexOf.call(endNode.parentNode.childNodes, endNode); + endNode = endNode.parentNode; + } + selection.setBaseAndExtent(startNode, startOffset, endNode, endOffset); + } + } else { + selection.removeAllRanges(); + this.quill.selection.root.blur(); + document.body.focus(); + } } this._editor = (this.shadow_el.querySelector('.ql-editor') as HTMLDivElement) @@ -167,7 +191,7 @@ export namespace QuillInput { } } -export interface QuillInput extends QuillInput.Attrs {} +export interface QuillInput extends QuillInput.Attrs { } export class QuillInput extends HTMLBox { properties: QuillInput.Props diff --git a/panel/models/tabulator.ts b/panel/models/tabulator.ts index c7318144ddc..83c9e78a2f5 100644 --- a/panel/models/tabulator.ts +++ b/panel/models/tabulator.ts @@ -367,6 +367,8 @@ export class DataTabulatorView extends HTMLBoxView { this.connect(p.sorters.change, () => this.setSorters()) this.connect(p.theme_classes.change, () => this.setCSSClasses(this.tabulator.element)) this.connect(this.model.source.properties.data.change, () => { + if (this.tabulator === undefined) + return this._selection_updating = true this.setData() this._selection_updating = false diff --git a/panel/models/util.ts b/panel/models/util.ts index 59397cd8a6d..e16bc40275d 100644 --- a/panel/models/util.ts +++ b/panel/models/util.ts @@ -1,3 +1,6 @@ +import {concat} from "@bokehjs/core/util/array" + + export const get = (obj: any, path: string, defaultValue: any = undefined) => { const travel = (regexp: RegExp) => String.prototype.split @@ -51,3 +54,25 @@ export function deepCopy(obj: any): any { export function isPlainObject (obj: any) { return Object.prototype.toString.call(obj) === '[object Object]'; } + +export function reshape(arr: any[], dim: number[]) { + let elemIndex = 0; + + if (!dim || !arr) return []; + + function _nest(dimIndex: number): any[] { + let result = []; + + if (dimIndex === dim.length - 1) { + result = concat(arr.slice(elemIndex, elemIndex + dim[dimIndex])); + elemIndex += dim[dimIndex]; + } else { + for (let i = 0; i < dim[dimIndex]; i++) { + result.push(_nest(dimIndex + 1)); + } + } + + return result; + } + return _nest(0); +} diff --git a/panel/pane/base.py b/panel/pane/base.py index 0cb22bf135f..8794375e687 100644 --- a/panel/pane/base.py +++ b/panel/pane/base.py @@ -267,47 +267,46 @@ def _update_object( ] if indexes: index = indexes[0] + new_model = (new_model,) + parent.children[index][1:] + parent.children[index] = new_model else: raise ValueError - new_model = (new_model,) + parent.children[index][1:] elif isinstance(parent, _BkReactiveHTML): for node, children in parent.children.items(): if old_model in children: index = children.index(old_model) new_models = list(children) new_models[index] = new_model + parent.children[node] = new_models break elif isinstance(parent, _BkTabs): index = [tab.child for tab in parent.tabs].index(old_model) + old_tab = parent.tabs[index] + props = dict(old_tab.properties_with_values(), child=new_model) + parent.tabs[index] = _BkTabPanel(**props) else: index = parent.children.index(old_model) + parent.children[index] = new_model except ValueError: self.param.warning( f'{type(self).__name__} pane model {old_model!r} could not be ' f'replaced with new model {new_model!r}, ensure that the parent ' 'is not modified at the same time the panel is being updated.' ) - else: - if isinstance(parent, _BkReactiveHTML): - parent.children[node] = new_models - elif isinstance(parent, _BkTabs): - old_tab = parent.tabs[index] - props = dict(old_tab.properties_with_values(), child=new_model) - parent.tabs[index] = _BkTabPanel(**props) - else: - parent.children[index] = new_model - layout_parent = self.layout._models.get(ref, [None])[0] - if parent is layout_parent: - parent.update(**self.layout._compute_sizing_mode( - parent.children, - dict( - sizing_mode=self.layout.sizing_mode, - styles=self.layout.styles, - width=self.layout.width, - min_width=self.layout.min_width, - margin=self.layout.margin - ) - )) + return + + layout_parent = self.layout._models.get(ref, [None])[0] + if parent is layout_parent: + parent.update(**self.layout._compute_sizing_mode( + parent.children, + dict( + sizing_mode=self.layout.sizing_mode, + styles=self.layout.styles, + width=self.layout.width, + min_width=self.layout.min_width, + margin=self.layout.margin + ) + )) from ..io import state ref = root.ref['id'] @@ -678,7 +677,8 @@ def _update_from_object(cls, object: Any, old_object: Any, was_internal: bool, i cls._recursive_update(old, new) elif isinstance(object, Reactive): cls._recursive_update(old_object, object) - else: + elif old_object.object is not object: + # See https://github.com/holoviz/param/pull/901 old_object.object = object else: # Replace pane entirely diff --git a/panel/pane/holoviews.py b/panel/pane/holoviews.py index 4b4830a3285..855de13082e 100644 --- a/panel/pane/holoviews.py +++ b/panel/pane/holoviews.py @@ -499,7 +499,7 @@ def _render(self, doc, comm, root): params = {} if self.theme is not None: params['theme'] = self.theme - elif doc.theme and getattr(doc.theme, '_json') != {'attrs': {}}: + elif doc.theme and doc.theme._json != {'attrs': {}}: params['theme'] = doc.theme elif self._design.theme.bokeh_theme: params['theme'] = self._design.theme.bokeh_theme @@ -897,7 +897,7 @@ def link_axes(root_view, root_model): changed.append('y_range') # Reinitialize callbacks linked to replaced axes - subplots = getattr(p, 'subplots') + subplots = p.subplots if subplots: plots = subplots.values() else: diff --git a/panel/pane/plot.py b/panel/pane/plot.py index cf73a59d4ef..e484764c0d3 100644 --- a/panel/pane/plot.py +++ b/panel/pane/plot.py @@ -349,6 +349,9 @@ def _update(self, ref: str, model: Model) -> None: manager.canvas.draw_idle() def _data(self, obj): + if obj is None: + return + try: obj.set_dpi(self.dpi) except Exception as ex: diff --git a/panel/pane/vtk/vtk.py b/panel/pane/vtk/vtk.py index 6d2f8d54381..b1bfc2603f5 100644 --- a/panel/pane/vtk/vtk.py +++ b/panel/pane/vtk/vtk.py @@ -76,7 +76,7 @@ class AbstractVTK(PaneBase): def _process_param_change(self, msg): msg = super()._process_param_change(msg) if 'axes' in msg and msg['axes'] is not None: - VTKAxes = getattr(sys.modules['panel.models.vtk'], 'VTKAxes') + VTKAxes = sys.modules['panel.models.vtk'].VTKAxes axes = msg['axes'] msg['axes'] = VTKAxes(**axes) return msg @@ -86,7 +86,7 @@ def _update_model( root: Model, model: Model, doc: Document, comm: Optional[Comm] ) -> None: if 'axes' in msg and msg['axes'] is not None: - VTKAxes = getattr(sys.modules['panel.models.vtk'], 'VTKAxes') + VTKAxes = sys.modules['panel.models.vtk'].VTKAxes axes = msg['axes'] if isinstance(axes, dict): msg['axes'] = VTKAxes(**axes) diff --git a/panel/param.py b/panel/param.py index 81463ba9681..b949958121e 100644 --- a/panel/param.py +++ b/panel/param.py @@ -372,11 +372,11 @@ def toggle_pane(change, parameter=pname): pane = Param(parameterized, name=parameterized.name, **kwargs) if isinstance(self._expand_layout, Tabs): - title = self.object.param[pname].label + title = self.object.param[parameter].label pane = (title, pane) self._expand_layout.append(pane) - def update_pane(change, parameter=pname): + def update_pane(change, parameter=pname, toggle=toggle): "Adds or removes subpanel from layout" layout = self._expand_layout existing = [p for p in layout.objects if isinstance(p, Param) @@ -924,10 +924,10 @@ def update_pane(*events): deps.append(p) self._replace_pane() - for _, params in full_groupby(params, lambda x: (x.inst or x.cls, x.what)): - p = params[0] + for _, sub_params in full_groupby(params, lambda x: (x.inst or x.cls, x.what)): + p = sub_params[0] pobj = (p.inst or p.cls) - ps = [_p.name for _p in params] + ps = [_p.name for _p in sub_params] if isinstance(pobj, Reactive) and self.loading_indicator: props = {p: 'loading' for p in ps if p in pobj._linkable_params} if props: diff --git a/panel/pipeline.py b/panel/pipeline.py index 82e6e1a49ac..99eb896389b 100644 --- a/panel/pipeline.py +++ b/panel/pipeline.py @@ -171,7 +171,7 @@ def __init__(self, stages=[], graph={}, **params): try: import holoviews as hv except Exception: - raise ImportError('Pipeline requires holoviews to be installed') + raise ImportError('Pipeline requires holoviews to be installed') from None super().__init__(**params) diff --git a/panel/reactive.py b/panel/reactive.py index 78d67b4d2e3..de41e4fd273 100644 --- a/panel/reactive.py +++ b/panel/reactive.py @@ -142,7 +142,7 @@ def __init__(self, **params): #---------------------------------------------------------------- @classproperty - @lru_cache(maxsize=None) + @lru_cache(maxsize=None) # noqa: B019 (cls is not an instance) def _property_mapping(cls): rename = {} for scls in cls.__mro__[::-1]: @@ -1372,7 +1372,7 @@ def __init__(mcs, name: str, bases: Tuple[Type, ...], dict_: Mapping[str, Any]): mcs._node_callbacks: Dict[str, List[Tuple[str, str]]] = {} mcs._inline_callbacks = [] for node, attrs in mcs._parser.attrs.items(): - for (attr, parameters, template) in attrs: + for (attr, parameters, _template) in attrs: for p in parameters: if p in mcs.param or '.' in p: continue @@ -1608,7 +1608,7 @@ def _loaded(cls) -> bool: ) def _cleanup(self, root: Model | None = None) -> None: - for child, panes in self._panes.items(): + for _child, panes in self._panes.items(): for pane in panes: pane._cleanup(root) super()._cleanup(root) @@ -1739,7 +1739,7 @@ def _get_children( elif children_param in self._panes: # Find existing models old_panes = self._panes[children_param] - for i, pane in enumerate(child_panes): + for pane in child_panes: if pane in old_panes and root.ref['id'] in pane._models: child, _ = pane._models[root.ref['id']] else: @@ -1768,8 +1768,8 @@ def _get_template(self) -> Tuple[str, List[str], Mapping[str, List[Tuple[str, Li # ${objects[{{ loop.index0 }}]} # {% endfor %} template_string = self._template - for var, obj in self._parser.loop_map.items(): - for var in self._parser.loop_var_map[var]: + for parent_var, obj in self._parser.loop_map.items(): + for var in self._parser.loop_var_map[parent_var]: template_string = template_string.replace( '${%s}' % var, '${%s[{{ loop.index0 }}]}' % obj) @@ -1797,7 +1797,7 @@ def _get_template(self) -> Tuple[str, List[str], Mapping[str, List[Tuple[str, Li f"{type(self).__name__} could not render " f"template, errored with:\n\n{type(e).__name__}: {e}.\n" f"Full template:\n\n{template_string}" - ) + ) from e # Parse templated HTML parser = ReactiveHTMLParser(self.__class__, template=False) diff --git a/panel/template/base.py b/panel/template/base.py index dfdf269303c..199dbebdebe 100644 --- a/panel/template/base.py +++ b/panel/template/base.py @@ -301,7 +301,8 @@ def _repr_mimebundle_( client_comm = state._comm_manager.get_client_comm( on_msg=partial(self._on_msg, ref, manager), on_error=partial(self._on_error, ref), - on_stdout=partial(self._on_stdout, ref) + on_stdout=partial(self._on_stdout, ref), + on_open=lambda _: comm.init() ) manager.client_comm_id = client_comm.id doc.add_root(manager) diff --git a/panel/tests/chat/test_feed.py b/panel/tests/chat/test_feed.py index 2c44c315d3e..f901308b58d 100644 --- a/panel/tests/chat/test_feed.py +++ b/panel/tests/chat/test_feed.py @@ -41,6 +41,16 @@ def test_hide_header(self, chat_feed): chat_feed.header = "" assert chat_feed._card.hide_header + def test_card_params(self, chat_feed): + chat_feed.card_params = { + "header_background": "red", + "header": "Test", + "hide_header": False + } + assert chat_feed._card.header_background == "red" + assert chat_feed._card.header == "Test" + assert not chat_feed._card.hide_header + def test_send(self, chat_feed): message = chat_feed.send("Message") wait_until(lambda: len(chat_feed.objects) == 1) @@ -352,7 +362,9 @@ async def callback(contents, user, instance): } instance.respond() elif user == "arm": - user_entry = instance.objects[-2] + for user_entry in instance.objects: + if user_entry.user == "User": + break user_contents = user_entry.object yield { "user": "leg", @@ -379,6 +391,15 @@ def callback(contents, user, instance): wait_until(lambda: len(chat_feed.objects) == 1) assert chat_feed.objects[0].object == "Mutated" + def test_forward_message_params(self, chat_feed): + chat_feed = ChatFeed(reaction_icons={"like": "thumb-up"}, reactions=["like"]) + chat_feed.send("Hey!") + chat_message = chat_feed.objects[0] + assert chat_feed.message_params == {"reaction_icons": {"like": "thumb-up"}, "reactions": ["like"]} + assert chat_message.object == "Hey!" + assert chat_message.reactions == ["like"] + assert chat_message.reaction_icons.options == {"like": "thumb-up"} + @pytest.mark.xdist_group("chat") class TestChatFeedCallback: @@ -440,8 +461,7 @@ async def echo(contents, user, instance): chat_feed.callback = echo chat_feed.send("Message", respond=True) - await asyncio.sleep(0.5) - assert len(chat_feed.objects) == 2 + await async_wait_until(lambda: len(chat_feed.objects) == 2) assert chat_feed.objects[1].object == "Message" @pytest.mark.parametrize("callback_user", [None, "Bob"]) @@ -490,23 +510,25 @@ async def echo(contents, user, instance): chat_feed.callback = echo chat_feed.send("Message", respond=True) - await asyncio.sleep(0.5) + await async_wait_until(lambda: len(chat_feed.objects) == 2) assert len(chat_feed.objects) == 2 assert chat_feed.objects[1].object == "Message" @pytest.mark.asyncio - async def test_generator(self, chat_feed): + def test_generator(self, chat_feed): async def echo(contents, user, instance): message = "" for char in contents: message += char yield message + assert instance.objects[-1].show_activity_dot chat_feed.callback = echo chat_feed.send("Message", respond=True) - await asyncio.sleep(0.5) + wait_until(lambda: len(chat_feed.objects) == 2) assert len(chat_feed.objects) == 2 assert chat_feed.objects[1].object == "Message" + assert not chat_feed.objects[-1].show_activity_dot @pytest.mark.asyncio async def test_async_generator(self, chat_feed): @@ -519,12 +541,13 @@ async def echo(contents, user, instance): async for char in async_gen(contents): message += char yield message + assert instance.objects[-1].show_activity_dot chat_feed.callback = echo chat_feed.send("Message", respond=True) - await asyncio.sleep(0.5) - assert len(chat_feed.objects) == 2 + await async_wait_until(lambda: len(chat_feed.objects) == 2) assert chat_feed.objects[1].object == "Message" + assert not chat_feed.objects[-1].show_activity_dot def test_placeholder_disabled(self, chat_feed): def echo(contents, user, instance): @@ -583,11 +606,13 @@ async def echo(contents, user, instance): def test_placeholder_threshold_exceed_generator(self, chat_feed): async def echo(contents, user, instance): + assert instance._placeholder not in instance._chat_log await asyncio.sleep(0.5) assert instance._placeholder in instance._chat_log yield "hello testing" + assert instance._placeholder not in instance._chat_log - chat_feed.placeholder_threshold = 0.1 + chat_feed.placeholder_threshold = 0.2 chat_feed.callback = echo chat_feed.send("Message", respond=True) assert chat_feed._placeholder not in chat_feed._chat_log @@ -799,6 +824,16 @@ def custom_serializer(obj): with pytest.raises(ValueError, match="must return a string"): chat_feed.serialize(custom_serializer=custom_serializer) + def test_serialize_filter_by(self, chat_feed): + def filter_by_reactions(messages): + return [obj for obj in messages if "favorite" in obj.reactions] + + chat_feed.send(ChatMessage("no")) + chat_feed.send(ChatMessage("yes", reactions=["favorite"])) + filtered = chat_feed.serialize(filter_by=filter_by_reactions) + assert len(filtered) == 1 + assert filtered[0]["content"] == "yes" + @pytest.mark.xdist_group("chat") class TestChatFeedSerializeBase: diff --git a/panel/tests/conftest.py b/panel/tests/conftest.py index f0aaf56691e..3cf51198dd8 100644 --- a/panel/tests/conftest.py +++ b/panel/tests/conftest.py @@ -114,7 +114,7 @@ def pytest_configure(config): for marker, info in optional_markers.items(): config.addinivalue_line("markers", "{}: {}".format(marker, info['marker-descr'])) - if getattr(config.option, 'jupyter') and not port_open(JUPYTER_PORT): + if config.option.jupyter and not port_open(JUPYTER_PORT): start_jupyter() diff --git a/panel/tests/io/test_cache.py b/panel/tests/io/test_cache.py index a11ea773b4a..589cd27d714 100644 --- a/panel/tests/io/test_cache.py +++ b/panel/tests/io/test_cache.py @@ -3,10 +3,13 @@ import pathlib import time +from collections import Counter + import numpy as np import pandas as pd import param import pytest +import requests try: import diskcache @@ -15,7 +18,8 @@ diskcache_available = pytest.mark.skipif(diskcache is None, reason="requires diskcache") from panel.io.cache import _find_hash_func, cache -from panel.io.state import set_curdoc +from panel.io.state import set_curdoc, state +from panel.tests.util import serve_and_wait ################ # Test hashing # @@ -219,6 +223,26 @@ def test_per_session_cache(document): assert fn(a=0, b=0) == 0 assert fn(a=0, b=0) == 1 +def test_per_session_cache_server(port): + counts = Counter() + + @cache(per_session=True) + def get_data(): + counts[state.curdoc] += 1 + return "Some data" + + def app(): + get_data() + get_data() + return + + serve_and_wait(app, port=port) + + requests.get(f"http://localhost:{port}/") + requests.get(f"http://localhost:{port}/") + + assert list(counts.values()) == [1, 1] + @pytest.mark.xdist_group("cache") @diskcache_available def test_disk_cache(): diff --git a/panel/tests/io/test_state.py b/panel/tests/io/test_state.py index b57e1c0e14f..392c585c5f5 100644 --- a/panel/tests/io/test_state.py +++ b/panel/tests/io/test_state.py @@ -32,7 +32,7 @@ def test_fn(i=[0]): results = [] with ThreadPoolExecutor(max_workers=4) as executor: - for i in range(4): + for _ in range(4): future = executor.submit(state.as_cached, 'test', test_fn) results.append(future) assert [r.result() for r in results] == [1, 1, 1, 1] diff --git a/panel/tests/pane/test_holoviews.py b/panel/tests/pane/test_holoviews.py index 72fe8666061..67dc74c3e8b 100644 --- a/panel/tests/pane/test_holoviews.py +++ b/panel/tests/pane/test_holoviews.py @@ -743,3 +743,37 @@ def test_holoviews_property_override(document, comm): assert model.styles["background"] == 'red' assert model.children[0].css_classes == ['test_class'] + + +@hv_available +def test_holoviews_date_picker_widget(document, comm): + ds = { + "time": [np.datetime64("2000-01-01"), np.datetime64("2000-01-02")], + "x": [0, 1], + "y": [0, 1], + } + viz = hv.Dataset(ds, ["x", "time"], ["y"]) + layout = pn.panel(viz.to( + hv.Scatter, ["x"], ["y"]), widgets={"time": pn.widgets.DatePicker} + ) + widget_box = layout[0][1] + assert isinstance(layout, pn.Row) + assert isinstance(widget_box, pn.WidgetBox) + assert isinstance(widget_box[0], pn.widgets.DatePicker) + + +@hv_available +def test_holoviews_datetime_picker_widget(document, comm): + ds = { + "time": [np.datetime64("2000-01-01"), np.datetime64("2000-01-02")], + "x": [0, 1], + "y": [0, 1], + } + viz = hv.Dataset(ds, ["x", "time"], ["y"]) + layout = pn.panel(viz.to( + hv.Scatter, ["x"], ["y"]), widgets={"time": pn.widgets.DatetimePicker} + ) + widget_box = layout[0][1] + assert isinstance(layout, pn.Row) + assert isinstance(widget_box, pn.WidgetBox) + assert isinstance(widget_box[0], pn.widgets.DatetimePicker) diff --git a/panel/tests/pane/test_plot.py b/panel/tests/pane/test_plot.py index 526a8940bb6..3e7d230c5df 100644 --- a/panel/tests/pane/test_plot.py +++ b/panel/tests/pane/test_plot.py @@ -40,6 +40,19 @@ def test_get_matplotlib_pane_type(): assert PaneBase.get_pane_type(mpl_figure()) is Matplotlib +@mpl_available +def test_matplotlib_pane_initially_empty(document, comm): + pane = pn.pane.Matplotlib() + assert pane.object is None + + model = pane.get_root(document, comm=comm) + assert model.text == '' + + pane.object = mpl_figure() + assert model.text.startswith('<img src="data:image/png;base64,') + assert pane._models[model.ref['id']][0] is model + + @mpl_available def test_matplotlib_pane(document, comm): pane = pn.pane.Matplotlib(mpl_figure()) diff --git a/panel/tests/test_config.py b/panel/tests/test_config.py index 3e723d04402..6b5299c910a 100644 --- a/panel/tests/test_config.py +++ b/panel/tests/test_config.py @@ -105,14 +105,14 @@ def test_console_output_replace_error(document, comm, get_display_handle): handle = get_display_handle(model) try: - 1/0 + 1/0 # noqa: B018 except Exception as e: pane._on_error(model.ref['id'], e) assert 'text/html' in handle assert 'ZeroDivisionError' in handle['text/html'] try: - 1 + '2' + 1 + '2' # noqa: B018 except Exception as e: pane._on_error(model.ref['id'], e) assert 'text/html' in handle @@ -129,14 +129,14 @@ def test_console_output_accumulate_error(document, comm, get_display_handle): handle = get_display_handle(model) try: - 1/0 + 1/0 # noqa: B018 except Exception as e: pane._on_error(model.ref['id'], e) assert 'text/html' in handle assert 'ZeroDivisionError' in handle['text/html'] try: - 1 + '2' + 1 + '2' # noqa: B018 except Exception as e: pane._on_error(model.ref['id'], e) assert 'text/html' in handle @@ -154,7 +154,7 @@ def test_console_output_disable_error(document, comm, get_display_handle): handle = get_display_handle(model) try: - 1/0 + 1/0 # noqa: B018 except Exception as e: pane._on_error(model.ref['id'], e) assert handle == {} diff --git a/panel/tests/test_interact.py b/panel/tests/test_interact.py index d876b6dfab5..88fa921c3f2 100644 --- a/panel/tests/test_interact.py +++ b/panel/tests/test_interact.py @@ -261,8 +261,4 @@ def test_interact_throttled(): for slider, kwargs in slider_dict.items(): widget = getattr(widgets, slider)(**kwargs) - try: - interactive(func, x=widget, throttled=throttled) - assert True - except Exception as e: - assert False, e + interactive(func, x=widget, throttled=throttled) diff --git a/panel/tests/test_server.py b/panel/tests/test_server.py index 0966a985769..8a3367c65af 100644 --- a/panel/tests/test_server.py +++ b/panel/tests/test_server.py @@ -162,7 +162,7 @@ async def task(): curdoc = state.curdoc await asyncio.sleep(0.5) docs[curdoc] = [] - for i in range(5): + for _ in range(5): await asyncio.sleep(0.1) docs[curdoc].append(state.curdoc) @@ -186,7 +186,7 @@ async def task(depth=1): if depth > 0: asyncio.ensure_future(task(depth-1)) docs[curdoc] = [] - for i in range(10): + for _ in range(10): await asyncio.sleep(0.1) docs[curdoc].append(state.curdoc) @@ -511,7 +511,6 @@ def app(): state.onload(cb2) # Simulate rendering def loaded(): - state.curdoc state._schedule_on_load(state.curdoc, None) state.execute(loaded, schedule=True) return 'App' @@ -536,7 +535,6 @@ def app(): state.onload(cb2) # Simulate rendering def loaded(): - state.curdoc state._schedule_on_load(state.curdoc, None) state.execute(loaded, schedule=True) return 'App' diff --git a/panel/tests/ui/io/test_location.py b/panel/tests/ui/io/test_location.py index 4f1b5a17159..b71d91e6675 100644 --- a/panel/tests/ui/io/test_location.py +++ b/panel/tests/ui/io/test_location.py @@ -13,8 +13,8 @@ def verify_document_location(expected_location, page): for param in expected_location: - wait_until(lambda: param in page.evaluate('() => document.location'), page) - wait_until(lambda: page.evaluate('() => document.location')[param] == expected_location[param], page) + wait_until(lambda: param in page.evaluate('() => document.location'), page) # noqa: B023 + wait_until(lambda: page.evaluate('() => document.location')[param] == expected_location[param], page) # noqa: B023 def test_set_url_params_update_document(page): diff --git a/panel/tests/ui/pane/test_plotly.py b/panel/tests/ui/pane/test_plotly.py index c99ba369b83..402e66e2b8b 100644 --- a/panel/tests/ui/pane/test_plotly.py +++ b/panel/tests/ui/pane/test_plotly.py @@ -46,6 +46,22 @@ def plotly_3d_plot(): return plot_3d, title +@pytest.fixture +def plotly_img_plot(): + fig_dict = dict( + data={ + "z": np.random.randint(0, 255, size=(6, 30, 3)).astype(np.uint8), + "type": "image", + }, + layout={ + "width": 300, + "height": 60, + "margin": {"l": 0, "r": 0, "b": 0, "t": 0}, + }, + ) + return Plotly(fig_dict, width=300, height=60) + + def test_plotly_no_console_errors(page, plotly_2d_plot): msgs, _ = serve_component(page, plotly_2d_plot) @@ -54,6 +70,7 @@ def test_plotly_no_console_errors(page, plotly_2d_plot): assert [msg for msg in msgs if msg.type == 'error' and 'favicon' not in msg.location['url']] == [] + def test_plotly_2d_plot(page, plotly_2d_plot): serve_component(page, plotly_2d_plot) @@ -185,3 +202,20 @@ def test_plotly_select_data(page, plotly_2d_plot): assert 'range' in selected assert 'x' in selected['range'] assert 'y' in selected['range'] + + + +def test_plotly_img_plot(page, plotly_img_plot): + msgs, _ = serve_component(page, plotly_img_plot) + + # main pane + plotly_plot = page.locator('.js-plotly-plot .plot-container.plotly') + expect(plotly_plot).to_have_count(1) + + assert [msg for msg in msgs if msg.type == 'error' and 'favicon' not in msg.location['url']] == [] + + # Select and hover on first point + point = plotly_plot.locator('image') + point.hover(force=True) + + wait_until(lambda: plotly_img_plot.hover_data == {'points': [{'curveNumber': 0, 'x': 15, 'y': 3, 'colormodel': 'rgb'}]}, page) diff --git a/panel/tests/ui/widgets/test_icon.py b/panel/tests/ui/widgets/test_icon.py index c3489aa3298..883a165cbb8 100644 --- a/panel/tests/ui/widgets/test_icon.py +++ b/panel/tests/ui/widgets/test_icon.py @@ -8,6 +8,12 @@ pytestmark = pytest.mark.ui +SVG = """ + +""" # noqa: E501 +ACTIVE_SVG = """ + +""" # noqa: E501 def test_toggle_icon_click(page): icon = ToggleIcon() @@ -100,3 +106,70 @@ def cb(event): icon.value = True icon.icon = "heart" assert page.locator('.ti-heart') + + # update active icon_name to svg + icon.active_icon = ACTIVE_SVG + assert page.locator('.icon-tabler-ad-filled') + + +def test_toggle_icon_svg(page): + icon = ToggleIcon(icon=SVG, active_icon=ACTIVE_SVG) + serve_component(page, icon) + + # test defaults + assert icon.icon == SVG + assert not icon.value + assert page.locator('.icon-tabler-ad-off') + + events = [] + def cb(event): + events.append(event) + icon.param.watch(cb, "value") + + # test icon click updates value + page.click('.bk-SVGIcon') + wait_until(lambda: len(events) == 1, page) + assert icon.value + assert page.locator('.icon-tabler-ad-filled') + +def test_toggle_icon_tabler_to_svg(page): + tabler = "ad-off" + + icon = ToggleIcon(icon=tabler, active_icon=ACTIVE_SVG) + serve_component(page, icon) + + # test defaults + assert icon.icon == tabler + assert not icon.value + assert page.locator('.icon-tabler-ad-off') + + events = [] + def cb(event): + events.append(event) + icon.param.watch(cb, "value") + + # test icon click updates value + page.click('.bk-TablerIcon') + wait_until(lambda: len(events) == 1, page) + assert icon.value + assert page.locator('.icon-tabler-ad-filled') + +def test_toggle_icon_svg_to_tabler(page): + icon = ToggleIcon(icon=SVG, active_icon="ad-filled") + serve_component(page, icon) + + # test defaults + assert icon.icon == SVG + assert not icon.value + assert page.locator('.icon-tabler-ad-off') + + events = [] + def cb(event): + events.append(event) + icon.param.watch(cb, "value") + + # test icon click updates value + page.click('.bk-SVGIcon') + wait_until(lambda: len(events) == 1, page) + assert icon.value + assert page.locator('.icon-tabler-ad-filled') diff --git a/panel/tests/ui/widgets/test_input.py b/panel/tests/ui/widgets/test_input.py index 19de96501a0..1436ee71b5a 100644 --- a/panel/tests/ui/widgets/test_input.py +++ b/panel/tests/ui/widgets/test_input.py @@ -1,5 +1,6 @@ import datetime +import numpy as np import pytest pytest.importorskip("playwright") @@ -603,6 +604,34 @@ def test_datetimepicker_remove_value(page, datetime_start_end): wait_until(lambda: datetime_picker_widget.value is None, page) +def test_datetime_picker_start_end_datetime64(page): + datetime_picker_widget = DatetimePicker( + value=datetime.datetime(2021, 3, 2), + start=np.datetime64("2021-03-02"), + end=np.datetime64("2021-03-03") + ) + + serve_component(page, datetime_picker_widget) + + datetime_picker = page.locator('.flatpickr-input') + datetime_picker.dblclick() + + # locate by aria label March 1, 2021 + prev_month_day = page.locator('[aria-label="March 1, 2021"]') + # assert class "flatpickr-day flatpickr-disabled" + assert "flatpickr-disabled" in prev_month_day.get_attribute("class"), "The date should be disabled" + + # locate by aria label March 3, 2021 + next_month_day = page.locator('[aria-label="March 3, 2021"]') + # assert not class "flatpickr-day flatpickr-disabled" + assert "flatpickr-disabled" not in next_month_day.get_attribute("class"), "The date should be enabled" + + # locate by aria label March 4, 2021 + next_next_month_day = page.locator('[aria-label="March 4, 2021"]') + # assert class "flatpickr-day flatpickr-disabled" + assert "flatpickr-disabled" in next_next_month_day.get_attribute("class"), "The date should be disabled" + + def test_text_area_auto_grow_init(page): text_area = TextAreaInput(auto_grow=True, value="1\n2\n3\n4\n") diff --git a/panel/tests/ui/widgets/test_tabulator.py b/panel/tests/ui/widgets/test_tabulator.py index 0604b6f0a64..0c4e21e3edb 100644 --- a/panel/tests/ui/widgets/test_tabulator.py +++ b/panel/tests/ui/widgets/test_tabulator.py @@ -1725,7 +1725,7 @@ def test_tabulator_pagination(page, df_mixed, pagination): counts = count_per_page(len(df_mixed), page_size) i = 0 while True: - wait_until(lambda: widget.page == i + 1, page) + wait_until(lambda: widget.page == i + 1, page) # noqa: B023 rows = page.locator('.tabulator-row') expect(rows).to_have_count(counts[i]) assert page.locator(f'[aria-label="Show Page {i+1}"]').count() == 1 @@ -1828,7 +1828,7 @@ class P(param.Parameterized): p.s = filt_val df_filtered = df_mixed.loc[df_mixed[filt_col] == filt_val, :] - wait_until(lambda: widget.current_view.equals(df_filtered), page) + wait_until(lambda: widget.current_view.equals(df_filtered), page) # noqa: B023 # Check the table has the right number of rows expect(page.locator('.tabulator-row')).to_have_count(len(df_filtered)) @@ -1857,7 +1857,7 @@ def filt_(df, val): w_filter.value = filt_val df_filtered = filt_(df_mixed, filt_val) - wait_until(lambda: widget.current_view.equals(df_filtered), page) + wait_until(lambda: widget.current_view.equals(df_filtered), page) # noqa: B023 # Check the table has the right number of rows expect(page.locator('.tabulator-row')).to_have_count(len(df_filtered)) diff --git a/panel/tests/ui/widgets/test_texteditor.py b/panel/tests/ui/widgets/test_texteditor.py index 86110483bbc..d491b84e23c 100644 --- a/panel/tests/ui/widgets/test_texteditor.py +++ b/panel/tests/ui/widgets/test_texteditor.py @@ -126,4 +126,4 @@ def test_texteditor_regression_click_toolbar_cursor_stays_in_place(page): editor.press('Enter') page.locator('.ql-bold').click() editor.press('B') - wait_until(lambda: widget.value == '

A

B

', page) + wait_until(lambda: widget.value == '

A

B

', page) diff --git a/panel/tests/util.py b/panel/tests/util.py index 86f927e5a8e..c1d0e26b19b 100644 --- a/panel/tests/util.py +++ b/panel/tests/util.py @@ -379,7 +379,7 @@ def wait_for_port(stdout): nbsr = NBSR(stdout) m = None output = [] - for i in range(20): + for _ in range(20): o = nbsr.readline(0.5) if not o: continue diff --git a/panel/tests/widgets/test_icon.py b/panel/tests/widgets/test_icon.py index 3aee458874e..17373a80cd7 100644 --- a/panel/tests/widgets/test_icon.py +++ b/panel/tests/widgets/test_icon.py @@ -20,3 +20,7 @@ def test_custom_values(self): def test_empty_icon(self): with pytest.raises(ValueError, match="The icon parameter must not "): ToggleIcon(icon="") + + def test_icon_svg_empty_active_icon(self): + with pytest.raises(ValueError, match="The active_icon parameter must not "): + ToggleIcon(icon="") diff --git a/panel/tests/widgets/test_tqdm.py b/panel/tests/widgets/test_tqdm.py index d6efa382634..701c0baa87d 100644 --- a/panel/tests/widgets/test_tqdm.py +++ b/panel/tests/widgets/test_tqdm.py @@ -15,7 +15,7 @@ def test_tqdm(): tqdm = Tqdm(layout="row", sizing_mode="stretch_width") - for index in tqdm(range(3)): + for _ in tqdm(range(3)): pass assert tqdm.value == 3 @@ -44,7 +44,7 @@ def test_process_map(): def test_tqdm_leave_false(): tqdm = Tqdm(layout="row", sizing_mode="stretch_width") - for index in tqdm(range(3), leave=False): + for _ in tqdm(range(3), leave=False): pass assert tqdm.value == 0 @@ -55,7 +55,7 @@ def test_tqdm_leave_false(): def test_tqdm_color(): tqdm = Tqdm() - for index in tqdm(range(3), colour='red'): + for _ in tqdm(range(3), colour='red'): pass assert tqdm.text_pane.styles == {'color': 'red'} @@ -65,7 +65,7 @@ def get_tqdm_app(): tqdm = Tqdm(layout="row", sizing_mode="stretch_width") def run(*events): - for index in tqdm(range(10)): + for _ in tqdm(range(10)): time.sleep(0.2) button = pn.widgets.Button(name="Run Loop", button_type="primary") @@ -98,7 +98,7 @@ def get_tqdm_app_simple(): tqdm = Tqdm(layout="row", sizing_mode="stretch_width") def run(*events): - for index in tqdm(range(10)): + for _ in tqdm(range(10)): time.sleep(0.2) button = pn.widgets.Button(name="Run Loop", button_type="primary") diff --git a/panel/theme/base.py b/panel/theme/base.py index cf3b22df576..c3f99707fc3 100644 --- a/panel/theme/base.py +++ b/panel/theme/base.py @@ -246,6 +246,8 @@ def _apply_params(cls, viewable, mref, modifiers, document=None): # this may end up causing issues. from ..io.resources import CDN_DIST, patch_stylesheet + if mref not in viewable._models: + return model, _ = viewable._models[mref] params = { k: v for k, v in modifiers.items() if k != 'children' and diff --git a/panel/util/__init__.py b/panel/util/__init__.py index a95abecc6f0..ff5c4ab850d 100644 --- a/panel/util/__init__.py +++ b/panel/util/__init__.py @@ -23,7 +23,6 @@ from importlib import import_module from typing import Any, AnyStr -import bleach import bokeh import numpy as np import param @@ -51,7 +50,23 @@ PARAM_NAME_PATTERN = re.compile(r'^.*\d{5}$') -HTML_SANITIZER = bleach.sanitizer.Cleaner(strip=True) +class LazyHTMLSanitizer: + """ + Wraps bleach.sanitizer.Cleaner lazily importing it on the first + call to the clean method. + """ + + def __init__(self, **kwargs): + self._cleaner = None + self._kwargs = kwargs + + def clean(self, text): + if self._cleaner is None: + import bleach + self._cleaner = bleach.sanitizer.Cleaner(**self._kwargs) + return self._cleaner.clean(text) + +HTML_SANITIZER = LazyHTMLSanitizer(strip=True) def hashable(x): @@ -87,8 +102,6 @@ def param_name(name: str) -> str: return name[:name.index(match[0])] if match else name - - def abbreviated_repr(value, max_length=25, natural_breaks=(',', ' ')): """ Returns an abbreviated repr for the supplied object. Attempts to @@ -466,3 +479,9 @@ def styler_update(styler, new_df): todo = tuple(ops) todos.append(todo) return todos + + +def try_datetime64_to_datetime(value): + if isinstance(value, np.datetime64): + value = value.astype('datetime64[ms]').astype(datetime) + return value diff --git a/panel/viewable.py b/panel/viewable.py index 082e6709324..1963f65da88 100644 --- a/panel/viewable.py +++ b/panel/viewable.py @@ -511,7 +511,8 @@ def _render_mimebundle(self, model: Model, doc: Document, comm: Comm, location: client_comm = state._comm_manager.get_client_comm( on_msg=functools.partial(self._on_msg, ref, manager), on_error=functools.partial(self._on_error, ref), - on_stdout=functools.partial(self._on_stdout, ref) + on_stdout=functools.partial(self._on_stdout, ref), + on_open=lambda _: comm.init() ) self._comms[ref] = (comm, client_comm) manager.client_comm_id = client_comm.id diff --git a/panel/widgets/icon.py b/panel/widgets/icon.py index df21840e9db..a2d355536d0 100644 --- a/panel/widgets/icon.py +++ b/panel/widgets/icon.py @@ -13,11 +13,11 @@ class ToggleIcon(Widget): active_icon = param.String(default='', doc=""" The name of the icon to display when toggled from - tabler-icons.io](https://tabler-icons.io)/""") + tabler-icons.io](https://tabler-icons.io)/ or an SVG.""") icon = param.String(default='heart', doc=""" The name of the icon to display from - [tabler-icons.io](https://tabler-icons.io)/""") + [tabler-icons.io](https://tabler-icons.io)/ or an SVG.""") size = param.String(default=None, doc=""" An explicit size specified as a CSS font-size, e.g. '1.5em' or '20px'.""") @@ -33,5 +33,12 @@ class ToggleIcon(Widget): def __init__(self, **params): super().__init__(**params) + + @param.depends("icon", "active_icon", watch=True, on_init=True) + def _update_icon(self): if not self.icon: raise ValueError('The icon parameter must not be empty.') + + icon_is_svg = self.icon.startswith(' Type[Model]: try: from bokeh.models import ColorMap except Exception: - raise ImportError('ColorMap widget requires bokeh version >= 3.3.0.') + raise ImportError('ColorMap widget requires bokeh version >= 3.3.0.') from None return ColorMap @param.depends('value_name', watch=True, on_init=True) diff --git a/panel/widgets/slider.py b/panel/widgets/slider.py index 9499211f54c..d7c8ab75ba1 100644 --- a/panel/widgets/slider.py +++ b/panel/widgets/slider.py @@ -780,8 +780,8 @@ class DatetimeRangeSlider(DateRangeSlider): def _widget_type(self): try: from bokeh.models import DatetimeRangeSlider - except Exception: - raise ValueError("DatetimeRangeSlider requires bokeh >= 2.4.3") + except ImportError: + raise ValueError("DatetimeRangeSlider requires bokeh >= 2.4.3") from None return DatetimeRangeSlider diff --git a/panel/widgets/tables.py b/panel/widgets/tables.py index e89f7236fa7..8f7eeca56bc 100644 --- a/panel/widgets/tables.py +++ b/panel/widgets/tables.py @@ -1814,10 +1814,10 @@ def _config_columns(self, column_objs: List[TableColumn]) -> List[Dict[str, Any] col_dict['formatter'] = formatter.pop('type') col_dict['formatterParams'] = formatter title_formatter = self.title_formatters.get(field) - if title_formatter: + if isinstance(title_formatter, str): col_dict['titleFormatter'] = title_formatter elif isinstance(title_formatter, dict): - formatter = dict(title_formatter) + title_formatter = dict(title_formatter) col_dict['titleFormatter'] = title_formatter.pop('type') col_dict['titleFormatterParams'] = title_formatter col_name = self._renamed_cols[field] @@ -1851,7 +1851,7 @@ def _config_columns(self, column_objs: List[TableColumn]) -> List[Dict[str, Any] col_dict['editor'] = 'list' if col_dict.get('editorParams', {}).get('values', False) is True: del col_dict['editorParams']['values'] - col_dict['editorParams']['valuesLookup'] + col_dict['editorParams']['valuesLookup'] = True if field in self.frozen_columns or i in self.frozen_columns: col_dict['frozen'] = True if isinstance(self.widths, dict) and isinstance(self.widths.get(field), str): diff --git a/pyproject.toml b/pyproject.toml index b7155e83241..918a6cf80a9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,6 @@ requires = [ "bokeh >=3.3.0,<3.4.0", "pyviz_comms >=0.7.4", "requests", - "bleach", "packaging", "tqdm >=4.48.0", "markdown", @@ -40,8 +39,10 @@ ignore = [ "E741", "W605", "E701", # Multiple statements on one line + "B006", # Do not use mutable data structures for argument defaults ] select = [ + "B", "E", "F", "W", diff --git a/setup.py b/setup.py index db90e2b05d5..ccde91ae337 100644 --- a/setup.py +++ b/setup.py @@ -213,8 +213,7 @@ def run(self): 'tests': _tests, 'recommended': _recommended, 'doc': _recommended + [ - 'nbsite >=0.8.2', - 'myst-nb >=0.17,<1', + 'nbsite >=0.8.4', 'lxml', 'pandas <2.1.0' # Avoid deprecation warnings ],