Skip to content

Commit

Permalink
Add template params (#516)
Browse files Browse the repository at this point in the history
  • Loading branch information
ahuang11 committed Apr 18, 2024
1 parent 3f085c6 commit 706c90f
Show file tree
Hide file tree
Showing 5 changed files with 116 additions and 14 deletions.
19 changes: 19 additions & 0 deletions lumen/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,25 @@ def __getitem__(self, key):
_THEMES = ConfigDict("Theme", **_THEMES)


class Template(param.Parameter):
"""
A Parameter type to validate template types,
including dict, str, and Template classes.
"""

def __init__(self, default=_TEMPLATES['material'], **params):
super().__init__(default=default, **params)
self._validate(default)

def _validate(self, val):
self._validate_value(val, self.allow_None)

def _validate_value(self, val, allow_None):
if isinstance(val, (dict, str)) or issubclass(val, BasicTemplate):
return
raise ValueError(f"Template type {type(val).__name__} not recognized.")


class _config(param.Parameterized):
"""
Stores shared configuration for the entire Lumen application.
Expand Down
54 changes: 40 additions & 14 deletions lumen/dashboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
from .auth import Auth
from .base import Component, MultiTypeComponent
from .config import (
_DEFAULT_LAYOUT, _LAYOUTS, _TEMPLATES, _THEMES, config,
_DEFAULT_LAYOUT, _LAYOUTS, _TEMPLATES, _THEMES, Template, config,
)
from .filters.base import ConstantFilter, Filter, WidgetFilter # noqa
from .layout import Layout
Expand Down Expand Up @@ -110,8 +110,7 @@ class Config(Component):
title = param.String(default="Lumen Dashboard", constant=True, doc="""
The title of the dashboard.""")

template = param.Selector(default=_TEMPLATES['material'], constant=True, objects=_TEMPLATES,
check_on_set=False, doc="""
template = Template(default=_TEMPLATES['material'], constant=True, doc="""
The Panel template to render the dashboard into.""")

theme = param.Selector(default=_THEMES['default'], objects=_THEMES, constant=True,
Expand All @@ -124,6 +123,23 @@ class Config(Component):
_valid_keys: ClassVar[Literal['params']] = 'params'
_validate_params: ClassVar[bool] = True

@classmethod
def _extract_template_type(cls, template: str | Dict[str, Any]) -> Tuple[str, Dict[str, Any]]:
if isinstance(template, dict):
template_type = template.get("type", _TEMPLATES["material"])
template_params = template.copy()
else:
template_type = template
template_params = {"type": template}
return template_type, template_params

@classmethod
def _serialize_template(cls, template_params: Dict[str, Any]) -> str | Dict[str, Any]:
if len(template_params) == 1:
return template_params["type"]
else:
return template_params

@classmethod
def _validate_layout(cls, layout, spec, context):
if layout not in _LAYOUTS:
Expand All @@ -133,10 +149,11 @@ def _validate_layout(cls, layout, spec, context):

@classmethod
def _validate_template(
cls, template: str, spec: Dict[str, Any], context: Dict[str, Any]
) -> str:
cls, template: str | Dict[str, Any], spec: Dict[str, Any], context: Dict[str, Any]
) -> Dict[str, Any]:
template, template_params = cls._extract_template_type(template)
if template in _TEMPLATES:
return template
return cls._serialize_template(template_params)
elif '.' not in template:
raise ValidationError(
f'Config template {template!r} not found. Template must be one '
Expand All @@ -162,7 +179,8 @@ def _validate_template(
f'Config template \'{path}.{name}\' is not a valid Panel template.',
spec, 'template'
)
return template
template_params["type"] = template_cls
return cls._serialize_template(template_params)

@classmethod
def _validate_callback(cls, callback: Callable[..., Any] | str) -> Callable[Any, None]:
Expand Down Expand Up @@ -232,28 +250,34 @@ def from_spec(cls, spec: Dict[str, Any] | str) -> 'Dashboard':
"full specification for the Config."
)
if 'template' in spec:
template = spec['template']
template, template_params = cls._extract_template_type(spec['template'])
if template in _TEMPLATES:
template_cls = _TEMPLATES[template]
else:
template_cls = resolve_module_reference(template, BasicTemplate)
spec['template'] = template_cls
template_params['type'] = template_cls
spec['template'] = cls._serialize_template(template_params)

if 'theme' in spec:
spec['theme'] = _THEMES[spec['theme']]
if 'layout' in spec:
spec['layout'] = _LAYOUTS[spec['layout']]
for key in list(spec):
if key.startswith('on_'):
spec[key] = getattr(cls, f'_validate_{key}')(spec[key], spec, {})

return cls(**spec)

def to_spec(self, context: Dict[str, Any] | None = None) -> Dict[str, Any]:
spec = super().to_spec(context=context)
if 'layout' in spec:
spec['layout'] = {v: k for k, v in _LAYOUTS.items()}[spec['layout']]
if 'template' in spec:
tmpl = spec['template']
spec['template'] = {v: k for k, v in _TEMPLATES.items()}.get(tmpl, f'{tmpl.__module__}.{tmpl.__name__}')
template = spec['template']
template, template_params = self._extract_template_type(template)
template_params['type'] = {v: k for k, v in _TEMPLATES.items()}.get(
template, f'{template.__module__}.{template.__name__}')
spec['template'] = self._serialize_template(template_params)
if 'theme' in spec:
spec['theme'] = {v: k for k, v in _THEMES.items()}[spec['theme']]
for key in list(spec):
Expand Down Expand Up @@ -282,10 +306,12 @@ def __init__(self, **params):
pass

def construct_template(self):
params = {'title': self.title, 'theme': self.theme}
template, template_params = self._extract_template_type(self.template)
template_params.update({'title': self.title, 'theme': self.theme})
if self.logo:
params['logo'] = self.config.logo
return self.template(**params)
template_params['logo'] = self.config.logo
template_params.pop('type')
return template(**template_params)


class Defaults(Component):
Expand Down
17 changes: 17 additions & 0 deletions lumen/tests/sample_dashboard/template.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
config:
template: vanilla
sources:
test:
type: 'file'
tables: ['../sources/test.csv']
targets:
- title: "Test 1"
source: test
views:
- table: test
type: test
- title: "Test 2"
source: test
views:
- table: test
type: test
19 changes: 19 additions & 0 deletions lumen/tests/sample_dashboard/template_params.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
config:
template:
type: vanilla
collapsed_sidebar: true
sources:
test:
type: 'file'
tables: ['../sources/test.csv']
targets:
- title: "Test 1"
source: test
views:
- table: test
type: test
- title: "Test 2"
source: test
views:
- table: test
type: test
21 changes: 21 additions & 0 deletions lumen/tests/test_dashboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -218,3 +218,24 @@ def test_dashboard_with_view_and_transform_variable(set_root, document):
list(layout._pipelines.values())[0].param.trigger('update')

assert plot.object.vdims == ['Y']


def test_dashboard_with_template_string(set_root, document):
root = pathlib.Path(__file__).parent / 'sample_dashboard'
set_root(str(root))
dashboard = Dashboard(str(root / 'template.yaml'))
dashboard._render_dashboard()
assert isinstance(dashboard._template, pn.template.VanillaTemplate)

assert dashboard.to_spec()['config']['template'] == "vanilla"


def test_dashboard_with_template_params(set_root, document):
root = pathlib.Path(__file__).parent / 'sample_dashboard'
set_root(str(root))
dashboard = Dashboard(str(root / 'template_params.yaml'))
dashboard._render_dashboard()
assert isinstance(dashboard._template, pn.template.VanillaTemplate)
assert dashboard._template.collapsed_sidebar

assert dashboard.to_spec()['config']['template'] == {'type': 'vanilla', 'collapsed_sidebar': True}

0 comments on commit 706c90f

Please sign in to comment.