Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Correctly handle resource mode in conversion #3967

Merged
merged 7 commits into from
Oct 8, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ jobs:
PYTHON_VERSION: ${{ matrix.python-version }}
SETUPTOOLS_ENABLE_FEATURES: "legacy-editable"
DISPLAY: ":99.0"
PYTHONIOENCODING: "utf-8"
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Without this env var `doit env_create ...` uses by default
# the `pyviz` channel, except that we don't want to configure
Expand Down Expand Up @@ -99,6 +100,7 @@ jobs:
shell: bash -l {0}
env:
DESC: "Python ${{ matrix.python-version }} tests"
PYTHONIOENCODING: "utf-8"
PANEL_LOG_LEVEL: info
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SETUPTOOLS_ENABLE_FEATURES: "legacy-editable"
Expand All @@ -116,6 +118,10 @@ jobs:
cache: true
playwright: true
id: install
- name: build pyodide wheels
run: |
conda activate test-environment
python scripts/build_pyodide_wheels.py
- name: launch jupyter
run: |
conda activate test-environment
Expand Down
37 changes: 20 additions & 17 deletions panel/io/convert.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
from bokeh.embed.elements import script_for_render_items
from bokeh.embed.util import RenderItem, standalone_docs_json_and_render_items
from bokeh.embed.wrappers import wrap_in_script_tag
from bokeh.settings import settings as _settings
from bokeh.util.serialization import make_id
from typing_extensions import Literal

Expand All @@ -27,7 +26,7 @@
from .mime_render import find_imports
from .resources import (
CDN_DIST, DIST_DIR, INDEX_TEMPLATE, Resources, _env as _pn_env,
bundle_resources,
bundle_resources, set_resource_mode,
)
from .state import set_curdoc, state

Expand All @@ -39,6 +38,8 @@
PANEL_ROOT = pathlib.Path(__file__).parent.parent
BOKEH_VERSION = '2.4.3'
PY_VERSION = base_version(__version__)
PANEL_LOCAL_WHL = DIST_DIR / 'wheels' / f'panel-{__version__.replace("-dirty", "")}-py3-none-any.whl'
BOKEH_LOCAL_WHL = DIST_DIR / 'wheels' / f'bokeh-{BOKEH_VERSION}-py3-none-any.whl'
PANEL_CDN_WHL = f'{CDN_DIST}wheels/panel-{PY_VERSION}-py3-none-any.whl'
BOKEH_CDN_WHL = f'{CDN_DIST}wheels/bokeh-{BOKEH_VERSION}-py3-none-any.whl'
PYODIDE_URL = 'https://cdn.jsdelivr.net/pyodide/v0.21.3/full/pyodide.js'
Expand Down Expand Up @@ -164,7 +165,7 @@ def script_to_html(
css_resources: Literal['auto'] | List[str] | None = None,
runtime: Runtimes = 'pyodide',
prerender: bool = True,
panel_version: Literal['auto'] | str = 'auto',
panel_version: Literal['auto', 'local'] | str = 'auto',
manifest: str | None = None
) -> str:
"""
Expand All @@ -188,9 +189,6 @@ def script_to_html(
panel_version: 'auto' | str
The panel release version to use in the exported HTML.
"""
# Configure resources
_settings.resources.set_value('cdn')

# Run script
path = pathlib.Path(filename)
name = '.'.join(path.name.split('.')[:-1])
Expand Down Expand Up @@ -225,7 +223,10 @@ def script_to_html(
)

