From 19411ae90e564005a5324e303136983ccb50933a Mon Sep 17 00:00:00 2001 From: Tom Jakubowski Date: Wed, 1 Nov 2023 16:24:40 -0700 Subject: [PATCH] PerspectiveWidget HTML export support in Jupyter Add `text/html` output to PerspectiveWidget's `_repr_mimebundle_` method, which is read by nbconvert when exporting a notebook to HTML. --- docs/docs/development.md | 6 +++ python/perspective/perspective/__init__.py | 4 ++ .../templates/exported_widget.html.jinja | 41 +++++++++++++++++++ .../perspective/widget/__init__.py | 5 ++- .../perspective/perspective/widget/widget.py | 39 ++++++++++++++++++ python/perspective/setup.py | 2 + 6 files changed, 95 insertions(+), 2 deletions(-) create mode 100644 python/perspective/perspective/templates/exported_widget.html.jinja diff --git a/docs/docs/development.md b/docs/docs/development.md index 18120587ca..dc30c5fe28 100644 --- a/docs/docs/development.md +++ b/docs/docs/development.md @@ -104,6 +104,12 @@ To install the Jupyterlab/Jupyter Notebook plugins from your local working directory, simply install `python/perspective` with `pip` as you might normally do. +```bash +(cd packages/perspective-jupyterlab && yarn run build) +pip install -e python/perspective +jupyter labextension develop python/perspective +``` + Afterwards, you should see it listed as a "local extension" when you run `jupyter labextension list` and as a normal extension when you run `jupyter nbextension list`. diff --git a/python/perspective/perspective/__init__.py b/python/perspective/perspective/__init__.py index 9ad4071b61..1ec1a36a82 100644 --- a/python/perspective/perspective/__init__.py +++ b/python/perspective/perspective/__init__.py @@ -17,3 +17,7 @@ from .handlers import * from .nbextension import _jupyter_nbextension_paths from .widget import * + + +def _jupyter_labextension_paths(): + return [{"src": "labextension", "dest": "@finos/perspective-jupyterlab"}] diff --git a/python/perspective/perspective/templates/exported_widget.html.jinja b/python/perspective/perspective/templates/exported_widget.html.jinja new file mode 100644 index 0000000000..9ba7dddb5c --- /dev/null +++ b/python/perspective/perspective/templates/exported_widget.html.jinja @@ -0,0 +1,41 @@ + + + + + + + +
+ + + +
diff --git a/python/perspective/perspective/widget/__init__.py b/python/perspective/perspective/widget/__init__.py index 1b826a6a2f..0eba0dde2f 100644 --- a/python/perspective/perspective/widget/__init__.py +++ b/python/perspective/perspective/widget/__init__.py @@ -10,6 +10,7 @@ # ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ # ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ -from .widget import PerspectiveWidget +from .widget import PerspectiveWidget, set_jupyter_html_export -__all__ = ["PerspectiveWidget"] + +__all__ = ["PerspectiveWidget", "set_jupyter_html_export"] diff --git a/python/perspective/perspective/widget/widget.py b/python/perspective/perspective/widget/widget.py index 84751844ae..5c77866dc1 100644 --- a/python/perspective/perspective/widget/widget.py +++ b/python/perspective/perspective/widget/widget.py @@ -10,7 +10,10 @@ # ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ # ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +import base64 +import jinja2 import json +import os from datetime import date, datetime from functools import partial @@ -515,3 +518,39 @@ def _make_load_message(self): return _PerspectiveWidgetMessage(-2, "table", msg_data) else: raise PerspectiveError("Widget does not have any data loaded - use the `load()` method to provide it with data.") + + def _repr_mimebundle_(self, **kwargs): + super_bundle = super(DOMWidget, self)._repr_mimebundle_(**kwargs) + if not _jupyter_html_export_enabled(): + return super_bundle + # Serialize viewer attrs + view data to be rendered in the template + viewer_attrs = self.save() + data = self.table.view().to_arrow() + b64_data = base64.encodebytes(data) + + jinja_env = jinja2.Environment( + loader=jinja2.PackageLoader("perspective"), + autoescape=jinja2.select_autoescape(), + ) + template = jinja_env.get_template("exported_widget.html.jinja") + + return super(DOMWidget, self)._repr_mimebundle_(**kwargs) | { + "text/html": template.render( + psp_version=__version__, + viewer_id=self.model_id, + viewer_attrs=viewer_attrs, + b64_data=b64_data.decode("utf-8"), + ) + } + + +def _jupyter_html_export_enabled(): + return os.environ.get("PSP_JUPYTER_HTML_EXPORT", None) == "1" + + +def set_jupyter_html_export(val): + """Enables HTML export for Jupyter widgets, when set to True. + HTML export can also be enabled by setting the environment variable + `PSP_JUPYTER_HTML_EXPORT` to the string `1`. + """ + os.environ["PSP_JUPYTER_HTML_EXPORT"] = "1" if val else "0" diff --git a/python/perspective/setup.py b/python/perspective/setup.py index 6c306ebd38..2ef8758a5a 100644 --- a/python/perspective/setup.py +++ b/python/perspective/setup.py @@ -56,7 +56,9 @@ ######################## # Get requirement info # requires = [ + "Jinja2>=2.0,<4", "ipywidgets>=7.5.1,<9", + "future>=0.16.0,<1", "numpy>=1.21.6,<2", "pandas>=0.22.0,<3", "python-dateutil>=2.8.0,<3",