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

Allow customizing the title of each app served with pn.serve #1354

Merged
merged 4 commits into from
May 19, 2020
Merged
Show file tree
Hide file tree
Changes from 3 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
8 changes: 8 additions & 0 deletions examples/user_guide/Deploy_and_Export.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,14 @@
"pn.serve({'markdown': '# This is a Panel app', 'json': pn.pane.JSON({'abc': 123})})\n",
"```\n",
"\n",
"You can customize the HTML title of each application by supplying a dictionnary where the keys represent the URL slugs and the values represent the titles, e.g.:\n",
philippjfr marked this conversation as resolved.
Show resolved Hide resolved
"\n",
"```python\n",
"pn.serve(\n",
" {'markdown': '# This is a Panel app', 'json': pn.pane.JSON({'abc': 123})},\n",
" title={'markdown': 'A Markdown App', 'json': 'A JSON App'}\n",
")\n",
"\n",
"The ``pn.serve`` function accepts the same arguments as the `show` method.\n",
"\n",
"\n",
Expand Down
22 changes: 17 additions & 5 deletions panel/io/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,8 +130,9 @@ def serve(panels, port=0, websocket_origin=None, loop=None, show=True,
Whether to open the server in a new browser tab on start
start : boolean(optional, default=False)
Whether to start the Server
title: str (optional, default=None)
An HTML title for the application
title: str or {str: str} (optional, default=None)
An HTML title for the application or a dictionnary mapping
philippjfr marked this conversation as resolved.
Show resolved Hide resolved
from the URL slug to a customized title
verbose: boolean (optional, default=True)
Whether to print the address and port
location : boolean or panel.io.location.Location
Expand Down Expand Up @@ -188,8 +189,9 @@ def get_server(panel, port=0, websocket_origin=None, loop=None,
Whether to open the server in a new browser tab on start
start : boolean(optional, default=False)
Whether to start the Server
title: str (optional, default=None)
An HTML title for the application
title: str or {str: str} (optional, default=None)
An HTML title for the application or a dictionnary mapping
philippjfr marked this conversation as resolved.
Show resolved Hide resolved
from the URL slug to a customized title
verbose: boolean (optional, default=False)
Whether to report the address and port
location : boolean or panel.io.location.Location
Expand All @@ -210,6 +212,16 @@ def get_server(panel, port=0, websocket_origin=None, loop=None,
if isinstance(panel, dict):
apps = {}
for slug, app in panel.items():
if isinstance(title, dict):
try:
title_ = title[slug]
except KeyError:
raise KeyError(
"Keys of the title dictionnary and of the apps "
f"dictionnary must match. No {slug} key found in the "
philippjfr marked this conversation as resolved.
Show resolved Hide resolved
"title dictionnary.")
else:
title_ = title
slug = slug if slug.startswith('/') else '/'+slug
if 'flask' in sys.modules:
from flask import Flask
Expand All @@ -222,7 +234,7 @@ def get_server(panel, port=0, websocket_origin=None, loop=None,
extra_patterns.append(('^'+slug+'.*', ProxyFallbackHandler,
dict(fallback=wsgi, proxy=slug)))
continue
apps[slug] = partial(_eval_panel, app, server_id, title, location)
apps[slug] = partial(_eval_panel, app, server_id, title_, location)
else:
apps = {'/': partial(_eval_panel, panel, server_id, title, location)}

Expand Down
31 changes: 31 additions & 0 deletions panel/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

from panel.pane import HTML, Markdown
from panel.io import state
from panel import serve


@pytest.fixture
Expand Down Expand Up @@ -117,6 +118,36 @@ def markdown_server_session():
pass # tests may already close this


@pytest.fixture
def multiple_apps_server_sessions():
"""Serve multiple apps and yield a factory to allow
parameterizing the slugs and the titles."""
servers = []
def create_sessions(slugs, titles):
app1_slug, app2_slug = slugs
apps = {
app1_slug: Markdown('First app'),
app2_slug: Markdown('Second app')
}
server = serve(apps, port=5008, title=titles, show=False, start=False)
servers.append(server)
session1 = pull_session(
url=f"http://localhost:{server.port:d}/app1",
io_loop=server.io_loop
)
session2 = pull_session(
url=f"http://localhost:{server.port:d}/app2",
io_loop=server.io_loop
)
return session1, session2
yield create_sessions
for server in servers:
try:
server.stop()
except AssertionError:
continue # tests may already close this


@contextmanager
def set_env_var(env_var, value):
old_value = os.environ.get(env_var)
Expand Down
14 changes: 14 additions & 0 deletions panel/tests/test_server.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import pytest

from panel.models import HTML as BkHTML
from panel.io import state

Expand Down Expand Up @@ -43,3 +45,15 @@ def test_kill_all_servers(html_server_session, markdown_server_session):
state.kill_all_servers()
assert server_1._stopped
assert server_2._stopped

def test_multiple_titles(multiple_apps_server_sessions):
"""Serve multiple apps with a title per app."""
session1, session2 = multiple_apps_server_sessions(
slugs=('app1', 'app2'), titles={'app1': 'APP1', 'app2': 'APP2'})
assert session1.document.title == 'APP1'
assert session2.document.title == 'APP2'

# Slug names and title keys should match
with pytest.raises(KeyError):
session1, session2 = multiple_apps_server_sessions(
slugs=('app1', 'app2'), titles={'badkey': 'APP1', 'app2': 'APP2'})