From c6505f065346d640f38d95e4e949669112b43ed9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Mon, 21 Oct 2024 11:55:41 +0200 Subject: [PATCH 01/44] Add reference to patch_tabulator (#7423) * add reference to patch_tabulator * Bump pre-commit --------- Co-authored-by: Philipp Rudiger --- .pre-commit-config.yaml | 4 ++-- panel/compiler.py | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 32e84421e8..6d1ae3d01c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,7 +14,7 @@ repos: exclude: \.min\.js$ - id: trailing-whitespace - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.6.9 + rev: v0.7.0 hooks: - id: ruff files: panel/ @@ -47,7 +47,7 @@ repos: - id: oxipng stages: [manual] - repo: https://github.com/pre-commit/mirrors-eslint - rev: v9.12.0 + rev: v9.13.0 hooks: - id: eslint args: ['-c', 'panel/.eslintrc.js', 'panel/*.ts', 'panel/models/**/*.ts', '--fix'] diff --git a/panel/compiler.py b/panel/compiler.py index 108b1b7095..d9152fc9e4 100644 --- a/panel/compiler.py +++ b/panel/compiler.py @@ -349,13 +349,14 @@ def bundle_icons(verbose=False, external=True, download_list=None): shutil.copyfile(icon, dest_dir / os.path.basename(icon)) def patch_tabulator(): - # https://github.com/olifolkerd/tabulator/issues/4421 path = BUNDLE_DIR / 'datatabulator' / 'tabulator-tables@6.3.0' / 'dist' / 'js' / 'tabulator.min.js' text = path.read_text() + # https://github.com/olifolkerd/tabulator/issues/4421 old = '"focus"!==this.options("editTriggerEvent")&&"click"!==this.options("editTriggerEvent")' new = '"click"!==this.options("editTriggerEvent")' assert text.count(old) == 1 text = text.replace(old, new) + # https://github.com/olifolkerd/tabulator/pull/4598 old = '(i=!0,this.subscribed("table-resize")?this.dispatch("table-resize"):this.redraw())' new = '(i=!0,this.redrawing||(this.redrawing=!0,this.subscribed("table-resize")?this.dispatch("table-resize"):this.redraw(),this.redrawing=!1))' assert text.count(old) == 1 From 110117da166b48ddabe4c9285ced301478b8b7e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Tue, 22 Oct 2024 08:55:41 +0200 Subject: [PATCH 02/44] enh: Set Bokeh log level to info when importing dask.distributed (#7426) * enh: Set Bokeh log level to info when importing dask.distributed * Update panel/command/serve.py --- panel/command/serve.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/panel/command/serve.py b/panel/command/serve.py index 35e9a6d16a..ae4fe49838 100644 --- a/panel/command/serve.py +++ b/panel/command/serve.py @@ -670,5 +670,10 @@ def invoke(self, args: argparse.Namespace): # Empty layout are valid and the Bokeh warning is silenced as usually # not relevant to Panel users. silence(EMPTY_LAYOUT, True) + # dask.distributed changes the logging level of Bokeh, we will overwrite it + # if the environment variable is not set to the default Bokeh level + # See https://github.com/holoviz/panel/issues/2302 + if "DASK_DISTRIBUTED__LOGGING__BOKEH" not in os.environ: + os.environ["DASK_DISTRIBUTED__LOGGING__BOKEH"] = "info" args.dev = None super().invoke(args) From 03542a6005e1d4eb7dbf58fd8058257bdcbd9b1c Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Wed, 23 Oct 2024 06:25:26 -0400 Subject: [PATCH 03/44] Ensure Tabulator table content does not overflow (#7425) --- panel/models/tabulator.ts | 7 +++++++ panel/styles/models/tabulator.less | 3 +++ 2 files changed, 10 insertions(+) create mode 100644 panel/styles/models/tabulator.less diff --git a/panel/models/tabulator.ts b/panel/models/tabulator.ts index 3cc745b2a3..51867a87b4 100644 --- a/panel/models/tabulator.ts +++ b/panel/models/tabulator.ts @@ -2,6 +2,7 @@ import {display, undisplay} from "@bokehjs/core/dom" import {sum} from "@bokehjs/core/util/arrayable" import {isArray, isBoolean, isString, isNumber} from "@bokehjs/core/util/types" import {ModelEvent} from "@bokehjs/core/bokeh_events" +import type {StyleSheetLike} from "@bokehjs/core/dom" import {div} from "@bokehjs/core/dom" import {Enum} from "@bokehjs/core/kinds" import type * as p from "@bokehjs/core/properties" @@ -17,6 +18,8 @@ import {transform_cds_to_records} from "./data" import {HTMLBox, HTMLBoxView} from "./layout" import {schedule_when} from "./util" +import tabulator_css from "styles/models/tabulator.css" + export class TableEditEvent extends ModelEvent { constructor(readonly column: string, readonly row: number, readonly pre: boolean) { super() @@ -547,6 +550,10 @@ export class DataTabulatorView extends HTMLBoxView { this.restore_scroll() } + override stylesheets(): StyleSheetLike[] { + return [...super.stylesheets(), tabulator_css] + } + setCSSClasses(el: HTMLDivElement): void { el.className = "pnx-tabulator tabulator" for (const cls of this.model.theme_classes) { diff --git a/panel/styles/models/tabulator.less b/panel/styles/models/tabulator.less new file mode 100644 index 0000000000..f82ea6ab4a --- /dev/null +++ b/panel/styles/models/tabulator.less @@ -0,0 +1,3 @@ +.tabulator-table { + max-width: 100% +} From 12df5209429ca0a021de45138d67bcdceae754e6 Mon Sep 17 00:00:00 2001 From: Maxime Liquet <35924738+maximlt@users.noreply.github.com> Date: Wed, 23 Oct 2024 12:49:24 +0200 Subject: [PATCH 04/44] Tabulator: ensure markup panes wrap text in row_content (#7431) --- panel/styles/models/tabulator.less | 6 +++++- panel/tests/ui/widgets/test_tabulator.py | 15 +++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/panel/styles/models/tabulator.less b/panel/styles/models/tabulator.less index f82ea6ab4a..04c5dc6c46 100644 --- a/panel/styles/models/tabulator.less +++ b/panel/styles/models/tabulator.less @@ -1,3 +1,7 @@ .tabulator-table { - max-width: 100% + max-width: 100%; + + .tabulator-row .row-content .bk-panel-models-markup-HTML { + white-space: normal; + } } diff --git a/panel/tests/ui/widgets/test_tabulator.py b/panel/tests/ui/widgets/test_tabulator.py index 8663a597f2..c7643ed344 100644 --- a/panel/tests/ui/widgets/test_tabulator.py +++ b/panel/tests/ui/widgets/test_tabulator.py @@ -23,6 +23,7 @@ from panel.io.state import state from panel.layout.base import Column from panel.models.tabulator import _TABULATOR_THEMES_MAPPING +from panel.pane import Markdown from panel.tests.util import get_ctrl_modifier, serve_component, wait_until from panel.util import BOKEH_GE_3_6 from panel.widgets import Select, Tabulator, TextInput @@ -4079,3 +4080,17 @@ def test_tabulator_header_tooltips(page): page.wait_for_timeout(200) expect(page.locator('.tabulator-tooltip')).to_have_text("Test") + + +def test_tabulator_row_content_markup_wrap(page): + # https://github.com/holoviz/panel/issues/7388 + + df = pd.DataFrame({"col": ["foo"]}) + long_markdown = Markdown("xxxx " * 50) + widget = Tabulator(df, row_content=lambda row: long_markdown, expanded=[0], width=200) + + serve_component(page, widget) + + md = page.locator('.row-content .bk-panel-models-markup-HTML') + + assert md.bounding_box()['height'] >= 130 From 7f850dc3c57a516d4c02656675e884a5b1ddd930 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Wed, 23 Oct 2024 12:52:10 +0200 Subject: [PATCH 05/44] Bump panel.js to 1.5.3 --- CHANGELOG.md | 2 ++ doc/about/releases.md | 1 + panel/package-lock.json | 4 ++-- panel/package.json | 2 +- pyproject.toml | 4 ++-- 5 files changed, 8 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b25aac1b1..5917acf639 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,8 @@ This release fixes a number of smaller regressions related to `Tabulator` `row_c - Prevent `Tabulator` from overlapping when `max_height` is set ([#7403](https://github.com/holoviz/panel/pull/7403)) - Do not mutate layout `Children` inplace ([#7417](https://github.com/holoviz/panel/pull/7403)) - Set `Tabulator` null formatter to empty string ([#7421](https://github.com/holoviz/panel/pull/7421)) +- Ensure Tabulator table content does not overflow ([#7425](https://github.com/holoviz/panel/pull/7425)) + ### Compatibility diff --git a/doc/about/releases.md b/doc/about/releases.md index 1972d30591..e7e919821f 100644 --- a/doc/about/releases.md +++ b/doc/about/releases.md @@ -22,6 +22,7 @@ This release fixes a number of smaller regressions related to `Tabulator` `row_c - Prevent `Tabulator` from overlapping when `max_height` is set ([#7403](https://github.com/holoviz/panel/pull/7403)) - Do not mutate layout `Children` inplace ([#7417](https://github.com/holoviz/panel/pull/7403)) - Set `Tabulator` null formatter to empty string ([#7421](https://github.com/holoviz/panel/pull/7421)) +- Ensure Tabulator table content does not overflow ([#7425](https://github.com/holoviz/panel/pull/7425), [#7431](https://github.com/holoviz/panel/pull/7431)) ### Compatibility diff --git a/panel/package-lock.json b/panel/package-lock.json index bfbae0483d..1ef564d22f 100644 --- a/panel/package-lock.json +++ b/panel/package-lock.json @@ -1,12 +1,12 @@ { "name": "@holoviz/panel", - "version": "1.5.3-b.1", + "version": "1.5.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@holoviz/panel", - "version": "1.5.3-b.1", + "version": "1.5.3", "license": "BSD-3-Clause", "dependencies": { "@bokeh/bokehjs": "3.6.0", diff --git a/panel/package.json b/panel/package.json index 248b178691..2d0de61073 100644 --- a/panel/package.json +++ b/panel/package.json @@ -1,6 +1,6 @@ { "name": "@holoviz/panel", - "version": "1.5.3-b.1", + "version": "1.5.3", "description": "The powerful data exploration & web app framework for Python.", "license": "BSD-3-Clause", "repository": { diff --git a/pyproject.toml b/pyproject.toml index 3138f65395..64b76cd384 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -71,13 +71,13 @@ HoloViz = "https://holoviz.org/" [project.optional-dependencies] recommended = [ 'jupyterlab', - 'holoviews >=1.16.0', + 'holoviews >=1.18.0', 'matplotlib', 'pillow', 'plotly', ] fastapi = [ - 'bokeh-fastapi >= 0.1.0', + 'bokeh-fastapi >= 0.1.2', 'fastapi[standard]', ] dev = [ From 4d4cd6b6f6648535a4bbff7af23489e8e2a179ee Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Wed, 23 Oct 2024 08:11:32 -0400 Subject: [PATCH 06/44] Check whether property value has changed after transform (#7432) * Check whether property value has changed after transform * Add test --- panel/reactive.py | 30 +++++++++++++++++++++++------- panel/tests/test_custom.py | 21 +++++++++++++++++++++ 2 files changed, 44 insertions(+), 7 deletions(-) diff --git a/panel/reactive.py b/panel/reactive.py index 81b0a076ba..6d1e96a780 100644 --- a/panel/reactive.py +++ b/panel/reactive.py @@ -1582,16 +1582,32 @@ def _process_param_change(self, params): def _set_on_model(self, msg: Mapping[str, Any], root: Model, model: Model) -> None: if not msg: return - old = self._changing.get(root.ref['id'], []) - self._changing[root.ref['id']] = [ - attr for attr, value in msg.items() - if not model.lookup(attr).property.matches(getattr(model, attr), value) - ] + prev_changing = self._changing.get(root.ref['id'], []) + changing = [] + transformed = {} + for attr, value in msg.items(): + prop = model.lookup(attr).property + old = getattr(model, attr) + try: + matches = bool(prop.matches(old, value)) + except Exception: + for tp, converter in prop.alternatives: + if tp.is_valid(value): + value = converter(value) + break + try: + matches = bool(prop.matches(old, value)) + except Exception: + matches = False + if not matches: + transformed[attr] = value + changing.append(attr) + self._changing[root.ref['id']] = changing try: - model.update(**msg) + model.update(**transformed) finally: if old: - self._changing[root.ref['id']] = old + self._changing[root.ref['id']] = prev_changing else: del self._changing[root.ref['id']] diff --git a/panel/tests/test_custom.py b/panel/tests/test_custom.py index 45ef95e0ae..97e32a1ea3 100644 --- a/panel/tests/test_custom.py +++ b/panel/tests/test_custom.py @@ -1,3 +1,5 @@ +import numpy as np +import pandas as pd import param from panel.custom import PyComponent, ReactiveESM @@ -44,6 +46,25 @@ def test_py_component_cleanup(document, comm): assert not spy._view__._models +class ESMDataFrame(ReactiveESM): + + df = param.DataFrame() + + +def test_reactive_esm_sync_dataframe(document, comm): + esm_df = ESMDataFrame() + + model = esm_df.get_root(document, comm) + + esm_df.df = pd.DataFrame({"1": [2]}) + + assert isinstance(model.data.df, dict) + assert len(model.data.df) == 2 + expected = {"index": np.array([0]), "1": np.array([2])} + for col, values in model.data.df.items(): + np.testing.assert_array_equal(values, expected.get(col)) + + class ESMWithChildren(ReactiveESM): child = param.ClassSelector(class_=Viewable) From 26c2e7af922392c647dff9ff46cdd4504cd958b9 Mon Sep 17 00:00:00 2001 From: Marc Skov Madsen Date: Thu, 24 Oct 2024 13:21:12 +0200 Subject: [PATCH 07/44] Improve preview error handling (#7434) * improve preview error handling * review feedback --- panel/io/jupyter_server_extension.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/panel/io/jupyter_server_extension.py b/panel/io/jupyter_server_extension.py index d11a74e1a7..1c8a7a1f5a 100644 --- a/panel/io/jupyter_server_extension.py +++ b/panel/io/jupyter_server_extension.py @@ -389,6 +389,9 @@ async def open(self, path, *args, **kwargs) -> None: self.session_id = get_session_id(token) if self.session_id not in state._kernels: self.close() + msg = f"Session ID '{self.session_id}' does not correspond to any active kernel." + raise RuntimeError(msg) + kernel_info = state._kernels[self.session_id] self.kernel, self.comm_id, self.kernel_id, _ = kernel_info state._kernels[self.session_id] = kernel_info[:-1] + (True,) From d4e02f45473ac383be5be57f0f3e603312f4ccff Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Sun, 27 Oct 2024 12:00:34 +0100 Subject: [PATCH 08/44] Ensure ESM compilation correctly detects file extension (#7446) * Ensure ESM compilation correctly detects file extension * Treat .js files as .jsx when using ReactComponent --- panel/io/compile.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/panel/io/compile.py b/panel/io/compile.py index 0df128571a..d69211d306 100644 --- a/panel/io/compile.py +++ b/panel/io/compile.py @@ -297,7 +297,7 @@ def generate_project( name = component.__name__ esm_path = component._esm_path(compiled=False) if esm_path: - ext = esm_path.suffix + ext = esm_path.suffix.lstrip('.') else: ext = 'jsx' if issubclass(component, ReactComponent) else 'js' code, component_deps = extract_dependencies(component) @@ -391,6 +391,8 @@ def compile_components( print(f"An error occurred while running npm install:\n{RED}{e.stderr}{RESET}") # noqa return None + if any(issubclass(c, ReactComponent) for c in components): + extra_args.append('--loader:.js=jsx') if minify: extra_args.append('--minify') if out: From 8dd362f84fec362ae41df56624aa8b7fd0462584 Mon Sep 17 00:00:00 2001 From: Andrew <15331990+ahuang11@users.noreply.github.com> Date: Mon, 28 Oct 2024 02:44:27 -0700 Subject: [PATCH 09/44] Replace asyncio.sleep with time.sleep in ChatFeed docs (#7439) * Remove asyncio sleep * use time instead of asyncio --- examples/reference/chat/ChatFeed.ipynb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/reference/chat/ChatFeed.ipynb b/examples/reference/chat/ChatFeed.ipynb index bf117aeb7c..fbc8bd0ab7 100644 --- a/examples/reference/chat/ChatFeed.ipynb +++ b/examples/reference/chat/ChatFeed.ipynb @@ -701,7 +701,7 @@ "source": [ "with chat_feed.add_step(title=\"Execute the plan\", status=\"running\") as step:\n", " step.stream(\"\\n\\n...Executing plan...\")\n", - " await asyncio.sleep(1)\n", + " time.sleep(1)\n", " step.stream(\"\\n\\n...Handing over to SQL Agent\")" ] }, @@ -720,7 +720,7 @@ "source": [ "with chat_feed.add_step(title=\"Running SQL query\", user='SQL Agent') as step:\n", " step.stream('Querying...')\n", - " await asyncio.sleep(1)\n", + " time.sleep(1)\n", " step.stream('\\nSELECT * FROM TABLE')" ] }, From dc2fce3fc0b9e0001c3a1d0b4c4e8519a282068a Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Thu, 31 Oct 2024 10:33:06 +0100 Subject: [PATCH 10/44] Add copy button to Markdown codeblocks (#7451) --- panel/.eslintrc.js | 3 +-- panel/models/html.ts | 27 +++++++++++++++++++++++++++ panel/styles/models/html.less | 33 +++++++++++++++++++++++++++++++++ panel/theme/css/bootstrap.css | 8 ++++++-- panel/theme/css/fast.css | 8 ++++++-- panel/theme/css/material.css | 7 ++++++- panel/theme/css/native.css | 13 +++++++++---- 7 files changed, 88 insertions(+), 11 deletions(-) create mode 100644 panel/styles/models/html.less diff --git a/panel/.eslintrc.js b/panel/.eslintrc.js index abebdc905b..40f4c7ddce 100644 --- a/panel/.eslintrc.js +++ b/panel/.eslintrc.js @@ -85,7 +85,6 @@ module.exports = { "no-floating-decimal": ["error"], "no-multiple-empty-lines": ["error", {"max": 1, "maxBOF": 0, "maxEOF": 0}], "no-new-wrappers": "error", - "no-template-curly-in-string": "error", "no-throw-literal": "error", "no-trailing-spaces": ["error"], "no-var": "error", @@ -116,7 +115,7 @@ module.exports = { "overrides": {}, }], "guard-for-in": ["warn"], - "quotes": ["error", "double", {"avoidEscape": true, "allowTemplateLiterals": false}], + "quotes": ["error", "double", {"avoidEscape": true, "allowTemplateLiterals": true}], "curly": ["error", "all"], "prefer-template": ["error"], "generator-star-spacing": ["error", { diff --git a/panel/models/html.ts b/panel/models/html.ts index 53d7bc1fb1..a844b00fe9 100644 --- a/panel/models/html.ts +++ b/panel/models/html.ts @@ -1,4 +1,5 @@ import {ModelEvent, server_event} from "@bokehjs/core/bokeh_events" +import type {StyleSheetLike} from "@bokehjs/core/dom" import type * as p from "@bokehjs/core/properties" import type {Attrs, Dict} from "@bokehjs/core/types" import {entries} from "@bokehjs/core/util/object" @@ -6,6 +7,12 @@ import {Markup} from "@bokehjs/models/widgets/markup" import {PanelMarkupView} from "./layout" import {serializeEvent} from "./event-to-object" +import html_css from "styles/models/html.css" + +const COPY_ICON = `` + +const CHECK_ICON = `` + function searchAllDOMs(node: Element | ShadowRoot, selector: string): (Element | ShadowRoot)[] { let found: (Element | ShadowRoot)[] = [] if (node instanceof Element && node.matches(selector)) { @@ -134,6 +141,10 @@ export class HTMLView extends PanelMarkupView { }) } + override stylesheets(): StyleSheetLike[] { + return [...super.stylesheets(), html_css] + } + protected rerender() { this.render() this.invalidate_layout() @@ -148,6 +159,22 @@ export class HTMLView extends PanelMarkupView { run_scripts(this.container) } this._setup_event_listeners() + for (const codeblock of this.container.querySelectorAll(".codehilite")) { + const copy_button = document.createElement("button") + const pre = (codeblock.children[0] as HTMLPreElement) + copy_button.className = "copybtn" + copy_button.innerHTML = COPY_ICON + copy_button.addEventListener("click", () => { + const code = pre.innerText + navigator.clipboard.writeText(code).then(() => { + copy_button.innerHTML = CHECK_ICON + setTimeout(() => { + copy_button.innerHTML = COPY_ICON + }, 300) + }) + }) + codeblock.insertBefore(copy_button, pre) + } for (const anchor of this.container.querySelectorAll("a")) { const link = anchor.getAttribute("href") if (link && link.startsWith("#")) { diff --git a/panel/styles/models/html.less b/panel/styles/models/html.less new file mode 100644 index 0000000000..31a7fed788 --- /dev/null +++ b/panel/styles/models/html.less @@ -0,0 +1,33 @@ +.copybtn { + position: sticky; + display: flex; + top: 0; + left: 100%; + width: 1.7em; + height: 1.7em; + opacity: 0; + transition: opacity 0.3s, border .3s, background-color .3s; + user-select: none; + padding: 0; + border: none; + outline: none; + border-radius: 0.4em; + border: #1b1f2426 1px solid; + background-color: #f6f8fa; + color: #57606a; +} + +.codehilite pre { + margin-top: -1.7em; +} + +.copybtn svg { + stroke: currentColor; + width: 1.5em; + height: 1.5em; + padding: 0.1em; +} + +.codehilite:hover .copybtn { + opacity: 1; +} diff --git a/panel/theme/css/bootstrap.css b/panel/theme/css/bootstrap.css index 6a7bdbac5c..1792eddb85 100644 --- a/panel/theme/css/bootstrap.css +++ b/panel/theme/css/bootstrap.css @@ -869,15 +869,19 @@ div .tabulator .tabulator-header .tabulator-col { } /* Quill editor */ - .ql-editor, .ql-editor.ql-blank::before { color: var(--bs-body-color); } /* Float panel */ - .jsPanel .jsPanel-content { background-color: var(--bs-body-bg); color: var(--bs-body-color); } + +/* Copy Button */ +.copybtn { + background-color: var(--bs-body-bg); + color: var(--bs-body-color); +} diff --git a/panel/theme/css/fast.css b/panel/theme/css/fast.css index 232d482b2d..62997c4b23 100644 --- a/panel/theme/css/fast.css +++ b/panel/theme/css/fast.css @@ -1107,7 +1107,6 @@ table.panel-df { } /* Quill editor */ - .ql-editor { color: var(--neutral-foreground-rest); } @@ -1117,8 +1116,13 @@ table.panel-df { } /* Float panel */ - .jsPanel .jsPanel-content { background-color: var(--background-color); color: var(--neutral-foreground-rest); } + +/* Copy Button */ +.copybtn { + background-color: var(--neutral-fill-input-rest); + color: var(--neutral-foreground-rest); +} diff --git a/panel/theme/css/material.css b/panel/theme/css/material.css index 42d79fd9b3..5bca10494d 100644 --- a/panel/theme/css/material.css +++ b/panel/theme/css/material.css @@ -582,8 +582,13 @@ div .tabulator .tabulator-header .tabulator-col { } /* Float panel */ - .jsPanel .jsPanel-content { background-color: var(--mdc-theme-background); color: var(--mdc-theme-on-background); } + +/* Copy Button */ +.copybtn { + background-color: var(--mdc-theme-background); + color: var(--mdc-theme-on-background); +} diff --git a/panel/theme/css/native.css b/panel/theme/css/native.css index e46a86109a..d1a2ac0873 100644 --- a/panel/theme/css/native.css +++ b/panel/theme/css/native.css @@ -44,14 +44,12 @@ } /* Help Icon */ - .bk-description > .bk-icon { background-color: var(--background-text-color); opacity: 0.4; } /* Input widgets */ - textarea.bk-input { padding: 0.5em var(--padding-horizontal); } @@ -68,7 +66,6 @@ textarea.bk-input { } /* Quill editor */ - .ql-editor, .ql-editor.ql-blank::before { color: var(--background-text-color); @@ -90,8 +87,16 @@ textarea.bk-input { } /* Float panel */ - .jsPanel .jsPanel-content { background-color: var(--background-color); color: var(--background-text-color); } + +/* Copy Button */ +.copybtn { + background-color: #bbb; +} + +.copybtn:active { + background-color: #bbb; +} From f71258775c4f60473dd2cd0352d619edd028940f Mon Sep 17 00:00:00 2001 From: thuydotm Date: Thu, 31 Oct 2024 16:54:38 +0700 Subject: [PATCH 11/44] Add DatetimeSlider widget (#7374) --- .../reference/widgets/DatetimeSlider.ipynb | 102 +++++++++++++++ panel/models/__init__.py | 1 + panel/models/datetime_slider.py | 10 ++ panel/models/datetime_slider.ts | 56 +++++++++ panel/models/index.ts | 1 + panel/tests/widgets/test_slider.py | 118 +++++++++++++++++- panel/widgets/__init__.py | 7 +- panel/widgets/slider.py | 31 +++++ 8 files changed, 320 insertions(+), 6 deletions(-) create mode 100644 examples/reference/widgets/DatetimeSlider.ipynb create mode 100644 panel/models/datetime_slider.py create mode 100644 panel/models/datetime_slider.ts diff --git a/examples/reference/widgets/DatetimeSlider.ipynb b/examples/reference/widgets/DatetimeSlider.ipynb new file mode 100644 index 0000000000..1be9d8de08 --- /dev/null +++ b/examples/reference/widgets/DatetimeSlider.ipynb @@ -0,0 +1,102 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import datetime as dt\n", + "import panel as pn\n", + "\n", + "pn.extension()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The ``DatetimeSlider`` widget allows selecting a datetime value within a set bounds using a slider.\n", + "\n", + "Discover more on using widgets to add interactivity to your applications in the [how-to guides on interactivity](../../how_to/interactivity/index.md). Alternatively, learn [how to set up callbacks and (JS-)links between parameters](../../how_to/links/index.md) or [how to use them as part of declarative UIs with Param](../../how_to/param/index.md).\n", + "\n", + "#### Parameters:\n", + "\n", + "For details on other options for customizing the component see the [layout](../../how_to/layout/index.md) and [styling](../../how_to/styling/index.md) how-to guides.\n", + "\n", + "##### Core\n", + "\n", + "* **``start``** (date or datetime): The range's lower bound\n", + "* **``end``** (date or datetime): The range's upper bound\n", + "* **``value``** (date or datetime): The selected value as a datetime type\n", + "* **``value_throttled``** (datetime): The selected value as a datetime type throttled until mouseup\n", + "* **``step``** (number): The selected step of the slider in seconds, default is 1 minutes, i.e 60 seconds\n", + "\n", + "##### Display\n", + "\n", + "* **``bar_color``** (color): Color of the slider bar as a hexadecimal RGB value\n", + "* **``direction``** (str): Whether the slider should go from left to right ('ltr') or right to left ('rtl')\n", + "* **``disabled``** (boolean): Whether the widget is editable\n", + "* **``name``** (str): The title of the widget\n", + "* **``orientation``** (str): Whether the slider should be displayed in a 'horizontal' or 'vertical' orientation.\n", + "* **``tooltips``** (boolean): Whether to display tooltips on the slider handle\n", + "* **``format``** (string): The datetime's format\n", + "\n", + "___" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "datetime_slider = pn.widgets.DatetimeSlider(name='Datetime Slider', start=dt.datetime(2019, 1, 1), end=dt.datetime(2019, 6, 1), value=dt.datetime(2019, 2, 8, 15, 40, 30))\n", + "\n", + "datetime_slider" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "``DatetimeSlider.value`` returns a datetime type that can be read out or set like other widgets:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "datetime_slider.value" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Controls\n", + "\n", + "The `DatetimeSlider` widget exposes a number of options which can be changed from both Python and Javascript. Try out the effect of these parameters interactively:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "pn.Row(datetime_slider.controls(jslink=True), datetime_slider)" + ] + } + ], + "metadata": { + "language_info": { + "name": "python", + "pygments_lexer": "ipython3" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/panel/models/__init__.py b/panel/models/__init__.py index a5616ffbfb..90750473b1 100644 --- a/panel/models/__init__.py +++ b/panel/models/__init__.py @@ -6,6 +6,7 @@ """ from .browser import BrowserInfo # noqa from .datetime_picker import DatetimePicker # noqa +from .datetime_slider import DatetimeSlider # noqa from .esm import AnyWidgetComponent, ReactComponent, ReactiveESM # noqa from .feed import Feed # noqa from .icon import ButtonIcon, ToggleIcon, _ClickableIcon # noqa diff --git a/panel/models/datetime_slider.py b/panel/models/datetime_slider.py new file mode 100644 index 0000000000..665e6519b3 --- /dev/null +++ b/panel/models/datetime_slider.py @@ -0,0 +1,10 @@ +from bokeh.core.properties import Override +from bokeh.models.widgets.sliders import DateSlider + + +class DatetimeSlider(DateSlider): + """ Slider-based datetime selection widget. """ + + step = Override(default=60) + + format = Override(default="%d %b %Y %H:%M:%S") diff --git a/panel/models/datetime_slider.ts b/panel/models/datetime_slider.ts new file mode 100644 index 0000000000..0fee835f00 --- /dev/null +++ b/panel/models/datetime_slider.ts @@ -0,0 +1,56 @@ +// adapted from bokeh +// https://github.com/bokeh/bokeh/blob/branch-3.7/bokehjs/src/lib/models/widgets/sliders/date_slider.ts + +import {DEFAULT_FORMATTERS} from "@bokehjs/core/util/templating" +import type {SliderSpec} from "@bokehjs/models/widgets/sliders/abstract_slider" +import {NumericalSlider, NumericalSliderView} from "@bokehjs/models/widgets/sliders/numerical_slider" +import type {TickFormatter} from "@bokehjs/models/formatters/tick_formatter" +import type * as p from "@bokehjs/core/properties" +import {isString} from "@bokehjs/core/util/types" + +export class DatetimeSliderView extends NumericalSliderView { + declare model: DatetimeSlider + + override behaviour = "tap" as const + override connected = [true, false] + + protected override _calc_to(): SliderSpec { + const spec = super._calc_to() + spec.step *= 1_000 // step size is in seconds + return spec + } + + protected _formatter(value: number, format: string | TickFormatter): string { + if (isString(format)) { + return DEFAULT_FORMATTERS.datetime(value, format, {}) + } else { + return format.compute(value) + } + } +} + +export namespace DatetimeSlider { + export type Attrs = p.AttrsOf + export type Props = NumericalSlider.Props +} + +export interface DatetimeSlider extends DatetimeSlider.Attrs {} + +export class DatetimeSlider extends NumericalSlider { + declare properties: DatetimeSlider.Props + declare __view_type__: DatetimeSliderView + + constructor(attrs?: Partial) { + super(attrs) + } + static override __module__ = "panel.models.datetime_slider" + + static { + this.prototype.default_view = DatetimeSliderView + + this.override({ + step: 60, + format: "%d %b %Y %H:%M:%S", + }) + } +} diff --git a/panel/models/index.ts b/panel/models/index.ts index 013635d924..cdcea2da47 100644 --- a/panel/models/index.ts +++ b/panel/models/index.ts @@ -14,6 +14,7 @@ export {CustomSelect} from "./customselect" export {CustomMultiSelect} from "./multiselect" export {DataTabulator} from "./tabulator" export {DatetimePicker} from "./datetime_picker" +export {DatetimeSlider} from "./datetime_slider" export {DeckGLPlot} from "./deckgl" export {DiscretePlayer} from "./discrete_player" export {ECharts} from "./echarts" diff --git a/panel/tests/widgets/test_slider.py b/panel/tests/widgets/test_slider.py index 6147c03d9f..a588805b5a 100644 --- a/panel/tests/widgets/test_slider.py +++ b/panel/tests/widgets/test_slider.py @@ -1,5 +1,6 @@ from datetime import date, datetime +import numpy as np import pytest from bokeh.models import ( @@ -8,9 +9,9 @@ from panel import config from panel.widgets import ( - DateRangeSlider, DateSlider, DatetimeRangeSlider, DiscreteSlider, - EditableFloatSlider, EditableIntSlider, EditableRangeSlider, FloatSlider, - IntSlider, RangeSlider, StaticText, + DateRangeSlider, DateSlider, DatetimeRangeSlider, DatetimeSlider, + DiscreteSlider, EditableFloatSlider, EditableIntSlider, + EditableRangeSlider, FloatSlider, IntSlider, RangeSlider, StaticText, ) @@ -156,6 +157,117 @@ def test_date_slider(document, comm): assert widget.value == 1620777600000 +@pytest.mark.parametrize("start", [date(2018, 9, 1), datetime(2018, 9, 1)]) +@pytest.mark.parametrize("end", [date(2018, 9, 10), datetime(2018, 9, 10)]) +@pytest.mark.parametrize("value", [date(2018, 9, 4), datetime(2018, 9, 4)]) +def test_datetime_slider(document, comm, value, start, end): + datetime_slider = DatetimeSlider( + name='DatetimeSlider', + value=value, + start=start, + end=end, + ) + assert datetime_slider.start == start + assert datetime_slider.end == end + assert datetime_slider.value == value + + widget = datetime_slider.get_root(document, comm=comm) + + assert isinstance(widget, datetime_slider._widget_type) + assert widget.title == 'DatetimeSlider' + assert widget.value == 1536019200000 + assert widget.start == 1535760000000.0 + assert widget.end == 1536537600000.0 + + epoch = datetime(1970, 1, 1) + epoch_time = lambda dt: (dt - epoch).total_seconds() * 1000 + widget.value = epoch_time(datetime(2018, 9, 3)) + datetime_slider._process_events({'value': widget.value}) + assert datetime_slider.value == datetime(2018, 9, 3) + datetime_slider._process_events({'value_throttled': epoch_time(datetime(2018, 9, 3))}) + assert datetime_slider.value_throttled == datetime(2018, 9, 3) + + # Test raw timestamp value: + datetime_slider._process_events({'value': epoch_time(datetime(2018, 9, 4))}) + assert datetime_slider.value == datetime(2018, 9, 4) + datetime_slider._process_events({'value_throttled': epoch_time(datetime(2018, 9, 4))}) + assert datetime_slider.value_throttled == datetime(2018, 9, 4) + + datetime_slider.value = datetime(2018, 9, 6) + assert widget.value == 1536192000000 + + # Testing throttled mode + with config.set(throttled=True): + datetime_slider._process_events({'value': epoch_time(datetime(2021, 5, 15))}) + assert datetime_slider.value == datetime(2018, 9, 6) # no change + datetime_slider._process_events({'value_throttled': epoch_time(datetime(2021, 5, 15))}) + assert datetime_slider.value == datetime(2021, 5, 15) + + datetime_slider.value = datetime(2021, 5, 12) + assert widget.value == 1620777600000 + + +def test_datetime_slider_np_datetime64(document, comm): + start = np.datetime64('2018-09-01') + end = np.datetime64('2018-09-10') + value = np.datetime64('2018-09-04') + + datetime_slider = DatetimeSlider( + name='DatetimeSlider', + value=value, + start=start, + end=end, + ) + assert datetime_slider.start == start + assert datetime_slider.end == end + assert datetime_slider.value == value + + widget = datetime_slider.get_root(document, comm=comm) + + assert isinstance(widget, datetime_slider._widget_type) + assert widget.title == 'DatetimeSlider' + assert widget.value == value + assert widget.start == start + assert widget.end == end + + widget.value = np.datetime64('2018-09-03') + datetime_slider._process_events({'value': widget.value}) + assert datetime_slider.value == np.datetime64('2018-09-03') + datetime_slider._process_events({'value_throttled': np.datetime64('2018-09-03')}) + assert datetime_slider.value_throttled == np.datetime64('2018-09-03') + + # Test raw timestamp value: + datetime_slider._process_events({'value': np.datetime64('2018-09-04')}) + assert datetime_slider.value == np.datetime64('2018-09-04') + datetime_slider._process_events({'value_throttled': np.datetime64('2018-09-04')}) + assert datetime_slider.value_throttled == np.datetime64('2018-09-04') + + datetime_slider.value = np.datetime64('2018-09-06') + assert widget.value == np.datetime64('2018-09-06') + + # Testing throttled mode + with config.set(throttled=True): + datetime_slider._process_events({'value': np.datetime64('2021-05-15')}) + assert datetime_slider.value == np.datetime64('2018-09-06') # no change + datetime_slider._process_events({'value_throttled': np.datetime64('2021-05-15')}) + assert datetime_slider.value == np.datetime64('2021-05-15') + + datetime_slider.value = np.datetime64('2021-05-12') + assert widget.value == np.datetime64('2021-05-12') + + +def test_datetime_slider_param_as_datetime_is_readonly(): + assert DatetimeSlider.param.as_datetime.readonly + assert DatetimeSlider().param.as_datetime.readonly + datetime_slider = DatetimeSlider( + name='DatetimeSlider', + value=datetime(2018, 9, 4), + start=datetime(2018, 9, 1), + end=datetime(2018, 9, 10), + ) + assert datetime_slider.param.as_datetime.readonly + + def test_date_range_slider(document, comm): date_slider = DateRangeSlider(name='DateRangeSlider', value=(datetime(2018, 9, 2), datetime(2018, 9, 4)), diff --git a/panel/widgets/__init__.py b/panel/widgets/__init__.py index 02d8180038..b805aadc32 100644 --- a/panel/widgets/__init__.py +++ b/panel/widgets/__init__.py @@ -57,9 +57,9 @@ RadioButtonGroup, Select, ToggleGroup, ) from .slider import ( # noqa - DateRangeSlider, DateSlider, DatetimeRangeSlider, DiscreteSlider, - EditableFloatSlider, EditableIntSlider, EditableRangeSlider, FloatSlider, - IntRangeSlider, IntSlider, RangeSlider, + DateRangeSlider, DateSlider, DatetimeRangeSlider, DatetimeSlider, + DiscreteSlider, EditableFloatSlider, EditableIntSlider, + EditableRangeSlider, FloatSlider, IntRangeSlider, IntSlider, RangeSlider, ) from .speech_to_text import Grammar, GrammarList, SpeechToText # noqa from .tables import DataFrame, Tabulator # noqa @@ -87,6 +87,7 @@ "DateRangeSlider", "DatetimeRangeSlider", "DateSlider", + "DatetimeSlider", "DatetimeInput", "DatetimePicker", "DatetimeRangeInput", diff --git a/panel/widgets/slider.py b/panel/widgets/slider.py index fc65e60024..2fa58541c4 100644 --- a/panel/widgets/slider.py +++ b/panel/widgets/slider.py @@ -29,6 +29,7 @@ from ..io import state from ..io.resources import CDN_DIST from ..layout import Column, Panel, Row +from ..models.datetime_slider import DatetimeSlider as _BkDatetimeSlider from ..util import ( datetime_as_utctimestamp, edit_readonly, param_reprs, value_as_date, value_as_datetime, @@ -319,6 +320,36 @@ def _process_property_change(self, msg): return msg +class DatetimeSlider(DateSlider): + """ + The DatetimeSlider widget allows selecting a value within a set of + bounds using a slider. Supports datetime.date, datetime.datetime + and np.datetime64 values. The step size is fixed at 1 minute. + + Reference: https://panel.holoviz.org/reference/widgets/DatetimeSlider.html + + :Example: + + >>> import datetime as dt + >>> DatetimeSlider( + ... value=dt.datetime(2025, 1, 1), + ... start=dt.datetime(2025, 1, 1), + ... end=dt.datetime(2025, 1, 7), + ... name="A datetime value" + ... ) + """ + + as_datetime = param.Boolean(default=True, readonly=True, doc=""" + Whether to store the date as a datetime.""") + + step = param.Number(default=60, bounds=(1, None), doc=""" + The step size in seconds. Default is 1 minute, i.e 60 seconds.""") + + _property_conversion = staticmethod(value_as_datetime) + + _widget_type: ClassVar[type[Model]] = _BkDatetimeSlider + + class DiscreteSlider(CompositeWidget, _SliderBase): """ The DiscreteSlider widget allows selecting a value from a discrete From b43a52a0fec30cb2c9061d70a89df70c37ee6aba Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Thu, 31 Oct 2024 14:48:38 +0100 Subject: [PATCH 12/44] Ensure parameter overrides are applied to ESM components (#7452) * Ensure parameter overrides are applied to ESM components * Small fix --- panel/custom.py | 9 ++++++++- panel/tests/test_custom.py | 15 +++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/panel/custom.py b/panel/custom.py index f7d858ee67..417ff3eea5 100644 --- a/panel/custom.py +++ b/panel/custom.py @@ -361,7 +361,14 @@ def _get_properties(self, doc: Document) -> dict[str, Any]: props = super()._get_properties(doc) cls = type(self) data_params = {} - ignored = [p for p in Reactive.param if not issubclass(cls.param[p].owner, ReactiveESM)] + # Split data model properties from ESM model properties + # Note that inherited parameters are generally treated + # as ESM model properties unless their type has changed + ignored = [ + p for p in Reactive.param + if not issubclass(cls.param[p].owner, ReactiveESM) or + (p in Viewable.param and p != 'name' and type(Reactive.param[p]) is type(cls.param[p])) + ] for k, v in self.param.values().items(): p = self.param[k] is_viewable = is_viewable_param(p) diff --git a/panel/tests/test_custom.py b/panel/tests/test_custom.py index 97e32a1ea3..c558134e84 100644 --- a/panel/tests/test_custom.py +++ b/panel/tests/test_custom.py @@ -143,3 +143,18 @@ def test_reactive_esm_children_models_cleanup_on_replace(document, comm): assert ref in md2._models md2_model, _ = md2._models[ref] assert model.data.children == [md2_model] + +class ESMOverride(ReactiveESM): + + width = param.Integer(default=42) + +def test_esm_parameter_override(document, comm): + esm = ESMOverride() + + model = esm.get_root(document, comm) + + assert model.width == 42 + + esm.width = 84 + + assert model.width == 84 From c06f779ecf0bae756c43229ee036110253b3cf79 Mon Sep 17 00:00:00 2001 From: Marc Skov Madsen Date: Fri, 1 Nov 2024 17:23:38 +0100 Subject: [PATCH 13/44] Fix bug in is_viewable_param (#7454) --- panel/tests/test_viewable.py | 49 +++++++++++++++++++++++++++++++++++- panel/viewable.py | 44 +++++++++++++++++++------------- 2 files changed, 74 insertions(+), 19 deletions(-) diff --git a/panel/tests/test_viewable.py b/panel/tests/test_viewable.py index 9aef6bcd62..43b83807a9 100644 --- a/panel/tests/test_viewable.py +++ b/panel/tests/test_viewable.py @@ -7,7 +7,9 @@ from panel.interact import interactive from panel.pane import Markdown, Str, panel from panel.param import ParamMethod -from panel.viewable import Viewable, Viewer +from panel.viewable import ( + Child, Children, Viewable, Viewer, is_viewable_param, +) from .util import jb_available @@ -117,3 +119,48 @@ def test_clone_with_non_defaults(): assert ([(k, v) for k, v in sorted(v.param.values().items()) if k not in ('name')] == [(k, v) for k, v in sorted(clone.param.values().items()) if k not in ('name')]) + +def test_is_viewable_parameter(): + class Example(param.Parameterized): + p_dict = param.Dict() + p_child = Child() + p_children = Children() + + # ClassSelector + c_viewable = param.ClassSelector(class_=Viewable) + c_viewables = param.ClassSelector(class_=(Viewable,)) + c_none = param.ClassSelector(class_=None) + c_tuple = param.ClassSelector(class_=tuple) + c_list_tuple = param.ClassSelector(class_=(list, tuple)) + + # List + l_no_item_type = param.List() + l_item_type_viewable = param.List(item_type=Viewable) + l_item_type_not_viewable = param.List(item_type=tuple) + + l_item_types_viewable = param.List(item_type=(Viewable,)) + l_item_types_not_viewable = param.List(item_type=(tuple,)) + l_item_types_not_viewable2 = param.List(item_type=(list, tuple,)) + + example = Example() + + assert not is_viewable_param(example.param.p_dict) + assert is_viewable_param(example.param.p_child) + assert is_viewable_param(example.param.p_children) + + # ClassSelector + assert is_viewable_param(example.param.c_viewable) + assert is_viewable_param(example.param.c_viewables) + assert not is_viewable_param(example.param.c_none) + assert not is_viewable_param(example.param.c_tuple) + assert not is_viewable_param(example.param.c_list_tuple) + + # List + assert not is_viewable_param(example.param.l_no_item_type) + assert not is_viewable_param(example.param.l_no_item_type) + assert is_viewable_param(example.param.l_item_type_viewable) + assert not is_viewable_param(example.param.l_item_type_not_viewable) + + assert is_viewable_param(example.param.l_item_types_viewable) + assert not is_viewable_param(example.param.l_item_types_not_viewable) + assert not is_viewable_param(example.param.l_item_types_not_viewable2) diff --git a/panel/viewable.py b/panel/viewable.py index f346df18e6..7fc48d8e7e 100644 --- a/panel/viewable.py +++ b/panel/viewable.py @@ -1193,34 +1193,42 @@ def __set__(self, obj, val): +def _is_viewable_class_selector(class_selector: param.ClassSelector) -> bool: + if not class_selector.class_: + return False + if isinstance(class_selector.class_, tuple): + return all(issubclass(cls, Viewable) for cls in class_selector.class_) + return issubclass(class_selector.class_, Viewable) + +def _is_viewable_list(param_list: param.List) -> bool: + if not param_list.item_type: + return False + if isinstance(param_list.item_type, tuple): + return all(issubclass(cls, Viewable) for cls in param_list.item_type) + return issubclass(param_list.item_type, Viewable) + + def is_viewable_param(parameter: param.Parameter) -> bool: """ - Detects whether the Parameter uniquely identifies a Viewable - type. + Determines if a parameter uniquely identifies a Viewable type. Arguments --------- - parameter: param.Parameter + parameter : param.Parameter + The parameter to evaluate. Returns ------- - Whether the Parameter specieis a Parameter type + bool + True if the parameter specifies a Viewable type, False otherwise. """ - p = parameter - if ( - isinstance(p, (Child, Children)) or - (isinstance(p, param.ClassSelector) and p.class_ and ( - (isinstance(p.class_, tuple) and - all(issubclass(cls, Viewable) for cls in p.class_)) or - issubclass(p.class_, Viewable) - )) or - (isinstance(p, param.List) and p.item_type and ( - (isinstance(p.item_type, tuple) and - all(issubclass(cls, Viewable) for cls in p.item_type)) or - issubclass(p.item_type, Viewable) - )) - ): + if isinstance(parameter, (Child, Children)): return True + if isinstance(parameter, param.ClassSelector) and _is_viewable_class_selector(parameter): + return True + if isinstance(parameter, param.List) and _is_viewable_list(parameter): + return True + return False From 6c322c500766c6900bd7e854755d218edb6cd85a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Wed, 6 Nov 2024 12:47:22 +0100 Subject: [PATCH 14/44] fix: Log authorization callback errors (#7463) * fix: log auth errors * Add test * Set status code to 403 --- panel/io/server.py | 2 ++ panel/tests/command/test_serve.py | 28 ++++++++++++++++++++++++++-- panel/tests/util.py | 5 ++++- 3 files changed, 32 insertions(+), 3 deletions(-) diff --git a/panel/io/server.py b/panel/io/server.py index 153a8c76cb..61bb6c1cb7 100644 --- a/panel/io/server.py +++ b/panel/io/server.py @@ -426,6 +426,7 @@ def _authorize(self, session=False): auth_error = None except Exception: auth_error = f'Authorization callback errored. Could not validate user {state.user}.' + logger.warning(auth_error) return authorized, auth_error def _render_auth_error(self, auth_error): @@ -455,6 +456,7 @@ async def get(self, *args, **kwargs): if authorized is None: return elif not authorized: + self.set_status(403) page = self._render_auth_error(auth_error) self.set_header("Content-Type", 'text/html') self.write(page) diff --git a/panel/tests/command/test_serve.py b/panel/tests/command/test_serve.py index f15c451420..74b121f832 100644 --- a/panel/tests/command/test_serve.py +++ b/panel/tests/command/test_serve.py @@ -3,12 +3,14 @@ import tempfile import time +from textwrap import dedent + import pytest import requests from panel.tests.util import ( - linux_only, run_panel_serve, unix_only, wait_for_port, wait_for_regex, - write_file, + NBSR, linux_only, run_panel_serve, unix_only, wait_for_port, + wait_for_regex, write_file, ) @@ -175,3 +177,25 @@ def test_serve_setup(tmp_path): with run_panel_serve(["--port", "0", py, "--setup", setup_py], cwd=tmp_path) as p: _, output = wait_for_regex(p.stdout, regex=regex, return_output=True) assert output[0].strip() == "Setup running before" + + +def test_serve_authorize_callback_exception(tmp_path): + app = "import panel as pn; pn.panel('Hello').servable()" + py = tmp_path / "app.py" + py.write_text(app) + + setup_app = """\ + import panel as pn + def auth(userinfo): + raise ValueError("This is an error") + pn.config.authorize_callback = auth""" + setup_py = tmp_path / "setup.py" + setup_py.write_text(dedent(setup_app)) + + regex = re.compile('(Authorization callback errored)') + with run_panel_serve(["--port", "0", py, "--setup", setup_py], cwd=tmp_path) as p: + nsbr = NBSR(p.stdout) + port = wait_for_port(nsbr) + resp = requests.get(f"http://localhost:{port}/") + wait_for_regex(nsbr, regex=regex) + assert resp.status_code == 403 diff --git a/panel/tests/util.py b/panel/tests/util.py index e7d87b3ac7..f86f1a5cc1 100644 --- a/panel/tests/util.py +++ b/panel/tests/util.py @@ -395,7 +395,10 @@ def readline(self, timeout=None): return None def wait_for_regex(stdout, regex, count=1, return_output=False): - nbsr = NBSR(stdout) + if isinstance(stdout, NBSR): + nbsr = stdout + else: + nbsr = NBSR(stdout) m = None output, found = [], [] for _ in range(20): From 164cd2ae9bd16da02104d53f3febc39c1a2cc080 Mon Sep 17 00:00:00 2001 From: Marc Skov Madsen Date: Wed, 6 Nov 2024 18:09:55 +0100 Subject: [PATCH 15/44] Link to pycafe docs (#7464) --- doc/how_to/deployment/pycafe.md | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/doc/how_to/deployment/pycafe.md b/doc/how_to/deployment/pycafe.md index c42999723e..f6bdd58649 100644 --- a/doc/how_to/deployment/pycafe.md +++ b/doc/how_to/deployment/pycafe.md @@ -1,12 +1,12 @@ -# PY.CAFE Guide +# PyCafe Guide -This guide demonstrates how to deploy a Panel app on [PY.CAFE](https://py.cafe/). +This guide demonstrates how to deploy a Panel app on [py.cafe](https://py.cafe/). -PY.CAFE is a platform for creating and sharing data apps online, powered by [Pyodide](https://pyodide.org/). It offers a free tier for users. +PyCafe is a platform for creating and sharing data apps online, powered by [Pyodide](https://pyodide.org/). It offers a free tier for users. You can find the official PyCafe-Panel guide [here](https://py.cafe/docs/apps/panel). ## 1. Log In -Visit [PY.CAFE](https://py.cafe/) and either sign in or sign up for an account if you want to save your projects to your personal gallery. +Visit [py.cafe](https://py.cafe/) and either sign in or sign up for an account if you want to save your projects to your personal gallery. ## 2. Choose the Panel Framework @@ -89,3 +89,7 @@ Explore the [`panel-org`](https://py.cafe/panel-org) gallery examples below: - [Penguin Crossfilter](https://py.cafe/panel-org/penguin-crossfilter) from the [Penguin Crossfilter Tutorial](../../gallery/penguin_crossfilter) - [Portfolio Analyzer](https://py.cafe/panel-org/portfolio-analyzer) from the [Portfolio Analyzer Tutorial](../../gallery/portfolio_analyzer). - [VideoStream Interface](https://py.cafe/panel-org/videostream) from the [VideoStream Interface Tutorial](../../gallery/streaming_videostream). + +---- + +For more examples check out [awesome-panel.org](https://py.cafe/awesome.panel.org). From 28604f2c3fa1840d7f2ce6eeefd25cdb03441bf7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Fri, 8 Nov 2024 11:58:14 +0100 Subject: [PATCH 16/44] enh: Support polars in `pn.cache` (#7472) --- panel/io/cache.py | 68 +++++++++++++++++++++++++++++++----- panel/tests/io/test_cache.py | 28 +++++++++++++++ pixi.toml | 1 + 3 files changed, 89 insertions(+), 8 deletions(-) diff --git a/panel/io/cache.py b/panel/io/cache.py index af3bf5bb48..5392e53d95 100644 --- a/panel/io/cache.py +++ b/panel/io/cache.py @@ -58,13 +58,13 @@ def clear(self, func_hashes: list[str | None]=[None]) -> None: bytes, str, float, int, bool, bytearray, type(None) ) -_NP_SIZE_LARGE = 100_000 +_ARRAY_SIZE_LARGE = 100_000 -_NP_SAMPLE_SIZE = 100_000 +_ARRAY_SAMPLE_SIZE = 100_000 -_PANDAS_ROWS_LARGE = 100_000 +_DATAFRAME_ROWS_LARGE = 100_000 -_PANDAS_SAMPLE_SIZE = 100_000 +_DATAFRAME_SAMPLE_SIZE = 100_000 if sys.platform == 'win32': _TIME_FN = time.perf_counter @@ -125,8 +125,8 @@ def _pandas_hash(obj): if not isinstance(obj, (pd.Series, pd.DataFrame)): obj = pd.Series(obj) - if len(obj) >= _PANDAS_ROWS_LARGE: - obj = obj.sample(n=_PANDAS_SAMPLE_SIZE, random_state=0) + if len(obj) >= _DATAFRAME_ROWS_LARGE: + obj = obj.sample(n=_DATAFRAME_SAMPLE_SIZE, random_state=0) try: if isinstance(obj, pd.DataFrame): return ((b"%s" % pd.util.hash_pandas_object(obj).sum()) @@ -138,13 +138,62 @@ def _pandas_hash(obj): # it contains unhashable objects. return b"%s" % pickle.dumps(obj, pickle.HIGHEST_PROTOCOL) +def _polars_combine_hash_expr(columns): + """ + Inspired by pd.core.util.hashing.combine_hash_arrays, + rewritten to a Polars expression. + """ + import polars as pl + + mult = pl.lit(1000003, dtype=pl.UInt64) + initial_value = pl.lit(0x345678, dtype=pl.UInt64) + increment = pl.lit(82520, dtype=pl.UInt64) + final_addition = pl.lit(97531, dtype=pl.UInt64) + + out = initial_value + num_items = len(columns) + for i, col_name in enumerate(columns): + col = pl.col(col_name).hash(seed=0) + inverse_i = pl.lit(num_items - i, dtype=pl.UInt64) + out = (out ^ col) * mult + mult = mult + (increment + inverse_i + inverse_i) + + return out + final_addition + +def _polars_hash(obj): + import polars as pl + + hash_type = type(obj).__name__.encode() + + if isinstance(obj, pl.Series): + obj = obj.to_frame() + + columns = obj.collect_schema().names() + hash_columns = _container_hash(columns) + + # LazyFrame does not support len and sample + if hash_type != b"LazyFrame" and len(obj) >= _DATAFRAME_ROWS_LARGE: + obj = obj.sample(n=_DATAFRAME_SAMPLE_SIZE, seed=0) + elif hash_type == b"LazyFrame": + count = obj.select(pl.col(columns[0]).count()).collect().item() + if count >= _DATAFRAME_ROWS_LARGE: + obj = obj.select(pl.all().sample(n=_DATAFRAME_SAMPLE_SIZE, seed=0)) + + hash_expr = _polars_combine_hash_expr(columns) + hash_data = obj.select(hash_expr).sum() + if hash_type == b"LazyFrame": + hash_data = hash_data.collect() + hash_data = _int_to_bytes(hash_data.item()) + + return hash_type + hash_data + hash_columns + def _numpy_hash(obj): h = hashlib.new("md5") h.update(_generate_hash(obj.shape)) - if obj.size >= _NP_SIZE_LARGE: + if obj.size >= _ARRAY_SIZE_LARGE: import numpy as np state = np.random.RandomState(0) - obj = state.choice(obj.flat, size=_NP_SAMPLE_SIZE) + obj = state.choice(obj.flat, size=_ARRAY_SAMPLE_SIZE) h.update(obj.tobytes()) return h.digest() @@ -180,6 +229,9 @@ def _io_hash(obj): 'builtins.dict_items' : lambda obj: _container_hash(dict(obj)), 'builtins.getset_descriptor' : lambda obj: obj.__qualname__.encode(), "numpy.ufunc" : lambda obj: obj.__name__.encode(), + "polars.series.series.Series": _polars_hash, + "polars.dataframe.frame.DataFrame": _polars_hash, + "polars.lazyframe.frame.LazyFrame": _polars_hash, # Functions inspect.isbuiltin : lambda obj: obj.__name__.encode(), inspect.ismodule : lambda obj: obj.__name__, diff --git a/panel/tests/io/test_cache.py b/panel/tests/io/test_cache.py index 6dd70168ec..e4aa495520 100644 --- a/panel/tests/io/test_cache.py +++ b/panel/tests/io/test_cache.py @@ -151,6 +151,34 @@ def test_series_hash(): series2.iloc[0] = 3.14 assert not hashes_equal(series1, series2) +def test_polars_dataframe_hash(): + pl = pytest.importorskip("polars") + data = { + "A": [0.0, 1.0, 2.0, 3.0, 4.0], + "B": [0.0, 1.0, 0.0, 1.0, 0.0], + "C": ["foo1", "foo2", "foo3", "foo4", "foo5"], + } + # DataFrame + df1, df2 = pl.DataFrame(data), pl.DataFrame(data) + assert hashes_equal(df1, df2) + df2 = df2.with_columns(A=pl.col("A").sort(descending=True)) + assert not hashes_equal(df1, df2) + + # Lazy DataFrame + df1, df2 = pl.LazyFrame(data), pl.LazyFrame(data) + assert hashes_equal(df1, df2) + df2 = df2.with_columns(A=pl.col("A").sort(descending=True)) + assert not hashes_equal(df1, df2) + +def test_polars_series_hash(): + pl = pytest.importorskip("polars") + ser1 = pl.Series([0.0, 1.0, 2.0, 3.0, 4.0]) + ser2 = ser1.clone() + + assert hashes_equal(ser1, ser2) + ser2 = ser2.replace(0.0, 3.14) + assert not hashes_equal(ser1, ser2) + def test_ufunc_hash(): assert hashes_equal(np.absolute, np.absolute) assert not hashes_equal(np.sin, np.cos) diff --git a/pixi.toml b/pixi.toml index dec24c81d6..af37a2df46 100644 --- a/pixi.toml +++ b/pixi.toml @@ -141,6 +141,7 @@ ipympl = "*" ipyvuetify = "*" ipywidgets_bokeh = "*" numba = "*" +polars = "*" reacton = "*" scipy = "*" textual = "*" From 397d9958a6999b3bf5091b80409bf84b309fea5f Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Fri, 8 Nov 2024 12:14:11 +0100 Subject: [PATCH 17/44] Improve and document hold utility (#7474) --- doc/how_to/performance/hold.md | 53 +++++++++++++++++++++++++++++++ doc/how_to/performance/index.md | 8 +++++ panel/io/document.py | 56 +++++++++++++++++++++++++++++++-- panel/io/model.py | 28 +++++------------ panel/layout/base.py | 3 +- panel/layout/grid.py | 3 +- panel/reactive.py | 3 +- panel/util/warnings.py | 13 ++++++-- 8 files changed, 135 insertions(+), 32 deletions(-) create mode 100644 doc/how_to/performance/hold.md diff --git a/doc/how_to/performance/hold.md b/doc/how_to/performance/hold.md new file mode 100644 index 0000000000..53fedf04b7 --- /dev/null +++ b/doc/how_to/performance/hold.md @@ -0,0 +1,53 @@ +# Batching updates with `hold` + +When working with interactive dashboards and applications in Panel, you might encounter situations where updating multiple components simultaneously causes unnecessary re-renders. This is because Panel generally dispatches any change to a parameter immediately. This can lead to performance issues and a less responsive user experience because each individual update may trigger re-renders on the frontend. The `hold` utility in Panel allows you to batch updates to the frontend, reducing the number of re-renders and improving performance. + +In this guide, we'll explore how to use hold both as a context manager and as a decorator to optimize your Panel applications. + +## What is hold? + +The `hold` function is a context manager and decorator that temporarily holds events on a Bokeh Document. When you update multiple components within a hold block, the events are collected and dispatched all at once when the block exits. This means that the frontend will only re-render once, regardless of how many updates were made, leading to a smoother and more efficient user experience. + +## Using `hold` + +If you have a function that updates components and you want to ensure that all updates are held, you can use hold as a decorator, e.g. here we update 100 components at once. If you do not hold then each of these events is sent and applied in series, potentially resulting in visible updates. + +```{pyodide} +import panel as pn +from panel.io import hold + +@hold() +def increment(e): + for obj in column: + obj.object = str(e.new) + +column = pn.FlexBox(*['0']*100) +button = pn.widgets.Button(name='Increment', on_click=increment) + +pn.Column(column, button).servable() +``` + +Applying the hold decorator means all the updates are sent in a single Websocket message and applied on the frontend simultaneously. + +Alternatively the `hold` function can be used as a context manager, potentially giving you finer grained control over which events are batched and which are not: + +```{pyodide} +import time + +import panel as pn +from panel.io import hold + +def increment(e): + with button.param.update(name='Incrementing...', disabled=True): + time.sleep(0.5) + with hold(): + for obj in column: + obj.object = str(e.new) + +column = pn.FlexBox(*['0']*100) +button = pn.widgets.Button(name='Increment', on_click=increment) + +pn.Column(column, button).servable() +``` + +Here the updates to the `Button` are dispatched immediately while the updates to the counters are batched. diff --git a/doc/how_to/performance/index.md b/doc/how_to/performance/index.md index 622c462ce5..fdbef1bb59 100644 --- a/doc/how_to/performance/index.md +++ b/doc/how_to/performance/index.md @@ -19,6 +19,13 @@ Discover how to reuse sessions to improve the start render time. Discover how to enable throttling to reduce the number of events being processed. ::: +:::{grid-item-card} {octicon}`tab;2.5em;sd-mr-1 sd-animate-grow50` Batching Updates with `hold` +:link: hold +:link-type: doc + +Discover how to improve performance by using the `hold` context manager and decorator to batch updates to multiple components. +::: + :::: ```{toctree} @@ -28,4 +35,5 @@ Discover how to enable throttling to reduce the number of events being processed reuse_sessions throttling +hold ``` diff --git a/panel/io/document.py b/panel/io/document.py index ea03602e23..33b890567f 100644 --- a/panel/io/document.py +++ b/panel/io/document.py @@ -31,13 +31,15 @@ from ..config import config from ..util import param_watchers from .loading import LOADING_INDICATOR_CSS_CLASS -from .model import hold, monkeypatch_events # noqa: F401 API import +from .model import monkeypatch_events # noqa: F401 API import from .state import curdoc_locked, state if TYPE_CHECKING: + from bokeh.core.enums import HoldPolicyType from bokeh.core.has_props import HasProps from bokeh.protocol.message import Message from bokeh.server.connection import ServerConnection + from pyviz_comms import Comm logger = logging.getLogger(__name__) @@ -431,11 +433,18 @@ def dispatch_django( return futures @contextmanager -def unlocked() -> Iterator: +def unlocked(policy: HoldPolicyType = 'combine') -> Iterator: """ Context manager which unlocks a Document and dispatches ModelChangedEvents triggered in the context body to all sockets on current sessions. + + Arguments + --------- + policy: Literal['combine' | 'collect'] + One of 'combine' or 'collect' determining whether events + setting the same property are combined or accumulated to be + dispatched when the context manager exits. """ curdoc = state.curdoc session_context = getattr(curdoc, 'session_context', None) @@ -457,7 +466,7 @@ def unlocked() -> Iterator: monkeypatch_events(curdoc.callbacks._held_events) return - curdoc.hold() + curdoc.hold(policy=policy) try: yield finally: @@ -518,6 +527,47 @@ def unlocked() -> Iterator: except RuntimeError: curdoc.add_next_tick_callback(partial(retrigger_events, curdoc, retriggered_events)) +@contextmanager +def hold(doc: Document | None = None, policy: HoldPolicyType = 'combine', comm: Comm | None = None): + """ + Context manager that holds events on a particular Document + allowing them all to be collected and dispatched when the context + manager exits. This allows multiple events on the same object to + be combined if the policy is set to 'combine'. + + Arguments + --------- + doc: Document + The Bokeh Document to hold events on. + policy: HoldPolicyType + One of 'combine', 'collect' or None determining whether events + setting the same property are combined or accumulated to be + dispatched when the context manager exits. + comm: Comm + The Comm to dispatch events on when the context manager exits. + """ + doc = doc or state.curdoc + if doc is None: + yield + return + held = doc.callbacks.hold_value + try: + if policy is None: + doc.unhold() + yield + else: + with unlocked(policy=policy): + if not doc.callbacks.hold_value: + doc.hold(policy) + yield + finally: + if held: + doc.callbacks._hold = held + else: + if comm is not None: + from .notebook import push + push(doc, comm) + doc.unhold() @contextmanager def immediate_dispatch(doc: Document | None = None): diff --git a/panel/io/model.py b/panel/io/model.py index 322af5748f..f7a02874b9 100644 --- a/panel/io/model.py +++ b/panel/io/model.py @@ -22,6 +22,7 @@ from bokeh.models import ColumnDataSource, FlexBox, Model from bokeh.protocol.messages.patch_doc import patch_doc +from ..util.warnings import deprecated from .state import state if TYPE_CHECKING: @@ -174,7 +175,7 @@ def bokeh_repr(obj: Model, depth: int = 0, ignored: Optional[Iterable[str]] = No return r @contextmanager -def hold(doc: Document, policy: HoldPolicyType = 'combine', comm: Comm | None = None): +def hold(doc: Document | None = None, policy: HoldPolicyType = 'combine', comm: Comm | None = None): """ Context manager that holds events on a particular Document allowing them all to be collected and dispatched when the context @@ -192,22 +193,9 @@ def hold(doc: Document, policy: HoldPolicyType = 'combine', comm: Comm | None = comm: Comm The Comm to dispatch events on when the context manager exits. """ - doc = doc or state.curdoc - if doc is None: - yield - return - held = doc.callbacks.hold_value - try: - if policy is None: - doc.unhold() - else: - doc.hold(policy) - yield - finally: - if held: - doc.callbacks._hold = held - else: - if comm is not None: - from .notebook import push - push(doc, comm) - doc.unhold() + deprecated( + '1.7.0', 'panel.io.model.hold', 'panel.io.document.hold', + warn_version='1.6.0' + ) + from .document import hold + yield hold(doc, policy, comm) diff --git a/panel/layout/base.py b/panel/layout/base.py index d58286db4c..a7a639f3bf 100644 --- a/panel/layout/base.py +++ b/panel/layout/base.py @@ -15,8 +15,7 @@ from bokeh.models import Row as BkRow from param.parameterized import iscoroutinefunction, resolve_ref -from ..io.document import freeze_doc -from ..io.model import hold +from ..io.document import freeze_doc, hold from ..io.resources import CDN_DIST from ..models import Column as PnColumn from ..reactive import Reactive diff --git a/panel/layout/grid.py b/panel/layout/grid.py index c594ae1244..2039786be1 100644 --- a/panel/layout/grid.py +++ b/panel/layout/grid.py @@ -16,8 +16,7 @@ from bokeh.models import FlexBox as BkFlexBox, GridBox as BkGridBox -from ..io.document import freeze_doc -from ..io.model import hold +from ..io.document import freeze_doc, hold from ..io.resources import CDN_DIST from ..viewable import ChildDict from .base import ( diff --git a/panel/reactive.py b/panel/reactive.py index 6d1e96a780..7dbc3d2041 100644 --- a/panel/reactive.py +++ b/panel/reactive.py @@ -35,8 +35,7 @@ resolve_ref, resolve_value, ) -from .io.document import unlocked -from .io.model import hold +from .io.document import hold, unlocked from .io.notebook import push from .io.resources import ( CDN_DIST, loading_css, patch_stylesheet, process_raw_css, diff --git a/panel/util/warnings.py b/panel/util/warnings.py index 9c6938b1df..5284046715 100644 --- a/panel/util/warnings.py +++ b/panel/util/warnings.py @@ -59,14 +59,21 @@ def deprecated( remove_version: Version | str, old: str, new: str | None = None, + *, extra: str | None = None, + warn_version: Version | str | None = None ) -> None: + from .. import __version__ - import panel as pn - - current_version = Version(pn.__version__) + current_version = Version(__version__) base_version = Version(current_version.base_version) + if warn_version: + if isinstance(warn_version, str): + warn_version = Version(warn_version) + if base_version < warn_version: + return + if isinstance(remove_version, str): remove_version = Version(remove_version) From 773936d0fb3a260d1a97c2c2bb0baad684706553 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Fri, 8 Nov 2024 12:18:03 +0100 Subject: [PATCH 18/44] Various improvements for writing ESM components (#7462) --- doc/how_to/custom_components/esm/build.md | 8 +- panel/command/compile.py | 64 +++++----------- panel/custom.py | 90 +++++++++++++++-------- panel/io/compile.py | 66 ++++++++++++++++- panel/io/datamodel.py | 8 +- panel/layout/base.py | 3 +- panel/reactive.py | 2 +- panel/tests/io/test_compile.py | 20 ++++- 8 files changed, 177 insertions(+), 84 deletions(-) diff --git a/doc/how_to/custom_components/esm/build.md b/doc/how_to/custom_components/esm/build.md index c2f888c150..d951ee6b49 100644 --- a/doc/how_to/custom_components/esm/build.md +++ b/doc/how_to/custom_components/esm/build.md @@ -184,7 +184,7 @@ panel compile confetti ``` :::{hint} -`panel compile` accepts file paths, e.g. `my_components/custom.py`, and dotted module name, e.g. `my_package.custom`. If you provide a module name it must be importable. +`panel compile` accepts file paths, e.g. `my_components/custom.py`, or dotted module names, e.g. `my_package.custom`. If you provide a module name it must be importable. ::: This will automatically discover the `ConfettiButton` but you can also explicitly request a single component by adding the class name: @@ -216,7 +216,11 @@ esbuild output: ⚡ Done in 9ms ``` -The compiled JavaScript file will be automatically loaded if it remains alongside the component. If you rename the component or modify its code or `_importmap`, you must recompile the component. For ongoing development, consider using the `--dev` option to ignore the compiled file and automatically reload the development version when it changes. +If the supplied module or package contains multiple components they will all be bundled together by default. If instead you want to generate bundles for each file explicitly you must list them with the `:` syntax, e.g. `panel compile package.module:Component1,Component2`. You may also provide a glob pattern to request multiple components to be built individually without listing them all out, e.g. `panel compile "package.module:Component*"`. + +During runtime the compiled bundles will be resolved automatically, where bundles compiled for a specific component (i.e. `.bundle.js`) take highest precedence and we then search for module bundles up to the root package, e.g. for a component that lives in `package.module` we first search for `package.module.bundle.js` in the same directory as the component and then recursively search in parent directories until we reach the root of the package. + +If you rename the component or modify its code or `_importmap`, you must recompile the component. For ongoing development, consider using the `--dev` option to ignore the compiled file and automatically reload the development version when it changes. #### Compilation Steps diff --git a/panel/command/compile.py b/panel/command/compile.py index 776a555a2f..332a1203f3 100644 --- a/panel/command/compile.py +++ b/panel/command/compile.py @@ -1,12 +1,6 @@ -import os -import pathlib -import sys - -from collections import defaultdict - from bokeh.command.subcommand import Argument, Subcommand -from ..io.compile import RED, compile_components, find_components +from ..io.compile import RED, compile_components, find_module_bundles class Compile(Subcommand): @@ -42,52 +36,28 @@ class Compile(Subcommand): ) def invoke(self, args): - bundles = defaultdict(list) - for module_spec in args.modules: - if ':' in module_spec: - *parts, cls = module_spec.split(':') - module = ':'.join(parts) - else: - module = module_spec - cls = '' - classes = cls.split(',') if cls else None - module_name, ext = os.path.splitext(os.path.basename(module)) - if ext not in ('', '.py'): - print( # noqa - f'{RED} Can only compile ESM components defined in Python ' - 'file or importable module.' - ) - return 1 + bundles = {} + for module in args.modules: try: - components = find_components(module, classes) - except ValueError: - cls_error = f' and that class(es) {cls!r} are defined therein' if cls else '' - print( # noqa - f'{RED} Could not find any ESM components to compile, ensure ' - f'you provided the right module{cls_error}.' - ) + module_bundles = find_module_bundles(module) + except RuntimeError as e: + print(f'{RED} {e}') # noqa return 1 - if module in sys.modules: - module_path = sys.modules[module].__file__ - else: - module_path = module - module_path = pathlib.Path(module_path).parent - for component in components: - if component._bundle: - bundle_path = component._bundle - if isinstance(bundle_path, str): - path = (module_path / bundle_path).absolute() - else: - path = bundle_path.absolute() - bundles[str(path)].append(component) - elif len(components) > 1 and not classes: - component_module = module_name if ext else component.__module__ - bundles[module_path / f'{component_module}.bundle.js'].append(component) + if not module_bundles: + print ( # noqa + f'{RED} Could not find any ESM components to compile ' + f'in {module}, ensure you provided the right module.' + ) + for bundle, components in module_bundles.items(): + if bundle in bundles: + bundles[bundle] = components else: - bundles[module_path / f'{component.__name__}.bundle.js'].append(component) + bundles[bundle] += components errors = 0 for bundle, components in bundles.items(): + component_names = '\n- '.join(c.name for c in components) + print(f"Building {bundle} containing the following components:\n\n- {component_names}\n") # noqa out = compile_components( components, build_dir=args.build_dir, diff --git a/panel/custom.py b/panel/custom.py index 417ff3eea5..136c8bc840 100644 --- a/panel/custom.py +++ b/panel/custom.py @@ -2,6 +2,7 @@ import asyncio import hashlib +import importlib import inspect import os import pathlib @@ -168,7 +169,7 @@ def __init__(mcs, name: str, bases: tuple[type, ...], dict_: Mapping[str, Any]): model_name = f'{name}{ReactiveMetaBase._name_counter[name]}' ignored = [p for p in Reactive.param if not issubclass(type(mcs.param[p].owner), ReactiveESMMetaclass)] mcs._data_model = construct_data_model( - mcs, name=model_name, ignore=ignored + mcs, name=model_name, ignore=ignored, extras={'esm_constants': param.Dict} ) @@ -216,6 +217,8 @@ class CounterButton(pn.custom.ReactiveESM): _bundle: ClassVar[str | os.PathLike | None] = None + _constants: ClassVar[dict[str, Any]] = {} + _esm: ClassVar[str | os.PathLike] = "" # Specifies exports to make available to JS in a bundled file @@ -235,15 +238,27 @@ def __init__(self, **params): self._msg__callbacks = [] @classproperty - def _bundle_path(cls) -> os.PathLike | None: - if config.autoreload and cls._esm: - return + def _module_path(cls): + if hasattr(cls, '__path__'): + return pathlib.Path(cls.__path__) try: - mod_path = pathlib.Path(inspect.getfile(cls)).parent + return pathlib.Path(inspect.getfile(cls)).parent except (OSError, TypeError, ValueError): if not isinstance(cls._bundle, pathlib.PurePath): return + + @classproperty + def _bundle_path(cls) -> os.PathLike | None: + if config.autoreload and cls._esm: + return + mod_path = cls._module_path + if mod_path is None: + return if cls._bundle: + for scls in cls.__mro__: + if issubclass(scls, ReactiveESM) and cls._bundle == scls._bundle: + cls = scls + mod_path = cls._module_path bundle = cls._bundle if isinstance(bundle, pathlib.PurePath): return bundle @@ -251,21 +266,34 @@ def _bundle_path(cls) -> os.PathLike | None: bundle_path = mod_path / bundle if bundle_path.is_file(): return bundle_path - return - else: - raise ValueError( - 'Could not resolve {cls.__name__}._bundle. Ensure ' - 'you provide either a string with a relative or absolute ' - 'path or a Path object to a .js file extension.' - ) + raise ValueError( + f'Could not resolve {cls.__name__}._bundle: {cls._bundle}. Ensure ' + 'you provide either a string with a relative or absolute ' + 'path or a Path object to a .js file extension.' + ) + + # Attempt resolving bundle for this component specifically path = mod_path / f'{cls.__name__}.bundle.js' if path.is_file(): return path + + # Attempt to resolve bundle in current module and parent modules module = cls.__module__ - path = mod_path / f'{module}.bundle.js' - if path.is_file(): - return path - elif module in sys.modules: + modules = module.split('.') + for i in reversed(range(len(modules))): + submodule = '.'.join(modules[:i+1]) + try: + mod = importlib.import_module(submodule) + except (ModuleNotFoundError, ImportError): + continue + if not hasattr(mod, '__file__'): + continue + submodule_path = pathlib.Path(mod.__file__).parent + path = submodule_path / f'{submodule}.bundle.js' + if path.is_file(): + return path + + if module in sys.modules: module = os.path.basename(sys.modules[module].__file__).replace('.py', '') path = mod_path / f'{module}.bundle.js' return path if path.is_file() else None @@ -300,7 +328,7 @@ def _render_esm(cls, compiled: bool | Literal['compiling'] = True, server: bool if esm_path: if esm_path == cls._bundle_path and cls.__module__ in sys.modules and server: base_cls = cls - for scls in cls.__mro__[1:][::-1]: + for scls in cls.__mro__[1:]: if not issubclass(scls, ReactiveESM): continue if esm_path == scls._esm_path(compiled=compiled is True): @@ -391,6 +419,7 @@ def _get_properties(self, doc: Document) -> dict[str, Any]: else: bundle_hash = None data_props = self._process_param_change(data_params) + data_props['esm_constants'] = self._constants props.update({ 'bundle': bundle_hash, 'class_name': camel_to_kebab(cls.__name__), @@ -406,24 +435,26 @@ def _get_properties(self, doc: Document) -> dict[str, Any]: def _process_importmap(cls): return cls._importmap + def _get_child_model(self, child, doc, root, parent, comm): + if child is None: + return None + ref = root.ref['id'] + if isinstance(child, list): + return [ + sv._models[ref][0] if ref in sv._models else sv._get_model(doc, root, parent, comm) + for sv in child + ] + elif ref in child._models: + return child._models[ref][0] + return child._get_model(doc, root, parent, comm) + def _get_children(self, data_model, doc, root, parent, comm): children = {} - ref = root.ref['id'] for k, v in self.param.values().items(): p = self.param[k] if not is_viewable_param(p): continue - if v is None: - children[k] = None - elif isinstance(v, list): - children[k] = [ - sv._models[ref][0] if ref in sv._models else sv._get_model(doc, root, parent, comm) - for sv in v - ] - elif ref in v._models: - children[k] = v._models[ref][0] - else: - children[k] = v._get_model(doc, root, parent, comm) + children[k] = self._get_child_model(v, doc, root, parent, comm) return children def _setup_autoreload(self): @@ -673,6 +704,7 @@ def _process_importmap(cls): "react-is": f"https://esm.sh/react-is@{v_react}&external=react", "@emotion/cache": f"https://esm.sh/@emotion/cache?deps=react@{v_react},react-dom@{v_react}", "@emotion/react": f"https://esm.sh/@emotion/react?deps=react@{v_react},react-dom@{v_react}&external=react,react-is", + "@emotion/styled": f"https://esm.sh/@emotion/styled?deps=react@{v_react},react-dom@{v_react}&external=react,react-is", }) for k, v in imports.items(): if '?' not in v and 'esm.sh' in v: diff --git a/panel/io/compile.py b/panel/io/compile.py index d69211d306..6e8beba647 100644 --- a/panel/io/compile.py +++ b/panel/io/compile.py @@ -1,5 +1,6 @@ from __future__ import annotations +import fnmatch import importlib import json import os @@ -10,6 +11,7 @@ import sys import tempfile +from collections import defaultdict from contextlib import contextmanager from typing import TYPE_CHECKING @@ -71,6 +73,62 @@ def check_cli_tool(tool_name): return False +def find_module_bundles(module_spec: str) -> dict[pathlib.PurePath, list[ReactiveESM]]: + """ + Takes module specifications and extracts a set of components to bundle. + + Arguments + --------- + module_spec: str + Module specification either as a dotted module or a path to a module. + + Returns + ------- + Dictionary containing the bundle paths and list of components to bundle. + """ + # Split module spec, while respecting Windows drive letters + if ':' in module_spec and (module_spec[1:3] != ':\\' or module_spec.count(':') > 1): + module, cls = module_spec.rsplit(':', 1) + else: + module = module_spec + cls = '' + classes = cls.split(',') if cls else None + if module.endswith('.py'): + module_name, _ = os.path.splitext(os.path.basename(module)) + else: + module_name = module + try: + components = find_components(module, classes) + except ValueError: + cls_error = f' and that class(es) {cls!r} are defined therein' if cls else '' + raise RuntimeError( # noqa + f'Could not find any ESM components to compile, ensure ' + f'you provided the right module{cls_error}.' + ) + if module in sys.modules: + module_path = sys.modules[module].__file__ + else: + module_path = module + + bundles = defaultdict(list) + module_path = pathlib.Path(module_path).parent + for component in components: + if component._bundle: + bundle_path = component._bundle + if isinstance(bundle_path, str): + path = (module_path / bundle_path).absolute() + else: + path = bundle_path.absolute() + bundles[str(path)].append(component) + elif len(components) > 1 and not classes: + component_module = module_name or component.__module__ + bundles[module_path / f'{component_module}.bundle.js'].append(component) + else: + bundles[component._module_path / f'{component.__name__}.bundle.js'].append(component) + + return bundles + + def find_components(module_or_file: str | os.PathLike, classes: list[str] | None = None) -> list[type[ReactiveESM]]: """ Creates a temporary module given a path-like object and finds all @@ -94,6 +152,10 @@ def find_components(module_or_file: str | os.PathLike, classes: list[str] | None runner = CodeRunner(source, module_or_file, []) module = runner.new_module() runner.run(module) + if runner.error: + raise RuntimeError( + f'Compilation failed because supplied module errored on import:\n\n{runner.error}' + ) else: module = importlib.import_module(module_or_file) classes = classes or [] @@ -103,12 +165,12 @@ def find_components(module_or_file: str | os.PathLike, classes: list[str] | None isinstance(v, type) and issubclass(v, ReactiveESM) and not v.abstract and - (not classes or v.__name__ in classes) + (not classes or any(fnmatch.fnmatch(v.__name__, p) for p in classes)) ): if py_file: v.__path__ = path_obj.parent.absolute() components.append(v) - not_found = set(classes) - set(c.__name__ for c in components) + not_found = {cls for cls in classes if '*' not in cls} - set(c.__name__ for c in components) if classes and not_found: clss = ', '.join(map(repr, not_found)) raise ValueError(f'{clss} class(es) not found in {module_or_file!r}.') diff --git a/panel/io/datamodel.py b/panel/io/datamodel.py index 1489b012b3..b5a080ed03 100644 --- a/panel/io/datamodel.py +++ b/panel/io/datamodel.py @@ -115,7 +115,7 @@ def bytes_param(p, kwargs): } -def construct_data_model(parameterized, name=None, ignore=[], types={}): +def construct_data_model(parameterized, name=None, ignore=[], types={}, extras={}): """ Dynamically creates a Bokeh DataModel class from a Parameterized object. @@ -132,6 +132,8 @@ def construct_data_model(parameterized, name=None, ignore=[], types={}): types: dict A dictionary mapping from parameter name to a Parameter type, making it possible to override the default parameter types. + extras: dict + Additional properties to define on the DataModel. Returns ------- @@ -163,6 +165,10 @@ def construct_data_model(parameterized, name=None, ignore=[], types={}): for bkp, convert in accepts: bk_prop = bk_prop.accepts(bkp, convert) properties[pname] = bk_prop + for pname, ptype in extras.items(): + if issubclass(ptype, pm.Parameter): + ptype = PARAM_MAPPING.get(ptype)(None, {}) + properties[pname] = ptype name = name or parameterized.name return type(name, (DataModel,), properties) diff --git a/panel/layout/base.py b/panel/layout/base.py index a7a639f3bf..0c86d1bbab 100644 --- a/panel/layout/base.py +++ b/panel/layout/base.py @@ -563,8 +563,9 @@ def __init__(self, *items: list[Any | tuple[str, Any]], **params: Any): 'as positional arguments or as a keyword, not both.' ) items = params.pop('objects') - params['objects'], self._names = self._to_objects_and_names(items) + params['objects'], names = self._to_objects_and_names(items) super().__init__(**params) + self._names = names self._panels = defaultdict(dict) self.param.watch(self._update_names, 'objects') # ALERT: Ensure that name update happens first, should be diff --git a/panel/reactive.py b/panel/reactive.py index 7dbc3d2041..91a38f52b8 100644 --- a/panel/reactive.py +++ b/panel/reactive.py @@ -214,7 +214,7 @@ def _process_param_change(self, msg: dict[str, Any]) -> dict[str, Any]: stylesheets += properties['stylesheets'] wrapped = [] for stylesheet in stylesheets: - if isinstance(stylesheet, str) and stylesheet.split('?')[0].endswith('.css'): + if isinstance(stylesheet, str) and (stylesheet.split('?')[0].endswith('.css') or stylesheet.startswith('http')): cache = (state._stylesheets if state.curdoc else {}).get(state.curdoc, {}) if stylesheet in cache: stylesheet = cache[stylesheet] diff --git a/panel/tests/io/test_compile.py b/panel/tests/io/test_compile.py index a30c104399..3db35446f7 100644 --- a/panel/tests/io/test_compile.py +++ b/panel/tests/io/test_compile.py @@ -1,10 +1,28 @@ +import pathlib + import pytest +from panel.custom import ReactiveESM from panel.io.compile import ( - packages_from_code, packages_from_importmap, replace_imports, + find_module_bundles, packages_from_code, packages_from_importmap, + replace_imports, ) +class ESM1(ReactiveESM): + pass + + +def test_find_module_bundles_as_dotted_module(): + assert find_module_bundles('panel.tests.io.test_compile') == {pathlib.Path(__file__).parent / 'ESM1.bundle.js': [ESM1]} + +def test_find_module_bundles_as_path(): + path = pathlib.Path(__file__).parent / 'ESM1.bundle.js' + bundles = find_module_bundles(__file__) + assert path in bundles + assert len(bundles[path]) == 1 + assert bundles[path][0].name == 'ESM1' + def test_packages_from_code_esm_sh(): code, pkgs = packages_from_code('import * from "https://esm.sh/confetti-canvas@1.0.0"') assert code == 'import * from "confetti-canvas"' From bfc5859a660fed8bb8e4ac23d6d57c1441c1274e Mon Sep 17 00:00:00 2001 From: Maximilian Lattka Date: Fri, 8 Nov 2024 13:14:35 +0100 Subject: [PATCH 19/44] Fix #7380: Preview Panel with notifications enabled (#7466) --- panel/io/resources.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/panel/io/resources.py b/panel/io/resources.py index 35776a214c..fdf5ac13d2 100644 --- a/panel/io/resources.py +++ b/panel/io/resources.py @@ -258,7 +258,11 @@ def component_resource_path(component, attr, path): component_path = COMPONENT_PATH if state.rel_path: component_path = f"{state.rel_path}/{component_path}" - rel_path = os.fspath(resolve_custom_path(component, path, relative=True)).replace(os.path.sep, '/') + custom_path = resolve_custom_path(component, path, relative=True) + if custom_path: + rel_path = os.fspath(custom_path).replace(os.path.sep, '/') + else: + rel_path = path return f'{component_path}{component.__module__}/{component.__name__}/{attr}/{rel_path}' def patch_stylesheet(stylesheet, dist_url): @@ -641,6 +645,8 @@ def extra_resources(self, resources, resource_type): if not (getattr(model, resource_type, None) and model._loaded()): continue for resource in getattr(model, resource_type, []): + if state.rel_path: + resource = resource.lstrip(state.rel_path+'/') if not isurl(resource) and not resource.lstrip('./').startswith('static/extensions'): resource = component_resource_path(model, resource_type, resource) if resource not in resources: @@ -815,6 +821,8 @@ def js_modules(self): if not (getattr(model, '__javascript_modules__', None) and model._loaded()): continue for js_module in model.__javascript_modules__: + if state.rel_path: + js_module = js_module.lstrip(state.rel_path+'/') if not isurl(js_module) and not js_module.startswith('static/extensions'): js_module = component_resource_path(model, '__javascript_modules__', js_module) if js_module not in modules: From cf9dc6bb4f3ca2ec4eaa1473e3ab86a10de51dd8 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Fri, 8 Nov 2024 15:38:35 +0100 Subject: [PATCH 20/44] Bump panel.js to 1.5.4-a.1 --- panel/package-lock.json | 4 ++-- panel/package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/panel/package-lock.json b/panel/package-lock.json index 1ef564d22f..73f5695f71 100644 --- a/panel/package-lock.json +++ b/panel/package-lock.json @@ -1,12 +1,12 @@ { "name": "@holoviz/panel", - "version": "1.5.3", + "version": "1.5.4-a.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@holoviz/panel", - "version": "1.5.3", + "version": "1.5.4-a.1", "license": "BSD-3-Clause", "dependencies": { "@bokeh/bokehjs": "3.6.0", diff --git a/panel/package.json b/panel/package.json index 2d0de61073..70dcb1e993 100644 --- a/panel/package.json +++ b/panel/package.json @@ -1,6 +1,6 @@ { "name": "@holoviz/panel", - "version": "1.5.3", + "version": "1.5.4-a.1", "description": "The powerful data exploration & web app framework for Python.", "license": "BSD-3-Clause", "repository": { From bddcf356daf0a0a08eab93e46fa46edd50294774 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Mon, 11 Nov 2024 16:36:05 +0100 Subject: [PATCH 21/44] Fix issue collecting compile bundles (#7477) --- panel/command/compile.py | 4 ++-- panel/tests/command/test_compile.py | 25 +++++++++++++++++++++++++ pixi.toml | 1 + 3 files changed, 28 insertions(+), 2 deletions(-) create mode 100644 panel/tests/command/test_compile.py diff --git a/panel/command/compile.py b/panel/command/compile.py index 332a1203f3..9b6421df83 100644 --- a/panel/command/compile.py +++ b/panel/command/compile.py @@ -50,9 +50,9 @@ def invoke(self, args): ) for bundle, components in module_bundles.items(): if bundle in bundles: - bundles[bundle] = components - else: bundles[bundle] += components + else: + bundles[bundle] = components errors = 0 for bundle, components in bundles.items(): diff --git a/panel/tests/command/test_compile.py b/panel/tests/command/test_compile.py new file mode 100644 index 0000000000..b562b790f6 --- /dev/null +++ b/panel/tests/command/test_compile.py @@ -0,0 +1,25 @@ +import pathlib +import subprocess +import sys + +from panel.custom import JSComponent + +CWD = pathlib.Path(__file__).parent + + +class JSTestComponent(JSComponent): + + _esm = "export function render() { console.log('foo') }" + + +def test_compile_component(py_file): + cmd = [sys.executable, "-m", "panel", "compile", "panel.tests.command.test_compile:JSTestComponent", "--unminified"] + p = subprocess.Popen(cmd, shell=False, cwd=CWD) + p.wait() + + bundle = CWD / "JSTestComponent.bundle.js" + try: + assert bundle.is_file() + assert 'function render() {\n console.log("foo");\n}' in bundle.read_text() + finally: + bundle.unlink() diff --git a/pixi.toml b/pixi.toml index af37a2df46..9cf82be3f5 100644 --- a/pixi.toml +++ b/pixi.toml @@ -25,6 +25,7 @@ lite = ["py311", "lite"] [dependencies] nodejs = ">=18" +esbuild = "*" nomkl = "*" pip = "*" # Required From 89b0bc616e2646a1c705f35ec44d81ae630b52ac Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Mon, 11 Nov 2024 17:53:55 +0100 Subject: [PATCH 22/44] Combine throttle implementations (#7480) --- panel/models/html.ts | 24 +----------------------- panel/models/util.ts | 28 +++++++++++++++++++++------- 2 files changed, 22 insertions(+), 30 deletions(-) diff --git a/panel/models/html.ts b/panel/models/html.ts index a844b00fe9..42744b4e8d 100644 --- a/panel/models/html.ts +++ b/panel/models/html.ts @@ -6,6 +6,7 @@ import {entries} from "@bokehjs/core/util/object" import {Markup} from "@bokehjs/models/widgets/markup" import {PanelMarkupView} from "./layout" import {serializeEvent} from "./event-to-object" +import {throttle} from "./util" import html_css from "styles/models/html.css" @@ -79,29 +80,6 @@ export function run_scripts(node: Element): void { } } -function throttle(func: Function, limit: number): any { - let lastFunc: ReturnType | undefined - let lastRan: number - - return function(...args: any) { - // @ts-ignore - const context = this - - if (!lastRan) { - func.apply(context, args) - lastRan = Date.now() - } else { - clearTimeout(lastFunc) - lastFunc = setTimeout(function() { - if ((Date.now() - lastRan) >= limit) { - func.apply(context, args) - lastRan = Date.now() - } - }, limit - (Date.now() - lastRan)) - } - } -} - export class HTMLView extends PanelMarkupView { declare model: HTML _buffer: string | null = null diff --git a/panel/models/util.ts b/panel/models/util.ts index 50a70eba6e..5cdac07a98 100644 --- a/panel/models/util.ts +++ b/panel/models/util.ts @@ -11,13 +11,27 @@ export const get = (obj: any, path: string, defaultValue: any = undefined) => { return result === undefined || result === obj ? defaultValue : result } -export function throttle(func: any, timeFrame: number) { - let lastTime: number = 0 - return function() { - const now: number = Number(new Date()) - if (now - lastTime >= timeFrame) { - func() - lastTime = now +export function throttle(func: Function, limit: number): any { + let lastRan: number = 0 + let trailingCall: ReturnType | null = null + + return function(...args: any) { + // @ts-ignore + const context = this + const now = Date.now() + if (trailingCall) { + clearTimeout(trailingCall) + } + + if ((now - lastRan) >= limit) { + func.apply(context, args) + lastRan = Date.now() + } else { + trailingCall = setTimeout(function() { + func.apply(context, args) + lastRan = Date.now() + trailingCall = null + }, limit - (now - lastRan)) } } } From ce938bf6b087a1e64bbdad3503fecd9300264ef4 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Mon, 11 Nov 2024 17:55:01 +0100 Subject: [PATCH 23/44] Bump panel.js version to 1.5.4 alpha2 --- panel/package-lock.json | 4 ++-- panel/package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/panel/package-lock.json b/panel/package-lock.json index 73f5695f71..2bf2076f66 100644 --- a/panel/package-lock.json +++ b/panel/package-lock.json @@ -1,12 +1,12 @@ { "name": "@holoviz/panel", - "version": "1.5.4-a.1", + "version": "1.5.4-a.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@holoviz/panel", - "version": "1.5.4-a.1", + "version": "1.5.4-a.2", "license": "BSD-3-Clause", "dependencies": { "@bokeh/bokehjs": "3.6.0", diff --git a/panel/package.json b/panel/package.json index 70dcb1e993..59981229c8 100644 --- a/panel/package.json +++ b/panel/package.json @@ -1,6 +1,6 @@ { "name": "@holoviz/panel", - "version": "1.5.4-a.1", + "version": "1.5.4-a.2", "description": "The powerful data exploration & web app framework for Python.", "license": "BSD-3-Clause", "repository": { From f08c876eaa0b6a2b892e35c87a18397fa0f3c79a Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Tue, 12 Nov 2024 12:05:23 +0100 Subject: [PATCH 24/44] Take manual control over Plotly sizing (#7483) * Take manual control over Plotly sizing * Fix lint --- panel/models/plotly.ts | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/panel/models/plotly.ts b/panel/models/plotly.ts index 3f16accefd..900b5f515f 100644 --- a/panel/models/plotly.ts +++ b/panel/models/plotly.ts @@ -140,6 +140,12 @@ export class PlotlyPlotView extends HTMLBoxView { _end_relayouting = debounce(() => { this._relayouting = false }, 2000, false) + _throttled_resize: any + + override initialize(): void { + super.initialize() + this._throttled_resize = throttle(() => this.resize_layout(), 25) + } override connect_signals(): void { super.connect_signals() @@ -205,28 +211,33 @@ export class PlotlyPlotView extends HTMLBoxView { this.container = div() as PlotlyHTMLElement set_size(this.container, this.model) this._rendered = false - this.shadow_el.appendChild(this.container) this.watch_stylesheets() - this.plot().then(() => { + this.plot(true).then(() => { + this.shadow_el.appendChild(this.container) this._rendered = true + this.resize_layout() if (this.model.relayout != null) { (window as any).Plotly.relayout(this.container, this.model.relayout) } - (window as any).Plotly.Plots.resize(this.container) }) } override style_redraw(): void { - if (this._rendered && this.container != null) { - (window as any).Plotly.Plots.resize(this.container) + this.resize_layout() + } + + resize_layout(): void { + if (!this._rendered || this.container == null) { + return } + const width: number = Math.min(this.model.width || this.el.clientWidth, this.model.max_width || Infinity) + const height: number = Math.min(this.model.height || this.el.clientHeight, this.model.max_height || Infinity); + (window as any).Plotly.relayout(this.container, {width, height}) } override after_layout(): void { super.after_layout() - if (this._rendered && this.container != null) { - (window as any).Plotly.Plots.resize(this.container) - } + this._throttled_resize() } _trace_data(): any { From 54c9416bce9a965db95d2e4d33885ba5071a0d61 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Tue, 12 Nov 2024 18:42:14 +0100 Subject: [PATCH 25/44] Add CHANGELOG for 1.5.4 (#7486) --- CHANGELOG.md | 25 +++++++++++++++++++++++++ doc/about/releases.md | 25 +++++++++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5917acf639..3ab2afa215 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,30 @@ # Releases +## Version 1.5.4 + +This release primarily focuses on improving the ESM components including fixes for serialization of parameter values, improvements for compiling bundles, and building custom layouts. Additionally this release includes the new `DatetimeSlider`, adds a copy button to codeblocks in Markdown panes and starts adding better support for Polars. Many thanks and a warm welcome to our new contributor @MP-MaximilianLattka as well as our maintainer team, including @Hoxbro, @thuydotm, @ahuang11, @MarcSkovMadsen and @philippjfr. + +### Enhancements + +- Add `DatetimeSlider` widget ([#7374](https://github.com/holoviz/panel/pull/7374)) +- Improve Jupyter preview error handling ([#7434](https://github.com/holoviz/panel/pull/7434)) +- Add copy button to `Markdown` codeblocks ([#7451](https://github.com/holoviz/panel/pull/7451)) +- Various improvements for writing ESM components ([#7462](https://github.com/holoviz/panel/pull/7462)) +- Log authorization callback errors ([#7463](https://github.com/holoviz/panel/pull/7463)) +- Support polars in `pn.cache` ([#7472](https://github.com/holoviz/panel/pull/7472)) +- Improve and document `hold` utility ([#7474](https://github.com/holoviz/panel/pull/7474)) +- Improve how `panel compile` collects bundles ([#7477](https://github.com/holoviz/panel/pull/7477)) + +### Bug fixes + +- Fix issues detecting changed property values during serialization ([#7432](https://github.com/holoviz/panel/pull/7432)) +- Ensure ESM compilation correctly detects file extension ([#7446](https://github.com/holoviz/panel/pull/7446)) +- Ensure parameter overrides are applied to ESM components ([#7452](https://github.com/holoviz/panel/pull/7452)) +- Ensure component `Children` parameter correctly resolves when multiple types are defined ([#7454](https://github.com/holoviz/panel/pull/7454)) +- Fix issues using Jupyter Preview with notifications enabled ([#7466](https://github.com/holoviz/panel/pull/7466)) +- Ensure `HTML`/`Markdown` streaming does not freeze during rapid updates ([#7480](https://github.com/holoviz/panel/pull/7480)) +- Ensure `Plotly` sizes correctly on initial render ([#7483](https://github.com/holoviz/panel/pull/7483)) + ## Version 1.5.3 This release fixes a number of smaller regressions related to `Tabulator` `row_content`, ensures `--dev`/`--autoreload` picks up on external modules correctly and resolves OAuth guest endpoints correctly. Additionally it introduces some enhancements and bug fixes for custom components, such as adding support for loading custom components ESM Javascript bundles from the inbuilt endpoint ensuring that the bundle can be cached by the browser. Many thanks and welcome to our new contributors @chryshumble and @haojungc, our returning contributors @TheoMathurin, @aktech and @Coderambling and the core maintainer team @Hoxbro, @ahuang11, @MarcSkovMadsen and @philippjfr for their contributions to this release. diff --git a/doc/about/releases.md b/doc/about/releases.md index e7e919821f..c97a73cfbf 100644 --- a/doc/about/releases.md +++ b/doc/about/releases.md @@ -2,6 +2,31 @@ See [the HoloViz blog](https://blog.holoviz.org/#category=panel) for a visual summary of the major features added in each release. +## Version 1.5.4 + +This release primarily focuses on improving the ESM components including fixes for serialization of parameter values, improvements for compiling bundles, and building custom layouts. Additionally this release includes the new `DatetimeSlider`, adds a copy button to codeblocks in Markdown panes and starts adding better support for Polars. Many thanks and a warm welcome to our new contributor @MP-MaximilianLattka as well as our maintainer team, including @Hoxbro, @thuydtom, @ahuang11, @MarcSkovMadsen and @philippjfr. + +### Enhancements + +- Add `DatetimeSlider` widget ([#7374](https://github.com/holoviz/panel/pull/7374)) +- Improve Jupyter preview error handling ([#7434](https://github.com/holoviz/panel/pull/7434)) +- Add copy button to `Markdown` codeblocks ([#7451](https://github.com/holoviz/panel/pull/7451)) +- Various improvements for writing ESM components ([#7462](https://github.com/holoviz/panel/pull/7462)) +- Log authorization callback errors ([#7463](https://github.com/holoviz/panel/pull/7463)) +- Support polars in `pn.cache` ([#7472](https://github.com/holoviz/panel/pull/7472)) +- Improve and document `hold` utility ([#7474](https://github.com/holoviz/panel/pull/7474)) +- Improve how `panel compile` collects bundles ([#7477](https://github.com/holoviz/panel/pull/7477)) + +### Bug fixes + +- Fix issues detecting changed property values during serialization ([#7432](https://github.com/holoviz/panel/pull/7432)) +- Ensure ESM compilation correctly detects file extension ([#7446](https://github.com/holoviz/panel/pull/7446)) +- Ensure parameter overrides are applied to ESM components ([#7452](https://github.com/holoviz/panel/pull/7452)) +- Ensure component `Children` parameter correctly resolves when multiple types are defined ([#7454](https://github.com/holoviz/panel/pull/7454)) +- Fix issues using Jupyter Preview with notifications enabled ([#7466](https://github.com/holoviz/panel/pull/7466)) +- Ensure `HTML`/`Markdown` streaming does not freeze during rapid updates ([#7480](https://github.com/holoviz/panel/pull/7480)) +- Ensure `Plotly` sizes correctly on initial render ([#7483](https://github.com/holoviz/panel/pull/7483)) + ## Version 1.5.3 This release fixes a number of smaller regressions related to `Tabulator` `row_content`, ensures `--dev`/`--autoreload` picks up on external modules correctly and resolves OAuth guest endpoints correctly. Additionally it introduces some enhancements and bug fixes for custom components, such as adding support for loading custom components ESM Javascript bundles from the inbuilt endpoint ensuring that the bundle can be cached by the browser. Many thanks and welcome to our new contributors @chryshumble and @haojungc, our returning contributors @TheoMathurin, @aktech and @Coderambling and the core maintainer team @Hoxbro, @ahuang11, @MarcSkovMadsen and @philippjfr for their contributions to this release. From adf2774ff60ac0aef431d5e40e46fe9ab5211055 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Tue, 12 Nov 2024 23:06:33 +0100 Subject: [PATCH 26/44] Do not attempt to __reduce__ when hashing classes in cache (#7484) * Do not attempt to __reduce__ when hashing classes in cache * Add hash * Fix tests and add changelog --- CHANGELOG.md | 2 +- doc/about/releases.md | 1 + panel/io/cache.py | 2 +- panel/tests/io/test_cache.py | 18 +++++++++++------- 4 files changed, 14 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ab2afa215..1a4f268ad5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,7 +46,7 @@ This release fixes a number of smaller regressions related to `Tabulator` `row_c - Do not mutate layout `Children` inplace ([#7417](https://github.com/holoviz/panel/pull/7403)) - Set `Tabulator` null formatter to empty string ([#7421](https://github.com/holoviz/panel/pull/7421)) - Ensure Tabulator table content does not overflow ([#7425](https://github.com/holoviz/panel/pull/7425)) - +- Ensure `cache` handles hashing of classes and instances correctly ([#7478](https://github.com/holoviz/panel/issues/7478)) ### Compatibility diff --git a/doc/about/releases.md b/doc/about/releases.md index c97a73cfbf..91154380c1 100644 --- a/doc/about/releases.md +++ b/doc/about/releases.md @@ -48,6 +48,7 @@ This release fixes a number of smaller regressions related to `Tabulator` `row_c - Do not mutate layout `Children` inplace ([#7417](https://github.com/holoviz/panel/pull/7403)) - Set `Tabulator` null formatter to empty string ([#7421](https://github.com/holoviz/panel/pull/7421)) - Ensure Tabulator table content does not overflow ([#7425](https://github.com/holoviz/panel/pull/7425), [#7431](https://github.com/holoviz/panel/pull/7431)) +- Ensure `cache` handles hashing of classes and instances correctly ([#7478](https://github.com/holoviz/panel/issues/7478)) ### Compatibility diff --git a/panel/io/cache.py b/panel/io/cache.py index 5392e53d95..db51f41572 100644 --- a/panel/io/cache.py +++ b/panel/io/cache.py @@ -266,7 +266,7 @@ def _generate_hash_inner(obj): f'{obj!r} with following error: {type(e).__name__}("{e}").' ) from e return output - if hasattr(obj, '__reduce__'): + if hasattr(obj, '__reduce__') and inspect.isclass(obj): h = hashlib.new("md5") try: reduce_data = obj.__reduce__() diff --git a/panel/tests/io/test_cache.py b/panel/tests/io/test_cache.py index e4aa495520..9031702b27 100644 --- a/panel/tests/io/test_cache.py +++ b/panel/tests/io/test_cache.py @@ -17,9 +17,7 @@ diskcache = None diskcache_available = pytest.mark.skipif(diskcache is None, reason="requires diskcache") -from panel.io.cache import ( - _find_hash_func, _generate_hash, cache, is_equal, -) +from panel.io.cache import _generate_hash, cache, is_equal from panel.io.state import set_curdoc, state from panel.tests.util import serve_and_wait @@ -28,7 +26,7 @@ ################ def hashes_equal(v1, v2): - a, b = _find_hash_func(v1)(v1), _find_hash_func(v2)(v2) + a, b = _generate_hash(v1), _generate_hash(v2) return a == b def test_str_hash(): @@ -52,6 +50,11 @@ def test_none_hash(): assert hashes_equal(None, None) assert not hashes_equal(None, False) +def test_object_hash(): + obj1, obj2 = object(), object() + assert hashes_equal(obj1, obj1) + assert not hashes_equal(obj1, obj2) + def test_bytes_hash(): assert hashes_equal(b'0', b'0') assert not hashes_equal(b'0', b'1') @@ -70,10 +73,11 @@ def test_list_hash(): assert not hashes_equal([0], [1]) assert not hashes_equal(['a', ['b']], ['a', ['c']]) +def test_list_hash_recursive(): # Recursion l = [0] l.append(l) - assert hashes_equal(l, list(l)) + assert hashes_equal(list(l), list(l)) def test_tuple_hash(): assert hashes_equal((0,), (0,)) @@ -88,10 +92,10 @@ def test_dict_hash(): assert not hashes_equal({'a': 0}, {'a': 1}) assert not hashes_equal({'a': {'b': 0}}, {'a': {'b': 1}}) - # Recursion +def test_dict_hash_recursive(): d = {'a': {}} d['a'] = d - assert hashes_equal(d, dict(d)) + assert hashes_equal(dict(d), dict(d)) def test_stringio_hash(): sio1, sio2 = io.StringIO(), io.StringIO() From cd645dd5feecf8e56e5ada2625431fffe2059520 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Tue, 12 Nov 2024 23:52:51 +0100 Subject: [PATCH 27/44] Bump panel.js version to 1.5.4 --- CHANGELOG.md | 2 +- doc/about/releases.md | 2 +- panel/package-lock.json | 4 ++-- panel/package.json | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a4f268ad5..1ecdb0b7e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## Version 1.5.4 -This release primarily focuses on improving the ESM components including fixes for serialization of parameter values, improvements for compiling bundles, and building custom layouts. Additionally this release includes the new `DatetimeSlider`, adds a copy button to codeblocks in Markdown panes and starts adding better support for Polars. Many thanks and a warm welcome to our new contributor @MP-MaximilianLattka as well as our maintainer team, including @Hoxbro, @thuydotm, @ahuang11, @MarcSkovMadsen and @philippjfr. +This release primarily focuses on improving the ESM components including fixes for serialization of parameter values, improvements for compiling bundles, and building custom layouts. Additionally this release includes the new `DatetimeSlider`, adds a copy button to codeblocks in `Markdown` panes, improves responsive sizing for Plotly and starts adding better support for Polars. Many thanks and a warm welcome to our new contributor @MP-MaximilianLattka as well as our maintainer team, including @Hoxbro, @thuydotm, @ahuang11, @MarcSkovMadsen and @philippjfr. ### Enhancements diff --git a/doc/about/releases.md b/doc/about/releases.md index 91154380c1..d1bf5601ac 100644 --- a/doc/about/releases.md +++ b/doc/about/releases.md @@ -4,7 +4,7 @@ See [the HoloViz blog](https://blog.holoviz.org/#category=panel) for a visual su ## Version 1.5.4 -This release primarily focuses on improving the ESM components including fixes for serialization of parameter values, improvements for compiling bundles, and building custom layouts. Additionally this release includes the new `DatetimeSlider`, adds a copy button to codeblocks in Markdown panes and starts adding better support for Polars. Many thanks and a warm welcome to our new contributor @MP-MaximilianLattka as well as our maintainer team, including @Hoxbro, @thuydtom, @ahuang11, @MarcSkovMadsen and @philippjfr. +This release primarily focuses on improving the ESM components including fixes for serialization of parameter values, improvements for compiling bundles, and building custom layouts. Additionally this release includes the new `DatetimeSlider`, adds a copy button to codeblocks in `Markdown` panes, improves responsive sizing for Plotly and starts adding better support for Polars. Many thanks and a warm welcome to our new contributor @MP-MaximilianLattka as well as our maintainer team, including @Hoxbro, @thuydotm, @ahuang11, @MarcSkovMadsen and @philippjfr. ### Enhancements diff --git a/panel/package-lock.json b/panel/package-lock.json index 2bf2076f66..0de8d43ecc 100644 --- a/panel/package-lock.json +++ b/panel/package-lock.json @@ -1,12 +1,12 @@ { "name": "@holoviz/panel", - "version": "1.5.4-a.2", + "version": "1.5.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@holoviz/panel", - "version": "1.5.4-a.2", + "version": "1.5.4", "license": "BSD-3-Clause", "dependencies": { "@bokeh/bokehjs": "3.6.0", diff --git a/panel/package.json b/panel/package.json index 59981229c8..c5d53992f2 100644 --- a/panel/package.json +++ b/panel/package.json @@ -1,6 +1,6 @@ { "name": "@holoviz/panel", - "version": "1.5.4-a.2", + "version": "1.5.4", "description": "The powerful data exploration & web app framework for Python.", "license": "BSD-3-Clause", "repository": { From 9766b6b38abfc1f8c107c5d0d443347265c1c4b5 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Wed, 13 Nov 2024 16:37:03 +0100 Subject: [PATCH 28/44] Ensure FileDownload label text updates correctly (#7489) --- panel/models/file_download.ts | 7 ++++--- panel/tests/ui/widgets/test_misc.py | 16 ++++++++++++++++ 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/panel/models/file_download.ts b/panel/models/file_download.ts index 210964cc92..993225ad94 100644 --- a/panel/models/file_download.ts +++ b/panel/models/file_download.ts @@ -38,6 +38,7 @@ export class FileDownloadView extends InputWidgetView { anchor_el: HTMLAnchorElement button_el: HTMLButtonElement + label_el: Text declare input_el: HTMLInputElement // HACK: So this.input_el.id = "input" can be set in Bokeh 3.4 _downloadable: boolean = false _click_listener: any @@ -101,8 +102,9 @@ export class FileDownloadView extends InputWidgetView { this.anchor_el.appendChild(separator) this.icon_view.render() } + this.label_el = document.createTextNode(this.model.label) + this.anchor_el.appendChild(this.label_el) this._update_button_style() - this._update_label() // Changing the disabled property calls render() so it needs to be handled here. // This callback is inherited from ControlView in bokehjs. @@ -201,8 +203,7 @@ export class FileDownloadView extends InputWidgetView { } _update_label(): void { - const label = document.createTextNode(this.model.label) - this.anchor_el.appendChild(label) + this.label_el.data = this.model.label } _update_button_style(): void { diff --git a/panel/tests/ui/widgets/test_misc.py b/panel/tests/ui/widgets/test_misc.py index 526e77bf85..f8807c907a 100644 --- a/panel/tests/ui/widgets/test_misc.py +++ b/panel/tests/ui/widgets/test_misc.py @@ -5,6 +5,10 @@ import param import pytest +pytest.importorskip("playwright") + +from playwright.sync_api import expect + from panel.layout import Column, Tabs from panel.tests.util import serve_component from panel.widgets import FileDownload, TextInput @@ -13,6 +17,18 @@ not_windows = pytest.mark.skipif(sys.platform=='win32', reason="Does not work on Windows") +def test_file_download_label_updates(page): + + download = FileDownload(filename='f.txt', embed=False, callback=lambda: io.StringIO()) + + serve_component(page, download) + + expect(page.locator('.bk-btn a')).to_have_text('Download f.txt') + + download.filename = 'g.txt' + + expect(page.locator('.bk-btn a')).to_have_text('Download g.txt') + @not_windows def test_file_download_updates_when_navigating_between_dynamic_tabs(page): text_input = TextInput(value='abc') From 1c5abb937e8dfba3864e5d2011a8cbc5441b4b28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Thu, 14 Nov 2024 11:46:33 +0100 Subject: [PATCH 29/44] compat: websockets 14 (#7491) --- pyproject.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 64b76cd384..8dc56b9401 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -224,6 +224,9 @@ filterwarnings = [ "ignore:distutils Version classes are deprecated:DeprecationWarning:ipywidgets_bokeh.kernel", # OK "ignore:unclosed file <_io.TextIOWrapper name='(/dev/null|nul)' mode='w':ResourceWarning", # OK "ignore:Deprecated in traitlets 4.1, use the instance .metadata dictionary directly", # OK (ipywidgets internal) + # 2024-11 + "ignore:websockets.legacy is deprecated:DeprecationWarning", # https://github.com/encode/uvicorn/issues/1908 + "ignore:websockets.server.WebSocketServerProtocol is deprecated:DeprecationWarning", # https://github.com/encode/uvicorn/issues/1908 ] [tool.mypy] From 30a4a353c31aff53bf737beb2254bdf6bee26b54 Mon Sep 17 00:00:00 2001 From: Coderambling <159031875+Coderambling@users.noreply.github.com> Date: Thu, 14 Nov 2024 11:47:38 +0100 Subject: [PATCH 30/44] Minor changes in text of hold.md (#7487) --- doc/how_to/performance/hold.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/doc/how_to/performance/hold.md b/doc/how_to/performance/hold.md index 53fedf04b7..3c49250e15 100644 --- a/doc/how_to/performance/hold.md +++ b/doc/how_to/performance/hold.md @@ -10,7 +10,9 @@ The `hold` function is a context manager and decorator that temporarily holds ev ## Using `hold` -If you have a function that updates components and you want to ensure that all updates are held, you can use hold as a decorator, e.g. here we update 100 components at once. If you do not hold then each of these events is sent and applied in series, potentially resulting in visible updates. +### As a decorator + +If you have a function that updates components and you want to ensure that all updates are held, you can use hold as a decorator. E.g. here we update 100 components at once. If you do not hold then each of these events is sent and applied in series, potentially resulting in visible updates. ```{pyodide} import panel as pn @@ -29,7 +31,9 @@ pn.Column(column, button).servable() Applying the hold decorator means all the updates are sent in a single Websocket message and applied on the frontend simultaneously. -Alternatively the `hold` function can be used as a context manager, potentially giving you finer grained control over which events are batched and which are not: +### As a context manager + +Alternatively, the `hold` function can be used as a context manager, potentially giving you finer grained control over which events are batched and which are not: ```{pyodide} import time From e24f44342908b33bf38ab0c4c3e784d50063034a Mon Sep 17 00:00:00 2001 From: Marc Skov Madsen Date: Thu, 14 Nov 2024 11:54:00 +0100 Subject: [PATCH 31/44] add copy button pointer (#7490) --- panel/styles/models/html.less | 1 + 1 file changed, 1 insertion(+) diff --git a/panel/styles/models/html.less b/panel/styles/models/html.less index 31a7fed788..f26e8be056 100644 --- a/panel/styles/models/html.less +++ b/panel/styles/models/html.less @@ -30,4 +30,5 @@ .codehilite:hover .copybtn { opacity: 1; + cursor: pointer; } From 7015943dca27f141dd42fa5b00915ab8906be6be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Fri, 15 Nov 2024 12:22:18 +0100 Subject: [PATCH 32/44] refactor: Update ObjectSelector to Selector (#7493) --- .../custom_components/examples/table_viewer.md | 2 +- doc/how_to/param/dependencies.md | 4 ++-- doc/how_to/param/examples/subobjects.md | 2 +- doc/how_to/param/subobjects.md | 2 +- doc/how_to/param/uis.md | 4 ++-- panel/chat/feed.py | 4 ++-- panel/chat/input.py | 2 +- panel/chat/step.py | 2 +- panel/config.py | 12 ++++++------ panel/layout/base.py | 2 +- panel/layout/grid.py | 2 +- panel/layout/spacer.py | 2 +- panel/layout/tabs.py | 2 +- panel/pane/alert.py | 2 +- panel/pane/echarts.py | 4 ++-- panel/pane/equation.py | 2 +- panel/pane/holoviews.py | 8 ++++---- panel/pane/ipywidget.py | 2 +- panel/pane/markup.py | 4 ++-- panel/pane/perspective.py | 4 ++-- panel/pane/plot.py | 2 +- panel/pane/vega.py | 2 +- panel/template/base.py | 2 +- panel/template/react/__init__.py | 2 +- panel/tests/layout/test_base.py | 2 +- panel/tests/test_param.py | 6 +++--- panel/viewable.py | 8 ++++---- panel/widgets/button.py | 4 ++-- panel/widgets/codeeditor.py | 2 +- panel/widgets/indicators.py | 18 +++++++++--------- panel/widgets/input.py | 4 ++-- panel/widgets/misc.py | 6 +++--- panel/widgets/player.py | 4 ++-- panel/widgets/slider.py | 4 ++-- panel/widgets/speech_to_text.py | 4 ++-- panel/widgets/tables.py | 8 ++++---- panel/widgets/text_to_speech.py | 4 ++-- 37 files changed, 75 insertions(+), 75 deletions(-) diff --git a/doc/how_to/custom_components/examples/table_viewer.md b/doc/how_to/custom_components/examples/table_viewer.md index ddc43b3c07..35c5d27754 100644 --- a/doc/how_to/custom_components/examples/table_viewer.md +++ b/doc/how_to/custom_components/examples/table_viewer.md @@ -17,7 +17,7 @@ This example demonstrates Panel's reactive programming paradigm using the Param ```{pyodide} class ReactiveTables(Viewer): - dataset = param.ObjectSelector(default='iris', objects=['iris', 'autompg', 'population']) + dataset = param.Selector(default='iris', objects=['iris', 'autompg', 'population']) rows = param.Integer(default=10, bounds=(0, 19)) diff --git a/doc/how_to/param/dependencies.md b/doc/how_to/param/dependencies.md index e5e2da966d..c46b4a1d79 100644 --- a/doc/how_to/param/dependencies.md +++ b/doc/how_to/param/dependencies.md @@ -55,9 +55,9 @@ We also define a ``view`` method that returns an HTML iframe displaying the coun ```{pyodide} class GoogleMapViewer(param.Parameterized): - continent = param.ObjectSelector(default='Asia', objects=['Africa', 'Asia', 'Europe']) + continent = param.Selector(default='Asia', objects=['Africa', 'Asia', 'Europe']) - country = param.ObjectSelector(default='China', objects=['China', 'Thailand', 'Japan']) + country = param.Selector(default='China', objects=['China', 'Thailand', 'Japan']) _countries = {'Africa': ['Ghana', 'Togo', 'South Africa', 'Tanzania'], 'Asia' : ['China', 'Thailand', 'Japan'], diff --git a/doc/how_to/param/examples/subobjects.md b/doc/how_to/param/examples/subobjects.md index 3ba4f0c983..e96faf1f73 100644 --- a/doc/how_to/param/examples/subobjects.md +++ b/doc/how_to/param/examples/subobjects.md @@ -82,7 +82,7 @@ Having defined our basic domain model (of shapes in this case), we can now make ```{pyodide} class ShapeViewer(param.Parameterized): - shape = param.ObjectSelector(default=shapes[0], objects=shapes) + shape = param.Selector(default=shapes[0], objects=shapes) @param.depends('shape', 'shape.param') def view(self): diff --git a/doc/how_to/param/subobjects.md b/doc/how_to/param/subobjects.md index 47b6f06afb..e98ffdcf80 100644 --- a/doc/how_to/param/subobjects.md +++ b/doc/how_to/param/subobjects.md @@ -69,7 +69,7 @@ shapes = [NGon(), Circle()] class ShapeViewer(param.Parameterized): - shape = param.ObjectSelector(default=shapes[0], objects=shapes) + shape = param.Selector(default=shapes[0], objects=shapes) @param.depends('shape') def view(self): diff --git a/doc/how_to/param/uis.md b/doc/how_to/param/uis.md index 676094e8ad..05f0b37f21 100644 --- a/doc/how_to/param/uis.md +++ b/doc/how_to/param/uis.md @@ -81,8 +81,8 @@ class Example(BaseClass): date = param.Date(default=dt.datetime(2017, 1, 1), bounds=(dt.datetime(2017, 1, 1), dt.datetime(2017, 2, 1))) dataframe = param.DataFrame(default=pd.DataFrame({'A': [1, 2, 3]})) - select_string = param.ObjectSelector(default="yellow", objects=["red", "yellow", "green"]) - select_fn = param.ObjectSelector(default=list,objects=[list, set, dict]) + select_string = param.Selector(default="yellow", objects=["red", "yellow", "green"]) + select_fn = param.Selector(default=list,objects=[list, set, dict]) int_list = param.ListSelector(default=[3, 5], objects=[1, 3, 5, 7, 9], precedence=0.5) single_file = param.FileSelector(path='../../*/*.py*', precedence=0.5) multiple_files = param.MultiFileSelector(path='../../*/*.py?', precedence=0.5) diff --git a/panel/chat/feed.py b/panel/chat/feed.py index a0924f46e7..da77313ef4 100644 --- a/panel/chat/feed.py +++ b/panel/chat/feed.py @@ -99,7 +99,7 @@ class ChatFeed(ListPanel): the previous message value `contents`, the previous `user` name, and the component `instance`.""") - callback_exception = param.ObjectSelector( + callback_exception = param.Selector( default="summary", objects=["raise", "summary", "verbose", "ignore"], doc=""" How to handle exceptions raised by the callback. If "raise", the exception will be raised. @@ -199,7 +199,7 @@ class ChatFeed(ListPanel): The placeholder wrapped in a ChatMessage object; primarily to prevent recursion error in _update_placeholder.""") - _callback_state = param.ObjectSelector(objects=list(CallbackState), doc=""" + _callback_state = param.Selector(objects=list(CallbackState), doc=""" The current state of the callback.""") _prompt_trigger = param.Event(doc="Triggers the prompt input.") diff --git a/panel/chat/input.py b/panel/chat/input.py index 78f995234b..602941613c 100644 --- a/panel/chat/input.py +++ b/panel/chat/input.py @@ -63,7 +63,7 @@ class ChatAreaInput(_PnTextAreaInput): of rows the input area can grow.""", ) - resizable = param.ObjectSelector( + resizable = param.Selector( default="height", objects=["both", "width", "height", False], doc=""" diff --git a/panel/chat/step.py b/panel/chat/step.py index a7384f13c2..6b93ae4f6a 100644 --- a/panel/chat/step.py +++ b/panel/chat/step.py @@ -43,7 +43,7 @@ class ChatStep(Card): collapsed_on_success = param.Boolean(default=True, doc=""" Whether to collapse the card on completion.""") - context_exception = param.ObjectSelector( + context_exception = param.Selector( default="raise", objects=["raise", "summary", "verbose", "ignore"], doc=""" How to handle exceptions raised upon exiting the context manager. If "raise", the exception will be raised. diff --git a/panel/config.py b/panel/config.py index 4085177094..6c786e76a5 100644 --- a/panel/config.py +++ b/panel/config.py @@ -203,12 +203,12 @@ class _config(_base_config): information about user sessions. A value of -1 indicates an unlimited history.""") - sizing_mode = param.ObjectSelector(default=None, objects=[ + sizing_mode = param.Selector(default=None, objects=[ 'fixed', 'stretch_width', 'stretch_height', 'stretch_both', 'scale_width', 'scale_height', 'scale_both', None], doc=""" Specify the default sizing mode behavior of panels.""") - template = param.ObjectSelector(default=None, doc=""" + template = param.Selector(default=None, doc=""" The default template to render served applications into.""") throttled = param.Boolean(default=False, doc=""" @@ -222,12 +222,12 @@ class _config(_base_config): default='DEBUG', objects=['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'], doc="Log level of the Admin Panel logger") - _comms = param.ObjectSelector( + _comms = param.Selector( default='default', objects=['default', 'ipywidgets', 'vscode', 'colab'], doc=""" Whether to render output in Jupyter with the default Jupyter extension or use the jupyter_bokeh ipywidget model.""") - _console_output = param.ObjectSelector(default='accumulate', allow_None=True, + _console_output = param.Selector(default='accumulate', allow_None=True, objects=['accumulate', 'replace', 'disable', False], doc=""" How to log errors and stdout output triggered by callbacks @@ -271,7 +271,7 @@ class _config(_base_config): or filepath containing JSON to use with the basic auth provider.""") - _oauth_provider = param.ObjectSelector( + _oauth_provider = param.Selector( default=None, allow_None=True, objects=[], doc=""" Select between a list of authentication providers.""") @@ -310,7 +310,7 @@ class _config(_base_config): Whether to inline JS and CSS resources. If disabled, resources are loaded from CDN if one is available.""") - _theme = param.ObjectSelector(default=None, objects=['default', 'dark'], allow_None=True, doc=""" + _theme = param.Selector(default=None, objects=['default', 'dark'], allow_None=True, doc=""" The theme to apply to components.""") # Global parameters that are shared across all sessions diff --git a/panel/layout/base.py b/panel/layout/base.py index 0c86d1bbab..63f6c96723 100644 --- a/panel/layout/base.py +++ b/panel/layout/base.py @@ -848,7 +848,7 @@ class NamedListPanel(NamedListLike, Panel): active = param.Integer(default=0, bounds=(0, None), doc=""" Index of the currently displayed objects.""") - scroll = param.ObjectSelector( + scroll = param.Selector( default=False, objects=[False, True, "both-auto", "y-auto", "x-auto", "both", "x", "y"], doc="""Whether to add scrollbars if the content overflows the size diff --git a/panel/layout/grid.py b/panel/layout/grid.py index 2039786be1..0c1e9675e1 100644 --- a/panel/layout/grid.py +++ b/panel/layout/grid.py @@ -260,7 +260,7 @@ class GridSpec(Panel): objects = ChildDict(default={}, doc=""" The dictionary of child objects that make up the grid.""") - mode = param.ObjectSelector(default='warn', objects=['warn', 'error', 'override'], doc=""" + mode = param.Selector(default='warn', objects=['warn', 'error', 'override'], doc=""" Whether to warn, error or simply override on overlapping assignment.""") ncols = param.Integer(default=None, bounds=(0, None), doc=""" diff --git a/panel/layout/spacer.py b/panel/layout/spacer.py index 4be5c40ade..a4a0051c2e 100644 --- a/panel/layout/spacer.py +++ b/panel/layout/spacer.py @@ -101,7 +101,7 @@ class Divider(Reactive): >>> ) """ - width_policy = param.ObjectSelector(default="fit", readonly=True) + width_policy = param.Selector(default="fit", readonly=True) _bokeh_model = BkDiv diff --git a/panel/layout/tabs.py b/panel/layout/tabs.py index 0dc43f37e3..e4a795d0f6 100644 --- a/panel/layout/tabs.py +++ b/panel/layout/tabs.py @@ -43,7 +43,7 @@ class Tabs(NamedListPanel): dynamic = param.Boolean(default=False, doc=""" Dynamically populate only the active tab.""") - tabs_location = param.ObjectSelector( + tabs_location = param.Selector( default='above', objects=['above', 'below', 'left', 'right'], doc=""" The location of the tabs relative to the tab contents.""") diff --git a/panel/pane/alert.py b/panel/pane/alert.py index fb12e04990..b2d390bf0d 100644 --- a/panel/pane/alert.py +++ b/panel/pane/alert.py @@ -30,7 +30,7 @@ class Alert(Markdown): >>> Alert('Some important message', alert_type='warning') """ - alert_type = param.ObjectSelector(default="primary", objects=ALERT_TYPES) + alert_type = param.Selector(default="primary", objects=ALERT_TYPES) priority: ClassVar[float | bool | None] = 0 diff --git a/panel/pane/echarts.py b/panel/pane/echarts.py index 7a2d4384d5..22fd8d9ba1 100644 --- a/panel/pane/echarts.py +++ b/panel/pane/echarts.py @@ -44,10 +44,10 @@ class ECharts(ModelPane): the `objects` with a value containing a smaller number of series. """) - renderer = param.ObjectSelector(default="canvas", objects=["canvas", "svg"], doc=""" + renderer = param.Selector(default="canvas", objects=["canvas", "svg"], doc=""" Whether to render as HTML canvas or SVG""") - theme = param.ObjectSelector(default="default", objects=["default", "light", "dark"], doc=""" + theme = param.Selector(default="default", objects=["default", "light", "dark"], doc=""" Theme to apply to plots.""") priority: ClassVar[float | bool | None] = None diff --git a/panel/pane/equation.py b/panel/pane/equation.py index 163d4ead12..4af31999ce 100644 --- a/panel/pane/equation.py +++ b/panel/pane/equation.py @@ -42,7 +42,7 @@ class LaTeX(ModelPane): ... ) """ - renderer = param.ObjectSelector(default=None, allow_None=True, + renderer = param.Selector(default=None, allow_None=True, objects=['katex', 'mathjax'], doc=""" The JS renderer used to render the LaTeX expression. Defaults to katex.""") diff --git a/panel/pane/holoviews.py b/panel/pane/holoviews.py index f157072775..827d45eff8 100644 --- a/panel/pane/holoviews.py +++ b/panel/pane/holoviews.py @@ -58,7 +58,7 @@ class HoloViews(Pane): >>> HoloViews(some_holoviews_object) """ - backend = param.ObjectSelector( + backend = param.Selector( default=None, objects=['bokeh', 'matplotlib', 'plotly'], doc=""" The HoloViews backend used to render the plot (if None defaults to the currently selected renderer).""") @@ -81,18 +81,18 @@ class HoloViews(Pane): allow_None=True, doc=""" Bokeh theme to apply to the HoloViews plot.""") - widget_location = param.ObjectSelector(default='right_top', objects=[ + widget_location = param.Selector(default='right_top', objects=[ 'left', 'bottom', 'right', 'top', 'top_left', 'top_right', 'bottom_left', 'bottom_right', 'left_top', 'left_bottom', 'right_top', 'right_bottom'], doc=""" The layout of the plot and the widgets. The value refers to the position of the widgets relative to the plot.""") - widget_layout = param.ObjectSelector( + widget_layout = param.Selector( objects=[WidgetBox, Row, Column], constant=True, default=WidgetBox, doc=""" The layout object to display the widgets in.""") - widget_type = param.ObjectSelector(default='individual', + widget_type = param.Selector(default='individual', objects=['individual', 'scrubber'], doc=""") Whether to generate individual widgets for each dimension or on global scrubber.""") diff --git a/panel/pane/ipywidget.py b/panel/pane/ipywidget.py index a8a9c522f5..a4ca2b08ae 100644 --- a/panel/pane/ipywidget.py +++ b/panel/pane/ipywidget.py @@ -84,7 +84,7 @@ def _get_model( class IPyLeaflet(IPyWidget): - sizing_mode = param.ObjectSelector(default='stretch_width', objects=[ + sizing_mode = param.Selector(default='stretch_width', objects=[ 'fixed', 'stretch_width', 'stretch_height', 'stretch_both', 'scale_width', 'scale_height', 'scale_both', None]) diff --git a/panel/pane/markup.py b/panel/pane/markup.py index 583903a932..30a1b9c7be 100644 --- a/panel/pane/markup.py +++ b/panel/pane/markup.py @@ -168,7 +168,7 @@ class DataFrame(HTML): index_names = param.Boolean(default=True, doc=""" Prints the names of the indexes.""") - justify = param.ObjectSelector(default=None, allow_None=True, objects=[ + justify = param.Selector(default=None, allow_None=True, objects=[ 'left', 'right', 'center', 'justify', 'justify-all', 'start', 'end', 'inherit', 'match-parent', 'initial', 'unset'], doc=""" How to justify the column labels.""") @@ -505,7 +505,7 @@ class JSON(HTMLBasePane): hover_preview = param.Boolean(default=False, doc=""" Whether to display a hover preview for collapsed nodes.""") - theme = param.ObjectSelector(default="dark", objects=["light", "dark"], doc=""" + theme = param.Selector(default="dark", objects=["light", "dark"], doc=""" Whether the JSON tree view is expanded by default.""") priority: ClassVar[float | bool | None] = None diff --git a/panel/pane/perspective.py b/panel/pane/perspective.py index aceb0cc67d..476a9e3904 100644 --- a/panel/pane/perspective.py +++ b/panel/pane/perspective.py @@ -305,7 +305,7 @@ class Perspective(ModelPane, ReactiveData): sort = param.List(default=None, doc=""" How to sort. For example[["x","desc"]]""") - plugin = param.ObjectSelector(default=Plugin.GRID.value, objects=Plugin.options(), doc=""" + plugin = param.Selector(default=Plugin.GRID.value, objects=Plugin.options(), doc=""" The name of a plugin to display the data. For example hypergrid or d3_xy_scatter.""") plugin_config = param.Dict(default={}, nested_refs=True, doc=""" @@ -314,7 +314,7 @@ class Perspective(ModelPane, ReactiveData): settings = param.Boolean(default=True, doc=""" Whether to show the settings menu.""") - theme = param.ObjectSelector(default='pro', objects=THEMES, doc=""" + theme = param.Selector(default='pro', objects=THEMES, doc=""" The style of the PerspectiveViewer. For example pro-dark""") title = param.String(default=None, doc=""" diff --git a/panel/pane/plot.py b/panel/pane/plot.py index 0ede8f7dcb..8b83769eb7 100644 --- a/panel/pane/plot.py +++ b/panel/pane/plot.py @@ -443,7 +443,7 @@ class Folium(HTML): The Folium pane wraps Folium map components. """ - sizing_mode = param.ObjectSelector(default='stretch_width', objects=[ + sizing_mode = param.Selector(default='stretch_width', objects=[ 'fixed', 'stretch_width', 'stretch_height', 'stretch_both', 'scale_width', 'scale_height', 'scale_both', None]) diff --git a/panel/pane/vega.py b/panel/pane/vega.py index 59ef47d57c..3ad207eeba 100644 --- a/panel/pane/vega.py +++ b/panel/pane/vega.py @@ -160,7 +160,7 @@ class Vega(ModelPane): show_actions = param.Boolean(default=False, doc=""" Whether to show Vega actions.""") - theme = param.ObjectSelector(default=None, allow_None=True, objects=[ + theme = param.Selector(default=None, allow_None=True, objects=[ 'excel', 'ggplot2', 'quartz', 'vox', 'fivethirtyeight', 'dark', 'latimes', 'urbaninstitute', 'googlecharts']) diff --git a/panel/template/base.py b/panel/template/base.py index 10c9f54d67..a47161dc5e 100644 --- a/panel/template/base.py +++ b/panel/template/base.py @@ -671,7 +671,7 @@ class BasicTemplate(BaseTemplate): Specifies the base URL for all relative URLs in a page. Default is '', i.e. not the domain.""") - base_target = param.ObjectSelector(default="_self", + base_target = param.Selector(default="_self", objects=["_blank", "_self", "_parent", "_top"], doc=""" Specifies the base Target for all relative URLs in a page.""") diff --git a/panel/template/react/__init__.py b/panel/template/react/__init__.py index 5971ac9250..b0378150d9 100644 --- a/panel/template/react/__init__.py +++ b/panel/template/react/__init__.py @@ -19,7 +19,7 @@ class ReactTemplate(BasicTemplate): ReactTemplate is built on top of React Grid Layout web components. """ - compact = param.ObjectSelector(default=None, objects=[None, 'vertical', 'horizontal', 'both']) + compact = param.Selector(default=None, objects=[None, 'vertical', 'horizontal', 'both']) cols = param.Dict(default={'lg': 12, 'md': 10, 'sm': 6, 'xs': 4, 'xxs': 2}) diff --git a/panel/tests/layout/test_base.py b/panel/tests/layout/test_base.py index 422cbce9fc..20097b282b 100644 --- a/panel/tests/layout/test_base.py +++ b/panel/tests/layout/test_base.py @@ -494,7 +494,7 @@ def test_widgetbox(document, comm): def test_layout_with_param_setitem(document, comm): import param class TestClass(param.Parameterized): - select = param.ObjectSelector(default=0, objects=[0,1]) + select = param.Selector(default=0, objects=[0,1]) def __init__(self, **params): super().__init__(**params) diff --git a/panel/tests/test_param.py b/panel/tests/test_param.py index 02abc8503c..18dfb99b91 100644 --- a/panel/tests/test_param.py +++ b/panel/tests/test_param.py @@ -314,7 +314,7 @@ class Test(param.Parameterized): def test_object_selector_param(document, comm): class Test(param.Parameterized): - a = param.ObjectSelector(default='b', objects=[1, 'b', 'c']) + a = param.Selector(default='b', objects=[1, 'b', 'c']) test = Test() test_pane = Param(test) @@ -420,7 +420,7 @@ class Test(param.Parameterized): def test_object_selector_param_overrides(document, comm): class Test(param.Parameterized): - a = param.ObjectSelector(default='b', objects=[1, 'b', 'c']) + a = param.Selector(default='b', objects=[1, 'b', 'c']) test = Test() test_pane = Param(test, widgets={'a': {'options': ['b', 'c'], 'value': 'c'}}) @@ -953,7 +953,7 @@ class Test(param.Parameterized): def test_switch_param_subobject(document, comm): class Test(param.Parameterized): - a = param.ObjectSelector() + a = param.Selector() o1 = Test(name='Subobject 1') o2 = Test(name='Subobject 2') diff --git a/panel/viewable.py b/panel/viewable.py index 7fc48d8e7e..65fd6651d6 100644 --- a/panel/viewable.py +++ b/panel/viewable.py @@ -86,7 +86,7 @@ class Layoutable(param.Parameterized): css_classes = param.List(default=[], nested_refs=True, doc=""" CSS classes to apply to the layout.""") - design = param.ObjectSelector(default=None, objects=[], doc=""" + design = param.Selector(default=None, objects=[], doc=""" The design system to use to style components.""") height = param.Integer(default=None, bounds=(0, None), doc=""" @@ -127,7 +127,7 @@ class Layoutable(param.Parameterized): The width of the component (in pixels). This can be either fixed or preferred width, depending on width sizing policy.""") - width_policy = param.ObjectSelector( + width_policy = param.Selector( default="auto", objects=['auto', 'fixed', 'fit', 'min', 'max'], doc=""" Describes how the component should maintain its width. @@ -159,7 +159,7 @@ class Layoutable(param.Parameterized): management and other factors. """) - height_policy = param.ObjectSelector( + height_policy = param.Selector( default="auto", objects=['auto', 'fixed', 'fit', 'min', 'max'], doc=""" Describes how the component should maintain its height. @@ -191,7 +191,7 @@ class Layoutable(param.Parameterized): management and other factors. """) - sizing_mode = param.ObjectSelector(default=None, objects=[ + sizing_mode = param.Selector(default=None, objects=[ 'fixed', 'stretch_width', 'stretch_height', 'stretch_both', 'scale_width', 'scale_height', 'scale_both', None], doc=""" How the component should size itself. diff --git a/panel/widgets/button.py b/panel/widgets/button.py index 83b08835d7..344b3841f7 100644 --- a/panel/widgets/button.py +++ b/panel/widgets/button.py @@ -33,12 +33,12 @@ class _ButtonBase(Widget): - button_type = param.ObjectSelector(default='default', objects=BUTTON_TYPES, doc=""" + button_type = param.Selector(default='default', objects=BUTTON_TYPES, doc=""" A button theme; should be one of 'default' (white), 'primary' (blue), 'success' (green), 'info' (yellow), 'light' (light), or 'danger' (red).""") - button_style = param.ObjectSelector(default='solid', objects=BUTTON_STYLES, doc=""" + button_style = param.Selector(default='solid', objects=BUTTON_STYLES, doc=""" A button style to switch between 'solid', 'outline'.""") _rename: ClassVar[Mapping[str, str | None]] = {'name': 'label', 'button_style': None} diff --git a/panel/widgets/codeeditor.py b/panel/widgets/codeeditor.py index 8e766afd85..7549ad2ce7 100644 --- a/panel/widgets/codeeditor.py +++ b/panel/widgets/codeeditor.py @@ -49,7 +49,7 @@ class CodeEditor(Widget): readonly = param.Boolean(default=False, doc=""" Define if editor content can be modified. Alias for disabled.""") - theme = param.ObjectSelector(default="chrome", objects=list(ace_themes), + theme = param.Selector(default="chrome", objects=list(ace_themes), doc="Theme of the editor") value = param.String(default="", doc=""" diff --git a/panel/widgets/indicators.py b/panel/widgets/indicators.py index 56f1c87061..65c434565a 100644 --- a/panel/widgets/indicators.py +++ b/panel/widgets/indicators.py @@ -66,7 +66,7 @@ class Indicator(Widget): Indicator is a baseclass for widgets which indicate some state. """ - sizing_mode = param.ObjectSelector(default='fixed', objects=[ + sizing_mode = param.Selector(default='fixed', objects=[ 'fixed', 'stretch_width', 'stretch_height', 'stretch_both', 'scale_width', 'scale_height', 'scale_both', None]) @@ -158,7 +158,7 @@ class BooleanStatus(BooleanIndicator): >>> BooleanStatus(value=True, color='primary', width=100, height=100) """ - color = param.ObjectSelector(default='dark', objects=[ + color = param.Selector(default='dark', objects=[ 'primary', 'secondary', 'success', 'info', 'danger', 'warning', 'light', 'dark'], doc=""" The color of the circle, one of 'primary', 'secondary', 'success', 'info', 'danger', 'warning', 'light', 'dark'""") @@ -203,9 +203,9 @@ class LoadingSpinner(BooleanIndicator): >>> LoadingSpinner(value=True, color='primary', bgcolor='light', width=100, height=100) """ - bgcolor = param.ObjectSelector(default='light', objects=['dark', 'light']) + bgcolor = param.Selector(default='light', objects=['dark', 'light']) - color = param.ObjectSelector(default='dark', objects=[ + color = param.Selector(default='dark', objects=[ 'primary', 'secondary', 'success', 'info', 'danger', 'warning', 'light', 'dark']) @@ -284,13 +284,13 @@ class Progress(ValueIndicator): If no value is set the active property toggles animation of the progress bar on and off.""") - bar_color = param.ObjectSelector(default='success', objects=[ + bar_color = param.Selector(default='success', objects=[ 'primary', 'secondary', 'success', 'info', 'danger', 'warning', 'light', 'dark']) max = param.Integer(default=100, doc="The maximum value of the progress bar.") - sizing_mode = param.ObjectSelector(default=None, objects=[ + sizing_mode = param.Selector(default=None, objects=[ 'fixed', 'stretch_width', 'stretch_height', 'stretch_both', 'scale_width', 'scale_height', 'scale_both', None]) @@ -1120,7 +1120,7 @@ class Trend(SyncableData, Indicator): data = param.Parameter(doc=""" The plot data declared as a dictionary of arrays or a DataFrame.""") - layout = param.ObjectSelector(default="column", objects=["column", "row"]) + layout = param.Selector(default="column", objects=["column", "row"]) plot_x = param.String(default="x", doc=""" The name of the key in the plot_data to use on the x-axis.""") @@ -1131,7 +1131,7 @@ class Trend(SyncableData, Indicator): plot_color = param.String(default=BLUE, doc=""" The color to use in the plot.""") - plot_type = param.ObjectSelector(default="bar", objects=["line", "step", "area", "bar"], doc=""" + plot_type = param.Selector(default="bar", objects=["line", "step", "area", "bar"], doc=""" The plot type to render the plot data as.""") pos_color = param.String(GREEN, doc=""" @@ -1140,7 +1140,7 @@ class Trend(SyncableData, Indicator): neg_color = param.String(RED, doc=""" The color used to indicate a negative change.""") - sizing_mode = param.ObjectSelector(default=None, objects=[ + sizing_mode = param.Selector(default=None, objects=[ 'fixed', 'stretch_width', 'stretch_height', 'stretch_both', 'scale_width', 'scale_height', 'scale_both', None]) diff --git a/panel/widgets/input.py b/panel/widgets/input.py index bcf884579a..b5337e977f 100644 --- a/panel/widgets/input.py +++ b/panel/widgets/input.py @@ -171,7 +171,7 @@ class TextAreaInput(_TextInputBase): rows = param.Integer(default=2, doc=""" Number of rows in the text input field.""") - resizable = param.ObjectSelector( + resizable = param.Selector( objects=["both", "width", "height", False], doc=""" Whether the layout is interactively resizable, and if so in which dimensions: `width`, `height`, or `both`. @@ -1141,7 +1141,7 @@ class LiteralInput(Widget): placeholder = param.String(default='', doc=""" Placeholder for empty input field.""") - serializer = param.ObjectSelector(default='ast', objects=['ast', 'json'], doc=""" + serializer = param.Selector(default='ast', objects=['ast', 'json'], doc=""" The serialization (and deserialization) method to use. 'ast' uses ast.literal_eval and 'json' uses json.loads and json.dumps. """) diff --git a/panel/widgets/misc.py b/panel/widgets/misc.py index 027ac2340e..aea99669a9 100644 --- a/panel/widgets/misc.py +++ b/panel/widgets/misc.py @@ -39,7 +39,7 @@ class VideoStream(Widget): >>> VideoStream(name='Video Stream', timeout=100) """ - format = param.ObjectSelector(default='png', objects=['png', 'jpeg'], + format = param.Selector(default='png', objects=['png', 'jpeg'], doc=""" The file format as which the video is returned.""") @@ -86,12 +86,12 @@ class FileDownload(IconMixin): Whether to download on the initial click or allow for right-click save as.""") - button_type = param.ObjectSelector(default='default', objects=BUTTON_TYPES, doc=""" + button_type = param.Selector(default='default', objects=BUTTON_TYPES, doc=""" A button theme; should be one of 'default' (white), 'primary' (blue), 'success' (green), 'info' (yellow), 'light' (light), or 'danger' (red).""") - button_style = param.ObjectSelector(default='solid', objects=BUTTON_STYLES, doc=""" + button_style = param.Selector(default='solid', objects=BUTTON_STYLES, doc=""" A button style to switch between 'solid', 'outline'.""") callback = param.Callable(default=None, allow_refs=False, doc=""" diff --git a/panel/widgets/player.py b/panel/widgets/player.py index 93d94b656a..7f320125c6 100644 --- a/panel/widgets/player.py +++ b/panel/widgets/player.py @@ -30,7 +30,7 @@ class PlayerBase(Widget): Interval between updates, in milliseconds. Default is 500, i.e. two updates per second.""") - loop_policy = param.ObjectSelector( + loop_policy = param.Selector( default='once', objects=['once', 'loop', 'reflect'], doc=""" Policy used when player hits last frame""") @@ -49,7 +49,7 @@ class PlayerBase(Widget): height = param.Integer(default=80) - value_align = param.ObjectSelector( + value_align = param.Selector( objects=["start", "center", "end"], doc=""" Location to display the value of the slider ("start", "center", "end")""") diff --git a/panel/widgets/slider.py b/panel/widgets/slider.py index 2fa58541c4..305ad5f0fa 100644 --- a/panel/widgets/slider.py +++ b/panel/widgets/slider.py @@ -49,7 +49,7 @@ class _SliderBase(Widget): bar_color = param.Color(default="#e6e6e6", doc="""""") - direction = param.ObjectSelector(default='ltr', objects=['ltr', 'rtl'], doc=""" + direction = param.Selector(default='ltr', objects=['ltr', 'rtl'], doc=""" Whether the slider should go from left-to-right ('ltr') or right-to-left ('rtl').""") @@ -57,7 +57,7 @@ class _SliderBase(Widget): The name of the widget. Also used as the label of the widget. If not set, the widget has no label.""") - orientation = param.ObjectSelector(default='horizontal', objects=['horizontal', 'vertical'], + orientation = param.Selector(default='horizontal', objects=['horizontal', 'vertical'], doc=""" Whether the slider should be oriented horizontally or vertically.""") diff --git a/panel/widgets/speech_to_text.py b/panel/widgets/speech_to_text.py index 93274af00f..0bede0a8d3 100644 --- a/panel/widgets/speech_to_text.py +++ b/panel/widgets/speech_to_text.py @@ -318,7 +318,7 @@ class SpeechToText(Widget): incoming audio, and attempts to return a RecognitionResult using the audio captured so far.""") - lang = param.ObjectSelector(default="", objects=[""] + LANGUAGE_CODES, + lang = param.Selector(default="", objects=[""] + LANGUAGE_CODES, allow_None=True, label="Language", doc=""" The language of the current SpeechRecognition in BCP 47 format. For example 'en-US'. If not specified, this defaults @@ -353,7 +353,7 @@ class SpeechToText(Widget): button_hide = param.Boolean(default=False, label="Hide the Button", doc=""" If True no button is shown. If False a toggle Start/ Stop button is shown.""") - button_type = param.ObjectSelector(default="light", objects=BUTTON_TYPES, doc=""" + button_type = param.Selector(default="light", objects=BUTTON_TYPES, doc=""" The button styling.""") button_not_started = param.String(label="Button Text when not started", doc=""" diff --git a/panel/widgets/tables.py b/panel/widgets/tables.py index 6feb6d61e5..9663346b11 100644 --- a/panel/widgets/tables.py +++ b/panel/widgets/tables.py @@ -934,7 +934,7 @@ class DataFrame(BaseTable): auto_edit = param.Boolean(default=False, doc=""" Whether clicking on a table cell automatically starts edit mode.""") - autosize_mode = param.ObjectSelector(default='force_fit', objects=[ + autosize_mode = param.Selector(default='force_fit', objects=[ "none", "fit_columns", "fit_viewport", "force_fit"], doc=""" Determines the column autosizing mode, as one of the following options: @@ -1144,14 +1144,14 @@ class Tabulator(BaseTable): hidden_columns = param.List(default=[], nested_refs=True, doc=""" List of columns to hide.""") - layout = param.ObjectSelector(default='fit_data_table', objects=[ + layout = param.Selector(default='fit_data_table', objects=[ 'fit_data', 'fit_data_fill', 'fit_data_stretch', 'fit_data_table', 'fit_columns']) initial_page_size = param.Integer(default=20, bounds=(1, None), doc=""" Initial page size if page_size is None and therefore automatically set.""") - pagination = param.ObjectSelector(default=None, allow_None=True, + pagination = param.Selector(default=None, allow_None=True, objects=['local', 'remote']) page = param.Integer(default=1, doc=""" @@ -1199,7 +1199,7 @@ class Tabulator(BaseTable): Can either be specified as a simple boolean toggling the behavior on and off or as a dictionary specifying the option per column.""") - theme = param.ObjectSelector( + theme = param.Selector( default="simple", objects=[ 'default', 'site', 'simple', 'midnight', 'modern', 'bootstrap', 'bootstrap4', 'materialize', 'bulma', 'semantic-ui', 'fast', diff --git a/panel/widgets/text_to_speech.py b/panel/widgets/text_to_speech.py index 236b2bed11..de84d43df3 100644 --- a/panel/widgets/text_to_speech.py +++ b/panel/widgets/text_to_speech.py @@ -90,7 +90,7 @@ class Utterance(param.Parameterized): spoken. The text may be provided as plain text, or a well-formed SSML document.""") - lang = param.ObjectSelector(default="", doc=""" + lang = param.Selector(default="", doc=""" The language of the utterance.""") pitch = param.Number(default=1.0, bounds=(0.0, 2.0), doc=""" @@ -101,7 +101,7 @@ class Utterance(param.Parameterized): The speed at which the utterance will be spoken at expressed as a number between 0.1 and 10.""" ) - voice = param.ObjectSelector(doc=""" + voice = param.Selector(doc=""" The voice that will be used to speak the utterance.""") volume = param.Number(default=1.0, bounds=(0.0, 1.0), doc=""" The From 4da2b8ea93510026a61837ad5966c29285f7531a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Mon, 18 Nov 2024 09:58:58 +0100 Subject: [PATCH 33/44] compat: textual 0.86 (#7501) --- panel/pane/_textual.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/panel/pane/_textual.py b/panel/pane/_textual.py index 33e34c210f..0029509eb0 100644 --- a/panel/pane/_textual.py +++ b/panel/pane/_textual.py @@ -66,8 +66,10 @@ def _disable_mouse_support(self) -> None: self.flush() def _process_input(self, event): + # Textual 0.86 changed from `process_event` to `process_message` + fn = self.process_event if hasattr(self, 'process_event') else self.process_message for parsed_event in self._parser.feed(event.new): - self.process_event(parsed_event) + fn(parsed_event) def disable_input(self): if self._input_watcher is None: From a248460d4be9e3d0ffc40441a96d55f60f75b601 Mon Sep 17 00:00:00 2001 From: Leopold Talirz Date: Mon, 18 Nov 2024 09:59:10 +0100 Subject: [PATCH 34/44] docs: `auto` field of FileDownload widget (#7498) Was missing a word + clarify advantage of user-defined filename. --- examples/reference/widgets/FileDownload.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/reference/widgets/FileDownload.ipynb b/examples/reference/widgets/FileDownload.ipynb index 6f4fd1dc58..f59dcebe48 100644 --- a/examples/reference/widgets/FileDownload.ipynb +++ b/examples/reference/widgets/FileDownload.ipynb @@ -26,7 +26,7 @@ "\n", "##### Core\n", "\n", - "* **`auto`** (boolean): Whether to download the file the initial click (if `True`) or when clicking a second time (or via the right-click Save file menu).\n", + "* **`auto`** (boolean): Whether to download the file with the first click (if `True`) or only after clicking a second time (if `False`, enables right-click -> Save as).\n", "* **`callback`** (callable): A callable that returns a file or file-like object (takes precedence over `file` if set). \n", "* **`embed`** (boolean): Whether to embed the data on initialization.\n", "* **`file`** (str, Path or file-like object): A path to a file or a file-like object.\n", From 8421976315af703933e634b7180476a459a16ca1 Mon Sep 17 00:00:00 2001 From: Marc Skov Madsen Date: Mon, 18 Nov 2024 11:05:02 +0100 Subject: [PATCH 35/44] improve hold docs (#7500) Co-authored-by: Philipp Rudiger --- doc/how_to/performance/hold.md | 49 ++++++++++++++++++++++------------ 1 file changed, 32 insertions(+), 17 deletions(-) diff --git a/doc/how_to/performance/hold.md b/doc/how_to/performance/hold.md index 3c49250e15..81d55ed400 100644 --- a/doc/how_to/performance/hold.md +++ b/doc/how_to/performance/hold.md @@ -1,18 +1,34 @@ -# Batching updates with `hold` +# Batching Updates with `hold` When working with interactive dashboards and applications in Panel, you might encounter situations where updating multiple components simultaneously causes unnecessary re-renders. This is because Panel generally dispatches any change to a parameter immediately. This can lead to performance issues and a less responsive user experience because each individual update may trigger re-renders on the frontend. The `hold` utility in Panel allows you to batch updates to the frontend, reducing the number of re-renders and improving performance. -In this guide, we'll explore how to use hold both as a context manager and as a decorator to optimize your Panel applications. +In this guide, we'll explore how to use `hold` both as a context manager and as a decorator to optimize your Panel applications. -## What is hold? +## What is `hold`? -The `hold` function is a context manager and decorator that temporarily holds events on a Bokeh Document. When you update multiple components within a hold block, the events are collected and dispatched all at once when the block exits. This means that the frontend will only re-render once, regardless of how many updates were made, leading to a smoother and more efficient user experience. +The `hold` function is a context manager and decorator that temporarily holds events on a Bokeh Document. When you update multiple components within a `hold` block, the events are collected and dispatched all at once when the block exits. This means that the frontend will only re-render once, regardless of how many updates were made, leading to a smoother and more efficient user experience. + +Let’s try first **without `hold`** to understand the difference `hold` can make: + +```{pyodide} +import panel as pn +from panel.io import hold + +def increment(e): + for obj in column_0: + obj.object = str(e.new) + +column_0 = pn.FlexBox(*['0']*100) +button = pn.widgets.Button(name='Increment', on_click=increment) + +pn.Column(column_0, button).servable() +``` ## Using `hold` -### As a decorator +### As a Decorator -If you have a function that updates components and you want to ensure that all updates are held, you can use hold as a decorator. E.g. here we update 100 components at once. If you do not hold then each of these events is sent and applied in series, potentially resulting in visible updates. +If you have a function that updates components and you want to ensure that all updates are held, you can use `hold` as a decorator. For example, here we update 100 components at once. If you do not use `hold`, each of these events is sent and applied in series, potentially resulting in visible updates. ```{pyodide} import panel as pn @@ -20,20 +36,20 @@ from panel.io import hold @hold() def increment(e): - for obj in column: + for obj in column_1: obj.object = str(e.new) -column = pn.FlexBox(*['0']*100) +column_1 = pn.FlexBox(*['0']*100) button = pn.widgets.Button(name='Increment', on_click=increment) -pn.Column(column, button).servable() +pn.Column(column_1, button).servable() ``` -Applying the hold decorator means all the updates are sent in a single Websocket message and applied on the frontend simultaneously. +Applying the `hold` decorator means all the updates are sent in a single WebSocket message and applied on the frontend simultaneously. -### As a context manager +### As a Context Manager -Alternatively, the `hold` function can be used as a context manager, potentially giving you finer grained control over which events are batched and which are not: +Alternatively, the `hold` function can be used as a context manager, potentially giving you finer-grained control over which events are batched and which are not: ```{pyodide} import time @@ -43,15 +59,14 @@ from panel.io import hold def increment(e): with button.param.update(name='Incrementing...', disabled=True): - time.sleep(0.5) with hold(): - for obj in column: + for obj in column_2: obj.object = str(e.new) -column = pn.FlexBox(*['0']*100) +column_2 = pn.FlexBox(*['0']*100) button = pn.widgets.Button(name='Increment', on_click=increment) -pn.Column(column, button).servable() +pn.Column(column_2, button).servable() ``` -Here the updates to the `Button` are dispatched immediately while the updates to the counters are batched. +Here the updates to the `Button` are dispatched immediately, while the updates to the counters are batched. From b597638fb706a16054310e0ba767276cf0a7fa56 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Mon, 18 Nov 2024 15:46:16 +0100 Subject: [PATCH 36/44] Bump Vizzu version to 0.15 (#7485) --- examples/reference/panes/Vizzu.ipynb | 2 +- panel/models/vizzu.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/reference/panes/Vizzu.ipynb b/examples/reference/panes/Vizzu.ipynb index 204963d863..3d9678ca71 100644 --- a/examples/reference/panes/Vizzu.ipynb +++ b/examples/reference/panes/Vizzu.ipynb @@ -38,7 +38,7 @@ "* **`patch`**: Patches one or more rows in the data.\n", "___\n", "\n", - "The `Vizzu` pane is built on **version {{VIZZU_VERSION}}** of the [Vizzu Javascript](https://lib.vizzuhq.com/latest/) library." + "The `Vizzu` pane is built on **version {{VIZZU_VERSION}}** of the [Vizzu Javascript](https://lib.vizzuhq.com/{{VIZZU_VERSION}}/) library." ] }, { diff --git a/panel/models/vizzu.py b/panel/models/vizzu.py index 4a682fba9c..6a8b5d3d48 100644 --- a/panel/models/vizzu.py +++ b/panel/models/vizzu.py @@ -11,7 +11,7 @@ from ..config import config from ..util import classproperty -VIZZU_VERSION = "0.9.3" +VIZZU_VERSION = "0.15" class VizzuEvent(ModelEvent): From 951f620ef181b0ee4b0ab19753a21bd74dbf39ba Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Mon, 18 Nov 2024 19:46:32 +0100 Subject: [PATCH 37/44] Explicitly check hook signature (#7502) --- panel/viewable.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/panel/viewable.py b/panel/viewable.py index 65fd6651d6..d1786f9193 100644 --- a/panel/viewable.py +++ b/panel/viewable.py @@ -12,6 +12,7 @@ import asyncio import functools +import inspect import logging import os import sys @@ -600,9 +601,9 @@ def _preprocess(self, root: 'Model', changed=None, old_models=None) -> None: changed = self if changed is None else changed hooks = self._preprocessing_hooks+self._hooks for hook in hooks: - try: + if len(inspect.signature(hook).parameters) >= 4: hook(self, root, changed, old_models) - except TypeError: + else: hook(self, root) def _render_model(self, doc: Optional[Document] = None, comm: Optional[Comm] = None) -> 'Model': From d7049ce66b009bdf16cc83c31ea740a02e0ab12e Mon Sep 17 00:00:00 2001 From: Philip Meier Date: Mon, 18 Nov 2024 21:02:58 +0100 Subject: [PATCH 38/44] pin bokeh_fastapi to existing version (#7495) --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 8dc56b9401..e2d58865aa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -77,7 +77,7 @@ recommended = [ 'plotly', ] fastapi = [ - 'bokeh-fastapi >= 0.1.2', + 'bokeh-fastapi >= 0.1.1', 'fastapi[standard]', ] dev = [ From 1f821d651d8ad2b874fb90cbc2f29277f6817464 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 Nov 2024 22:47:25 +0100 Subject: [PATCH 39/44] Bump cross-spawn from 7.0.3 to 7.0.6 in /panel (#7503) Bumps [cross-spawn](https://github.com/moxystudio/node-cross-spawn) from 7.0.3 to 7.0.6. - [Changelog](https://github.com/moxystudio/node-cross-spawn/blob/master/CHANGELOG.md) - [Commits](https://github.com/moxystudio/node-cross-spawn/compare/v7.0.3...v7.0.6) --- updated-dependencies: - dependency-name: cross-spawn dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- panel/package-lock.json | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/panel/package-lock.json b/panel/package-lock.json index 0de8d43ecc..e15f4d35f4 100644 --- a/panel/package-lock.json +++ b/panel/package-lock.json @@ -1112,10 +1112,9 @@ "license": "MIT" }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "license": "MIT", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", From c6ac0a8ad3eb8a60daa1b279621d062748e9671c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Tue, 26 Nov 2024 14:35:14 +0100 Subject: [PATCH 40/44] compat: altair 5.5.0 (#7523) --- doc/how_to/styling/altair.md | 6 +++--- doc/tutorials/basic/templates.md | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/doc/how_to/styling/altair.md b/doc/how_to/styling/altair.md index 54cfc56836..337559e724 100644 --- a/doc/how_to/styling/altair.md +++ b/doc/how_to/styling/altair.md @@ -21,7 +21,7 @@ from vega_datasets import data pn.extension("vega") def plot(theme, color): - alt.themes.enable(theme) + alt.theme.enable(theme) return ( alt.Chart(data.cars()) @@ -41,7 +41,7 @@ def plot(theme, color): .interactive() ) -themes = sorted(alt.themes.names()) +themes = sorted(alt.theme.names()) theme = pn.widgets.Select(value="dark", options=themes, name="Theme") color = pn.widgets.ColorPicker(value="#F08080", name="Color") @@ -53,5 +53,5 @@ pn.Column( ).servable() ``` -Please note that the line `alt.themes.enable(theme)` will set the theme of all future generated plots +Please note that the line `alt.theme.enable(theme)` will set the theme of all future generated plots unless you specifically change it before usage in a `Vega` pane. diff --git a/doc/tutorials/basic/templates.md b/doc/tutorials/basic/templates.md index a4e0b624bf..116abfe9d9 100644 --- a/doc/tutorials/basic/templates.md +++ b/doc/tutorials/basic/templates.md @@ -91,9 +91,9 @@ ACCENT = "teal" image = pn.pane.JPG("https://assets.holoviz.org/panel/tutorials/wind_turbines_sunset.png") if pn.config.theme=="dark": - alt.themes.enable("dark") + alt.theme.enable("dark") else: - alt.themes.enable("default") + alt.theme.enable("default") @pn.cache # Add caching to only download data once def get_data(): @@ -153,7 +153,7 @@ Upon toggling, the app should switch to dark mode: In the code: - `pn.config.theme` determines the selected theme ("default" or "dark"). -- `alt.themes.enable("dark")` applies the "dark" theme to the plot. Panel doesn't do this automatically. +- `alt.theme.enable("dark")` applies the "dark" theme to the plot. Panel doesn't do this automatically. - `accent` sets the primary or accent color for the template, allowing quick branding of the app. - `main_layout` specifies a layout to wrap each object in the main list. Choose from `"card"` (default) or `None`. From 58daea3686ba7658d51cba6e4bd1c1016b5b1a70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Wed, 27 Nov 2024 15:43:25 +0100 Subject: [PATCH 41/44] compat: Hatchling 1.26 (#7526) --- .github/workflows/build.yaml | 8 ++++++++ .github/workflows/gallery.yaml | 2 +- pyproject.toml | 3 ++- scripts/verify_build_size.py | 32 ++++++++++++++++++++++++++++++++ 4 files changed, 43 insertions(+), 2 deletions(-) create mode 100644 scripts/verify_build_size.py diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 09b47c4e93..91753fa7d6 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -55,6 +55,8 @@ jobs: name: conda path: dist/*tar.bz2 if-no-files-found: error + - name: Verify size + run: python scripts/verify_build_size.py conda conda_publish: name: Publish Conda @@ -106,6 +108,10 @@ jobs: name: pip path: dist/ if-no-files-found: error + - name: Verify sizes + run: | + python scripts/verify_build_size.py sdist + python scripts/verify_build_size.py whl pip_install: name: Install PyPI @@ -165,6 +171,8 @@ jobs: name: npm if-no-files-found: error path: ./${{ env.PACKAGE }}/${{ env.TARBALL }} + - name: Verify size + run: python scripts/verify_build_size.py npm npm_publish: name: Publish NPM diff --git a/.github/workflows/gallery.yaml b/.github/workflows/gallery.yaml index cc74ad3acd..761cc05733 100644 --- a/.github/workflows/gallery.yaml +++ b/.github/workflows/gallery.yaml @@ -23,7 +23,7 @@ jobs: runs-on: ubuntu-latest defaults: run: - shell: bash -e {0} + shell: bash -el {0} steps: - name: Checkout uses: actions/checkout@v3 diff --git a/pyproject.toml b/pyproject.toml index e2d58865aa..b7b34d79c3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -111,6 +111,7 @@ raw-options = { version_scheme = "no-guess-dev" } [tool.hatch.build.targets.wheel] include = ["panel"] +exclude = ["panel/node_modules"] [tool.hatch.build.targets.wheel.force-include] "panel/dist" = "panel/dist" @@ -121,7 +122,7 @@ include = ["panel"] [tool.hatch.build.targets.sdist] include = ["panel", "scripts", "examples"] -exclude = ["scripts/jupyterlite"] +exclude = ["scripts/jupyterlite", "panel/node_modules"] [tool.hatch.build.targets.sdist.force-include] "panel/dist" = "panel/dist" diff --git a/scripts/verify_build_size.py b/scripts/verify_build_size.py new file mode 100644 index 0000000000..b71384d1c9 --- /dev/null +++ b/scripts/verify_build_size.py @@ -0,0 +1,32 @@ +import sys + +from pathlib import Path + +EXPECTED_SIZES_MB = { + "conda": 25, + "npm": 25, + "sdist": 30, + "whl": 30, +} + +GLOB_PATH = { + "conda": "dist/*.tar.bz2", + "npm": "panel/*.tgz", + "sdist": "dist/*.tar.gz", + "whl": "dist/*.whl", +} + +PATH = Path(__file__).parents[1] + + +def main(build): + files = list(PATH.rglob(GLOB_PATH[build])) + assert len(files) == 1, f"Expected one {build} file, got {len(files)}" + + size = files[0].stat().st_size / 1024**2 + assert size < EXPECTED_SIZES_MB[build], f"{build} file is too large: {size:.2f} MB" + print(f"{build} file size: {size:.2f} MB") + + +if __name__ == "__main__": + main(sys.argv[1]) From e99e12cefd6cd0a74961890958ecc19106667566 Mon Sep 17 00:00:00 2001 From: Maxime Liquet <35924738+maximlt@users.noreply.github.com> Date: Mon, 2 Dec 2024 10:12:39 +0100 Subject: [PATCH 42/44] remove _param_watchers compatibility code (#7527) --- panel/config.py | 3 +-- panel/io/document.py | 5 ++--- panel/io/embed.py | 5 ++--- panel/layout/base.py | 4 ++-- panel/pane/base.py | 4 ++-- panel/tests/pane/test_base.py | 3 +-- panel/tests/widgets/test_base.py | 3 +-- panel/util/__init__.py | 2 +- panel/util/parameters.py | 17 ----------------- 9 files changed, 12 insertions(+), 34 deletions(-) diff --git a/panel/config.py b/panel/config.py index 6c786e76a5..5e80fd251d 100644 --- a/panel/config.py +++ b/panel/config.py @@ -24,7 +24,6 @@ from .__version import __version__ from .io.logging import panel_log_handler from .io.state import state -from .util import param_watchers _LOCAL_DEV_VERSION = ( any(v in __version__ for v in ('post', 'dirty')) @@ -407,7 +406,7 @@ def __setattr__(self, attr, value): if state.curdoc not in self._session_config: self._session_config[state.curdoc] = {} self._session_config[state.curdoc][attr] = value - watchers = param_watchers(self).get(attr, {}).get('value', []) + watchers = self.param.watchers.get(attr, {}).get('value', []) for w in watchers: w.fn() elif f'_{attr}' in _config._parameter_set and hasattr(self, f'_{attr}_'): diff --git a/panel/io/document.py b/panel/io/document.py index 33b890567f..290e713d99 100644 --- a/panel/io/document.py +++ b/panel/io/document.py @@ -29,7 +29,6 @@ from bokeh.models import CustomJS from ..config import config -from ..util import param_watchers from .loading import LOADING_INDICATOR_CSS_CLASS from .model import monkeypatch_events # noqa: F401 API import from .state import curdoc_locked, state @@ -120,10 +119,10 @@ def _cleanup_doc(doc, destroy=True): pane._hooks = [] for p in pane.select(): p._hooks = [] - param_watchers(p, {}) + p.param.watchers = {} p._documents = {} p._internal_callbacks = {} - param_watchers(pane, {}) + pane.param.watchers = {} pane._documents = {} pane._internal_callbacks = {} else: diff --git a/panel/io/embed.py b/panel/io/embed.py index 791b5cccbb..15fb7e397a 100644 --- a/panel/io/embed.py +++ b/panel/io/embed.py @@ -16,7 +16,6 @@ from bokeh.models import CustomJS from param.parameterized import Watcher -from ..util import param_watchers from .model import add_to_doc, diff from .state import state @@ -82,7 +81,7 @@ def save_dict(state, key=(), depth=0, max_depth=None, save_path='', load_path=No def get_watchers(reactive): - return [w for pwatchers in param_watchers(reactive).values() + return [w for pwatchers in reactive.param.watchers.values() for awatchers in pwatchers.values() for w in awatchers] @@ -158,7 +157,7 @@ def links_to_jslinks(model, widget): mappings = [] for pname, tgt_spec in link.links.items(): - if Watcher(*link[:-4]) in param_watchers(widget)[pname]['value']: + if Watcher(*link[:-4]) in widget.param.watchers[pname]['value']: mappings.append((pname, tgt_spec)) if mappings: diff --git a/panel/layout/base.py b/panel/layout/base.py index 63f6c96723..70160e3fba 100644 --- a/panel/layout/base.py +++ b/panel/layout/base.py @@ -19,7 +19,7 @@ from ..io.resources import CDN_DIST from ..models import Column as PnColumn from ..reactive import Reactive -from ..util import param_name, param_reprs, param_watchers +from ..util import param_name, param_reprs from ..viewable import Children if TYPE_CHECKING: @@ -570,7 +570,7 @@ def __init__(self, *items: list[Any | tuple[str, Any]], **params: Any): self.param.watch(self._update_names, 'objects') # ALERT: Ensure that name update happens first, should be # replaced by watch precedence support in param - param_watchers(self)['objects']['value'].reverse() + self.param.watchers['objects']['value'].reverse() def _to_object_and_name(self, item): from ..pane import panel diff --git a/panel/pane/base.py b/panel/pane/base.py index 7a7c149633..1f5c758b70 100644 --- a/panel/pane/base.py +++ b/panel/pane/base.py @@ -28,7 +28,7 @@ from ..links import Link from ..models import ReactiveHTML as _BkReactiveHTML from ..reactive import Reactive -from ..util import param_reprs, param_watchers +from ..util import param_reprs from ..util.checks import is_dataframe, is_series from ..util.parameters import get_params_to_inherit from ..viewable import ( @@ -702,7 +702,7 @@ def _update_from_object(cls, object: Any, old_object: Any, was_internal: bool, i custom_watchers = [] if isinstance(object, Reactive): watchers = [ - w for pwatchers in param_watchers(object).values() + w for pwatchers in object.param.watchers.values() for awatchers in pwatchers.values() for w in awatchers ] custom_watchers = [ diff --git a/panel/tests/pane/test_base.py b/panel/tests/pane/test_base.py index 1173831a13..e9a5c59a57 100644 --- a/panel/tests/pane/test_base.py +++ b/panel/tests/pane/test_base.py @@ -17,7 +17,6 @@ Param, ParamFunction, ParamMethod, ParamRef, ReactiveExpr, ) from panel.tests.util import check_layoutable_properties -from panel.util import param_watchers SKIP_PANES = ( Bokeh, ChatMessage, HoloViews, Interactive, IPyWidget, Param, @@ -90,7 +89,7 @@ def test_pane_untracked_watchers(pane, document, comm): except ImportError: pytest.skip("Dependent library could not be imported.") watchers = [ - w for pwatchers in param_watchers(p).values() + w for pwatchers in p.param.watchers.values() for awatchers in pwatchers.values() for w in awatchers ] assert len([wfn for wfn in watchers if wfn not in p._internal_callbacks and not hasattr(wfn.fn, '_watcher_name')]) == 0 diff --git a/panel/tests/widgets/test_base.py b/panel/tests/widgets/test_base.py index 8f0ab1bfb4..1dcb172cd6 100644 --- a/panel/tests/widgets/test_base.py +++ b/panel/tests/widgets/test_base.py @@ -5,7 +5,6 @@ from panel.layout import Row from panel.links import CallbackGenerator from panel.tests.util import check_layoutable_properties -from panel.util import param_watchers from panel.widgets import ( CompositeWidget, Dial, FileDownload, FloatSlider, LinearGauge, LoadingSpinner, Terminal, TextInput, ToggleGroup, Tqdm, Widget, @@ -38,7 +37,7 @@ def test_widget_untracked_watchers(widget, document, comm): except ImportError: pytest.skip("Dependent library could not be imported.") watchers = [ - w for pwatchers in param_watchers(widg).values() + w for pwatchers in widg.param.watchers.values() for awatchers in pwatchers.values() for w in awatchers ] assert len([wfn for wfn in watchers if wfn not in widg._internal_callbacks and not hasattr(wfn.fn, '_watcher_name')]) == 0 diff --git a/panel/util/__init__.py b/panel/util/__init__.py index a8820fcfe4..b7edd2cba9 100644 --- a/panel/util/__init__.py +++ b/panel/util/__init__.py @@ -37,7 +37,7 @@ is_series, isdatetime, isfile, isIn, isurl, ) from .parameters import ( # noqa - edit_readonly, extract_dependencies, get_method_owner, param_watchers, + edit_readonly, extract_dependencies, get_method_owner, recursive_parameterized, ) diff --git a/panel/util/parameters.py b/panel/util/parameters.py index ec7575926f..7474a91ae8 100644 --- a/panel/util/parameters.py +++ b/panel/util/parameters.py @@ -7,10 +7,6 @@ import param -from packaging.version import Version - -_unset = object() - def should_inherit(parameterized: param.Parameterized, p: str, v: Any) -> Any: pobj = parameterized.param[p] @@ -85,19 +81,6 @@ def extract_dependencies(function): return params -def param_watchers(parameterized: param.Parameterized, value=_unset): - if Version(param.__version__) <= Version('2.0.0a2'): - if value is not _unset: - parameterized._param_watchers = value - else: - return parameterized._param_watchers - else: - if value is not _unset: - parameterized.param.watchers = value - else: - return parameterized.param.watchers - - def recursive_parameterized(parameterized: param.Parameterized, objects=None) -> list[param.Parameterized]: """ Recursively searches a Parameterized object for other Parmeterized From d1d82800250467fbd9eb0248beee37e8682d3154 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Dec 2024 10:12:57 +0100 Subject: [PATCH 43/44] Bump tornado from 6.4.1 to 6.4.2 in /examples/apps/django_multi_apps (#7513) Bumps [tornado](https://github.com/tornadoweb/tornado) from 6.4.1 to 6.4.2. - [Changelog](https://github.com/tornadoweb/tornado/blob/v6.4.2/docs/releases.rst) - [Commits](https://github.com/tornadoweb/tornado/compare/v6.4.1...v6.4.2) --- updated-dependencies: - dependency-name: tornado dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- examples/apps/django_multi_apps/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/apps/django_multi_apps/requirements.txt b/examples/apps/django_multi_apps/requirements.txt index 4f6ab137f3..1c09dd55fb 100644 --- a/examples/apps/django_multi_apps/requirements.txt +++ b/examples/apps/django_multi_apps/requirements.txt @@ -39,7 +39,7 @@ PyYAML==5.4 service-identity==18.1.0 six==1.13.0 sqlparse==0.5.0 -tornado==6.4.1 +tornado==6.4.2 Twisted==24.7.0 txaio==20.4.1 zope.interface==4.7.1 From b2969607b4c4d38d52f7a200a510934ad8fff631 Mon Sep 17 00:00:00 2001 From: Andrew <15331990+ahuang11@users.noreply.github.com> Date: Mon, 2 Dec 2024 01:13:22 -0800 Subject: [PATCH 44/44] tweak repr (#7521) --- panel/chat/message.py | 3 +++ panel/chat/utils.py | 2 +- panel/tests/chat/test_feed.py | 9 +++++++++ panel/tests/chat/test_message.py | 8 ++++++++ 4 files changed, 21 insertions(+), 1 deletion(-) diff --git a/panel/chat/message.py b/panel/chat/message.py index af67993355..b69043cd16 100644 --- a/panel/chat/message.py +++ b/panel/chat/message.py @@ -698,3 +698,6 @@ def serialize( prefix_with_viewable_label=prefix_with_viewable_label, prefix_with_container_label=prefix_with_container_label, ) + + def __repr__(self, depth: int = 0) -> str: + return f"ChatMessage(object={self.object!r}, user={self.user!r}, reactions={self.reactions!r})" diff --git a/panel/chat/utils.py b/panel/chat/utils.py index 773d395f72..32fce1bffa 100644 --- a/panel/chat/utils.py +++ b/panel/chat/utils.py @@ -127,7 +127,7 @@ def get_obj_label(obj): Get the label for the object; defaults to specified object name; if unspecified, defaults to the type name. """ - label = obj.name + label = obj.name if hasattr(obj, "name") else "" type_name = type(obj).__name__ # If the name is just type + ID, simply use type # e.g. Column10241 -> Column diff --git a/panel/tests/chat/test_feed.py b/panel/tests/chat/test_feed.py index 64aa7304d4..b29dc5b0c5 100644 --- a/panel/tests/chat/test_feed.py +++ b/panel/tests/chat/test_feed.py @@ -608,6 +608,15 @@ def test_update_chat_log_params(self, chat_feed): assert chat_feed._chat_log.scroll_button_threshold == 10 assert chat_feed._chat_log.auto_scroll_limit == 10 + def test_repr(self, chat_feed): + chat_feed.send("A") + chat_feed.send("B") + assert repr(chat_feed) == ( + "ChatFeed(_placeholder=ChatMessage, sizing_mode='stretch_width')\n" + " [0] ChatMessage(object='A', user='User', reactions=[])\n" + " [1] ChatMessage(object='B', user='User', reactions=[])" + ) + @pytest.mark.xdist_group("chat") class TestChatFeedPromptUser: diff --git a/panel/tests/chat/test_message.py b/panel/tests/chat/test_message.py index 29870587bc..4d56c81e3f 100644 --- a/panel/tests/chat/test_message.py +++ b/panel/tests/chat/test_message.py @@ -390,3 +390,11 @@ def test_serialize_audio(self): def test_serialize_dataframe(self): message = ChatMessage(DataFrame(pd.DataFrame({'a': [1, 2, 3]}))) assert message.serialize() == "DataFrame= a\n0 1\n1 2\n2 3" + + def test_repr(self): + message = ChatMessage(object="Hello", user="User", avatar="A", reactions=["favorite"]) + assert repr(message) == "ChatMessage(object='Hello', user='User', reactions=['favorite'])" + + def test_repr_dataframe(self): + message = ChatMessage(pd.DataFrame({'a': [1, 2, 3]}), avatar="D") + assert repr(message) == "ChatMessage(object= a\n0 1\n1 2\n2 3, user='User', reactions=[])"