Skip to content

Commit 0f9539f

Browse files
authored
Merge pull request #96 from rstudio/cdn
2 parents 81ae13e + 2636a75 commit 0f9539f

File tree

4 files changed

+42
-15
lines changed

4 files changed

+42
-15
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [UNRELEASED]
99

10+
* Closed #94: New `SHINYWIDGETS_CDN` and `SHINYWIDGETS_CDN_ONLY` environment variables were added to more easily specify the CDN provider. Also, the default provider has changed from <unpkg.com> to <cdn.jsdelivr.net/npm> (#95)
11+
* A warning is no longer issued (by default) when the path to a local widget extension is not found. This is because, if an internet connection is available, the widget assests are still loaded via CDN. To restore the previous behavior, set the `SHINYWIDGETS_EXTENSION_WARNING` environment variable to `"true"`. (#95)
1012
* Closed #14: Added a `bokeh_dependency()` function to simplify use of bokeh widgets. (#85)
1113

1214
## [0.1.6] - 2023-03-24

setup.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ setup_requires =
3434
install_requires =
3535
ipywidgets>=7.6.5
3636
jupyter_core
37-
shiny>=0.2.10.9001
37+
shiny>=0.3.0
3838
python-dateutil>=2.8.2
3939
# Needed because of https://github.com/python/importlib_metadata/issues/411
4040
importlib-metadata>=4.8.3,<5; python_version < "3.8"

shinywidgets/_dependencies.py

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,9 @@ def output_binding_dependency() -> HTMLDependency:
6161
# possible for a Widget to have traits that are themselves Widgets
6262
# (which could have their own npm module), but in practice, I haven't seen any cases
6363
# where a 3rd party widget can contain a 3rd party widget.
64-
def require_dependency(w: Widget, session: Session) -> Optional[HTMLDependency]:
64+
def require_dependency(
65+
w: Widget, session: Session, warn_if_missing: bool = True
66+
) -> Optional[HTMLDependency]:
6567
"""
6668
Obtain an HTMLDependency for a 3rd party ipywidget that points
6769
require('widget-npm-package') requests in the browser to the correct local path.
@@ -86,11 +88,13 @@ def require_dependency(w: Widget, session: Session) -> Optional[HTMLDependency]:
8688
if module_dir is None:
8789
module_dir = jupyter_extension_path(jupyter_extension_destination(w))
8890
if module_dir is None:
89-
warnings.warn(
90-
f"Failed to discover JavaScript dependencies for {type(w)}."
91-
+ " Make sure it is installed as a jupyter notebook extension.",
92-
stacklevel=2,
93-
)
91+
if warn_if_missing:
92+
warnings.warn(
93+
f"Couldn't find local path to widget extension for {type(w)}."
94+
+ " Since a CDN fallback is provided, the widget will still render if an internet connection is available."
95+
+ " To avoid depending on a CDN, make sure the widget is installed as a jupyter extension.",
96+
stacklevel=2,
97+
)
9498
return None
9599

96100
version = parse_version_safely(getattr(w, "_model_module_version", "1.0"))
@@ -113,7 +117,7 @@ def require_dependency(w: Widget, session: Session) -> Optional[HTMLDependency]:
113117

114118

115119
def bokeh_dependency() -> HTMLDependency:
116-
from bokeh.resources import Resources
120+
from bokeh.resources import Resources
117121

118122
resources = Resources(mode="inline").render()
119123
return ui.head_content(ui.HTML(resources))

shinywidgets/_shinywidgets.py

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from uuid import uuid4
1414
from weakref import WeakSet
1515

16-
from htmltools import Tag, TagList, css, tags
16+
from htmltools import Tag, TagList, css, head_content, tags
1717
from ipywidgets._version import (
1818
__protocol_version__, # pyright: ignore[reportUnknownVariableType]
1919
)
@@ -36,6 +36,16 @@
3636
widget_pkg,
3737
)
3838

39+
# Make it easier to customize the CDN fallback (and make it CDN-only)
40+
# https://ipywidgets.readthedocs.io/en/7.6.3/embedding.html#python-interface
41+
# https://github.com/jupyter-widgets/ipywidgets/blob/6f6156c7/packages/html-manager/src/libembed-amd.ts#L6-L14
42+
SHINYWIDGETS_CDN = os.getenv("SHINYWIDGETS_CDN", "https://cdn.jsdelivr.net/npm/")
43+
SHINYWIDGETS_CDN_ONLY = os.getenv("SHINYWIDGETS_CDN_ONLY", "false").lower() == "true"
44+
# Should shinywidgets warn if unable to find a local path to a widget extension?
45+
SHINYWIDGETS_EXTENSION_WARNING = (
46+
os.getenv("SHINYWIDGETS_EXTENSION_WARNING", "false").lower() == "true"
47+
)
48+
3949

4050
def output_widget(
4151
id: str, *, width: Optional[str] = None, height: Optional[str] = None
@@ -44,6 +54,12 @@ def output_widget(
4454
return tags.div(
4555
*libembed_dependency(),
4656
output_binding_dependency(),
57+
head_content(
58+
tags.script(
59+
data_jupyter_widgets_cdn=SHINYWIDGETS_CDN,
60+
data_jupyter_widgets_cdn_only=SHINYWIDGETS_CDN_ONLY,
61+
)
62+
),
4763
id=id,
4864
class_="shiny-ipywidget-output shiny-report-size shiny-report-theme",
4965
style=css(width=width, height=height),
@@ -83,7 +99,10 @@ def init_shiny_widget(w: Widget):
8399
# Make sure window.require() calls made by 3rd party widgets
84100
# (via handle_comm_open() -> new_model() -> loadClass() -> requireLoader())
85101
# actually point to directories served locally by shiny
86-
widget_dep = require_dependency(w, session)
102+
if SHINYWIDGETS_CDN_ONLY:
103+
widget_dep = None
104+
else:
105+
widget_dep = require_dependency(w, session, SHINYWIDGETS_EXTENSION_WARNING)
87106

88107
# By the time we get here, the user has already had an opportunity to specify a model_id,
89108
# so it isn't yet populated, generate a random one so we can assign the same id to the comm
@@ -108,11 +127,13 @@ def init_shiny_widget(w: Widget):
108127
# be doing on load in js/src/output.ts)
109128
# https://github.com/jupyter-widgets/widget-cookiecutter/blob/9694718/%7B%7Bcookiecutter.github_project_name%7D%7D/js/lib/extension.js#L8
110129
if widget_dep and widget_dep.source:
111-
session.app._dependency_handler.mount(
112-
f"/nbextensions/{widget_dep.name}",
113-
StaticFiles(directory=widget_dep.source["subdir"]),
114-
name=f"{widget_dep.name}-nbextension-static-resources",
115-
)
130+
src_dir = widget_dep.source.get("subdir", "")
131+
if src_dir:
132+
session.app._dependency_handler.mount(
133+
f"/nbextensions/{widget_dep.name}",
134+
StaticFiles(directory=src_dir),
135+
name=f"{widget_dep.name}-nbextension-static-resources",
136+
)
116137

117138
# everything after this point should be done once per session
118139
if session in SESSIONS:

0 commit comments

Comments
 (0)