# Environment
if panel_version == 'auto':
if panel_version == 'local':
panel_req = './' + str(PANEL_LOCAL_WHL.as_posix()).split('/')[-1]
bokeh_req = './' + str(BOKEH_LOCAL_WHL.as_posix()).split('/')[-1]
elif panel_version == 'auto':
panel_req = PANEL_CDN_WHL
bokeh_req = BOKEH_CDN_WHL
else:
Expand Down Expand Up @@ -324,10 +325,6 @@ def script_to_html(
html = (html
.replace('<body>', f'<body class="bk pn-loading {config.loading_spinner}">')
)

# Reset resources
_settings.resources.unset_value()

return html, web_worker


Expand All @@ -338,13 +335,16 @@ def convert_app(
runtime: Runtimes = 'pyodide-worker',
prerender: bool = True,
manifest: str | None = None,
panel_version: Literal['auto', 'local'] | str = 'auto',
verbose: bool = True,
):
try:
html, js_worker = script_to_html(
app, requirements=requirements, runtime=runtime,
prerender=prerender, manifest=manifest
)
with set_resource_mode('cdn'):
html, js_worker = script_to_html(
app, requirements=requirements, runtime=runtime,
prerender=prerender, manifest=manifest,
panel_version=panel_version
)
except KeyboardInterrupt:
return
except Exception as e:
Expand Down Expand Up @@ -372,8 +372,9 @@ def convert_apps(
build_index: bool = True,
build_pwa: bool = True,
pwa_config: Dict[Any, Any] = {},
max_workers: int = 4,
panel_version: Literal['auto', 'local'] | str = 'auto',
verbose: bool = True,
max_workers: int = 4
):
"""
Arguments
Expand Down Expand Up @@ -407,6 +408,8 @@ def convert_apps(
- theme_color: The theme color of the application
max_workers: int
The maximum number of parallel workers
panel_version: 'auto' | 'local'] | str
' The panel version to include.
"""
if isinstance(apps, str):
apps = [apps]
Expand All @@ -426,7 +429,7 @@ def convert_apps(
f = executor.submit(
convert_app, app, dest_path, requirements=requirements,
runtime=runtime, prerender=prerender, manifest=manifest,
verbose=verbose
panel_version=panel_version, verbose=verbose
)
futures.append(f)
for future in concurrent.futures.as_completed(futures):
Expand Down
8 changes: 5 additions & 3 deletions panel/io/resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@
import param

from bokeh.embed.bundle import (
Bundle as BkBundle, _bundle_extensions, _use_mathjax, bundle_models,
extension_dirs,
CSS_RESOURCES as BkCSS_RESOURCES, Bundle as BkBundle, _bundle_extensions,
_use_mathjax, bundle_models, extension_dirs,
)
from bokeh.resources import Resources as BkResources
from bokeh.settings import settings as _settings
Expand Down Expand Up @@ -376,7 +376,6 @@ def css_files(self):
if self.mode == 'inline':
break
css_files.append(dist_dir + f'css/{os.path.basename(cssf)}')

return css_files

@property
Expand Down Expand Up @@ -424,6 +423,9 @@ def from_bokeh(cls, bk_bundle):
hashes=bk_bundle.hashes,
)

def _render_css(self) -> str:
return BkCSS_RESOURCES.render(css_files=self._adjust_paths(self.css_files), css_raw=self.css_raw)

def _render_js(self):
return JS_RESOURCES.render(
js_raw=self.js_raw, js_files=self._adjust_paths(self.js_files),
Expand Down
4 changes: 2 additions & 2 deletions panel/models/tabulator.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from bokeh.models.widgets.tables import TableColumn

from ..config import config
from ..io.resources import bundled_files
from ..io.resources import JS_VERSION, bundled_files
from ..util import classproperty

TABULATOR_VERSION = "5.3.2"
Expand All @@ -23,7 +23,7 @@

THEME_PATH = f"tabulator-tables@{TABULATOR_VERSION}/dist/css/"
THEME_URL = f"{config.npm_cdn}/{THEME_PATH}"
PANEL_CDN = f"{config.npm_cdn}/@holoviz/panel/dist/bundled/{THEME_PATH}"
PANEL_CDN = f"{config.npm_cdn}/@holoviz/panel@{JS_VERSION}/dist/bundled/datatabulator/{THEME_PATH}"
TABULATOR_THEMES = [
'default', 'site', 'simple', 'midnight', 'modern', 'bootstrap',
'bootstrap4', 'materialize', 'bulma', 'semantic-ui', 'fast'
Expand Down
102 changes: 70 additions & 32 deletions panel/tests/ui/io/test_convert.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import glob
import os
import pathlib
import shutil
import tempfile
import time

Expand All @@ -15,7 +18,14 @@
pytestmark = pytest.mark.ui

from panel.config import config
from panel.io.convert import convert_apps
from panel.io.convert import BOKEH_LOCAL_WHL, PANEL_LOCAL_WHL, convert_apps

if not (PANEL_LOCAL_WHL.is_file() and BOKEH_LOCAL_WHL.is_file()):
pytest.skip(
"Skipped because pyodide wheels are not available for current "
"version. Build wheels for pyodide using `python scripts/build_pyodide_wheels.py`.",
allow_module_level=True
)

button_app = """
import panel as pn
Expand All @@ -36,52 +46,80 @@
pn.Row(slider, pn.bind(lambda v: v, slider)).servable();
"""

tabulator_app = """
import panel as pn
import pandas as pd
tabulator = pn.widgets.Tabulator(pd._testing.makeMixedDataFrame())

def on_click(e):
tabulator.theme = 'fast'

button = pn.widgets.Button()
button.on_click(on_click)

pn.Row(button, tabulator).servable();
"""

def write_app(app):
"""
Writes app to temporary file and returns path.
"""
nf = tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False)
nf = tempfile.NamedTemporaryFile(mode='w', suffix='.py', encoding='utf-8', delete=False)
nf.write(app)
nf.flush()
dest = pathlib.Path(nf.name).parent
try:
shutil.copy(PANEL_LOCAL_WHL, dest / PANEL_LOCAL_WHL.name)
except shutil.SameFileError:
pass
try:
shutil.copy(BOKEH_LOCAL_WHL, dest / BOKEH_LOCAL_WHL.name)
except shutil.SameFileError:
pass
return nf

@pytest.fixture(scope="module")
def start_server():
def launch_app():
_PROCESSES = []

def start(path):
def start(code):
nf = write_app(code)
app_path = pathlib.Path(nf.name)
process = Popen(
["python", "-m", "http.server", "8123", "--directory", str(path.parent)], stdout=PIPE
["python", "-m", "http.server", "8123", "--directory", str(app_path.parent)], stdout=PIPE
)
retries = 5
while retries > 0:
conn = HTTPConnection("localhost:8123")
try:
conn.request("HEAD", str(path.name))
conn.request("HEAD", str(app_path.name))
response = conn.getresponse()
if response is not None:
_PROCESSES.append(process)
_PROCESSES.append((process, nf.name))
break
except ConnectionRefusedError:
time.sleep(1)
retries -= 1

if not retries:
raise RuntimeError("Failed to start http server")
return app_path
yield start
for process in _PROCESSES:
for process, name in _PROCESSES:
process.terminate()
process.wait()
for f in glob.glob(f'{name[:-3]}*'):
os.remove(f)


@pytest.mark.parametrize('runtime', ['pyodide', 'pyscript', 'pyodide-worker'])
def test_pyodide_test_convert_button_app(page, runtime, start_server):
nf = write_app(button_app)
app_path = pathlib.Path(nf.name)
start_server(app_path)
def test_pyodide_test_convert_button_app(page, runtime, launch_app):
app_path = launch_app(button_app)

convert_apps([app_path], app_path.parent, runtime=runtime, build_pwa=False, build_index=False, prerender=False, panel_version='local')

convert_apps([app_path], app_path.parent, runtime=runtime, build_pwa=False, build_index=False, prerender=False)
msgs = []
page.on("console", lambda msg: msgs.append(msg))

page.goto(f"http://localhost:8123/{app_path.name[:-3]}.html")

Expand All @@ -95,14 +133,16 @@ def test_pyodide_test_convert_button_app(page, runtime, start_server):

expect(page.locator(".bk.bk-clearfix")).to_have_text('1')

assert [msg for msg in msgs if msg.type == 'error' and 'favicon' not in msg.location['url']] == []

@pytest.mark.parametrize('runtime', ['pyodide', 'pyscript', 'pyodide-worker'])
def test_pyodide_test_convert_slider_app(page, runtime, start_server):
nf = write_app(slider_app)
app_path = pathlib.Path(nf.name)
start_server(app_path)
def test_pyodide_test_convert_slider_app(page, runtime, launch_app):
app_path = launch_app(slider_app)

convert_apps([app_path], app_path.parent, runtime=runtime, build_pwa=False, build_index=False, prerender=False)
convert_apps([app_path], app_path.parent, runtime=runtime, build_pwa=False, build_index=False, prerender=False, panel_version='local')

msgs = []
page.on("console", lambda msg: msgs.append(msg))

page.goto(f"http://localhost:8123/{app_path.name[:-3]}.html")

Expand All @@ -117,27 +157,25 @@ def test_pyodide_test_convert_slider_app(page, runtime, start_server):

expect(page.locator(".bk.bk-clearfix")).to_have_text('0.1')

assert [msg for msg in msgs if msg.type == 'error' and 'favicon' not in msg.location['url']] == []

@pytest.mark.parametrize('runtime', ['pyodide', 'pyscript', 'pyodide-worker'])
def test_pyodide_test_convert_tabulator_app(page, runtime, launch_app):
app_path = launch_app(tabulator_app)

@pytest.mark.parametrize('runtime', ['pyodide-worker'])
def test_pyodide_test_convert_location_app(page, runtime, start_server):
nf = write_app(location_app)
app_path = pathlib.Path(nf.name)
start_server(app_path)
convert_apps([app_path], app_path.parent, runtime=runtime, build_pwa=False, build_index=False, prerender=False, panel_version='local')

convert_apps([app_path], app_path.parent, runtime=runtime, build_pwa=False, build_index=False, prerender=False)
msgs = []
page.on("console", lambda msg: msgs.append(msg))

app_url = f"http://localhost:8123/{app_path.name[:-3]}.html"
page.goto(f"{app_url}?value=3.14")
page.goto(f"http://localhost:8123/{app_path.name[:-3]}.html")

cls = f'bk pn-loading {config.loading_spinner}'
expect(page.locator('body')).to_have_class(cls)
expect(page.locator('body')).not_to_have_class(cls, timeout=60 * 1000)

expect(page.locator(".bk.bk-clearfix")).to_have_text('3.14')

page.click('.noUi-handle')
page.keyboard.press('ArrowRight')
page.click('.bk.bk-btn')

expect(page.locator(".bk.bk-clearfix")).to_have_text('3.2')
time.sleep(1)

assert page.url == f"{app_url}?value=3.2"
assert [msg for msg in msgs if msg.type == 'error' and 'favicon' not in msg.location['url']] == []
5 changes: 3 additions & 2 deletions panel/widgets/tables.py
Original file line number Diff line number Diff line change
Expand Up @@ -1188,14 +1188,15 @@ def _process_event(self, event):
def _get_theme(self, theme, resources=None):
from ..io.resources import RESOURCE_MODE
from ..models.tabulator import (
_TABULATOR_THEMES_MAPPING, THEME_PATH, THEME_URL, _get_theme_url,
_TABULATOR_THEMES_MAPPING, PANEL_CDN, THEME_PATH, THEME_URL,
_get_theme_url,
)
if RESOURCE_MODE == 'server' and resources in (None, 'server'):
theme_url = f'{LOCAL_DIST}bundled/datatabulator/{THEME_PATH}'
if state.rel_path:
theme_url = f'{state.rel_path}/{theme_url}'
else:
theme_url = THEME_URL
theme_url = PANEL_CDN
# Ensure theme_url updates before theme
cdn_url = _get_theme_url(THEME_URL, theme)
theme_url = _get_theme_url(theme_url, theme)
Expand Down