Source code for panel.theme.base

from __future__ import annotations

import functools
import os
import pathlib

from typing import (
    TYPE_CHECKING, Any, ClassVar, Dict, List, Literal, Tuple, Type,
)

import param

from bokeh.models import ImportedStyleSheet
from bokeh.themes import Theme as _BkTheme, _dark_minimal, built_in_themes

from ..config import config
from ..io.resources import (
    ResourceComponent, component_resource_path, get_dist_path,
    resolve_custom_path,
)
from ..util import relative_to

if TYPE_CHECKING:
    from bokeh.document import Document
    from bokeh.model import Model

    from ..io.resources import ResourceTypes
    from ..viewable import Viewable


[docs]class Inherit: """ Singleton object to declare stylesheet inheritance. """
[docs]class Theme(param.Parameterized): """ Theme objects declare the styles to switch between different color modes. Each `Design` may declare any number of color themes. `modifiers` The modifiers override parameter values of Panel components. """ base_css = param.Filename(doc=""" A stylesheet declaring the base variables that define the color scheme. By default this is inherited from a base class.""") bokeh_theme = param.ClassSelector(class_=(_BkTheme, str), default=None, doc=""" A Bokeh Theme class that declares properties to apply to Bokeh models. This is necessary to ensure that plots and other canvas based components are styled appropriately.""") css = param.Filename(doc=""" A stylesheet that overrides variables specifically for the Theme subclass. In most cases, this is not necessary.""") modifiers: ClassVar[Dict[Viewable, Dict[str, Any]]] = {}
BOKEH_DARK = dict(_dark_minimal.json) BOKEH_DARK['attrs']['Plot'].update({ "background_fill_color": "#2b3035", "border_fill_color": "#212529", }) THEME_CSS = pathlib.Path(__file__).parent / 'css'
[docs]class DefaultTheme(Theme): """ Baseclass for default or light themes. """ base_css = param.Filename(default=THEME_CSS / 'default.css') _name: ClassVar[str] = 'default'
[docs]class DarkTheme(Theme): """ Baseclass for dark themes. """ base_css = param.Filename(default=THEME_CSS / 'dark.css') bokeh_theme = param.ClassSelector(class_=(_BkTheme, str), default=_BkTheme(json=BOKEH_DARK)) _name: ClassVar[str] = 'dark'
[docs]class Design(param.Parameterized, ResourceComponent): theme = param.ClassSelector(class_=Theme, constant=True) # Defines parameter overrides to apply to each model modifiers: ClassVar[Dict[Viewable, Dict[str, Any]]] = {} # Defines the resources required to render this theme _resources: ClassVar[Dict[str, Dict[str, str]]] = {} # Declares valid themes for this Design _themes: ClassVar[Dict[str, Type[Theme]]] = { 'default': DefaultTheme, 'dark': DarkTheme } def __init__(self, theme=None, **params): if isinstance(theme, type) and issubclass(theme, Theme): theme = theme._name elif theme is None: theme = 'default' theme = self._themes[theme]() super().__init__(theme=theme, **params) def _reapply( self, viewable: Viewable, root: Model, old_models: List[Model] = None, isolated: bool=True, cache=None, document=None ) -> None: ref = root.ref['id'] for o in viewable.select(): if o.design and not isolated: continue elif not o.design and not isolated: o._design = self if old_models and ref in o._models: if o._models[ref][0] in old_models: continue self._apply_modifiers(o, ref, self.theme, isolated, cache, document) def _apply_hooks(self, viewable: Viewable, root: Model, changed: Viewable, old_models=None) -> None: from ..io.state import state if root.document in state._stylesheets: cache = state._stylesheets[root.document] else: state._stylesheets[root.document] = cache = {} with root.document.models.freeze(): self._reapply(changed, root, old_models, isolated=False, cache=cache, document=root.document) def _wrapper(self, viewable): return viewable @classmethod def _resolve_stylesheets(cls, value, defining_cls, inherited): from ..io.resources import resolve_stylesheet stylesheets = [] for stylesheet in value: if stylesheet is Inherit: stylesheets.extend(inherited) continue resolved = resolve_stylesheet(defining_cls, stylesheet, 'modifiers') stylesheets.append(resolved) return stylesheets @classmethod @functools.lru_cache def _resolve_modifiers(cls, vtype, theme): """ Iterate over the class hierarchy in reverse order and accumulate all modifiers that apply to the objects class and its super classes. """ modifiers, child_modifiers = {}, {} for scls in vtype.__mro__[::-1]: cls_modifiers = cls.modifiers.get(scls, {}) modifiers.update(theme.modifiers.get(scls, {})) for super_cls in cls.__mro__[::-1]: cls_modifiers = getattr(super_cls, 'modifiers', {}).get(scls, {}) for prop, value in cls_modifiers.items(): if prop == 'children': continue elif prop == 'stylesheets': modifiers[prop] = cls._resolve_stylesheets(value, super_cls, modifiers.get(prop, [])) else: modifiers[prop] = value child_modifiers.update(cls_modifiers.get('children', {})) return modifiers, child_modifiers @classmethod def _get_modifiers( cls, viewable: Viewable, theme: Theme = None, isolated: bool = True ): from ..io.resources import ( CDN_DIST, component_resource_path, resolve_custom_path, ) modifiers, child_modifiers = cls._resolve_modifiers(type(viewable), theme) modifiers = dict(modifiers) if 'stylesheets' in modifiers: if isolated: pre = list(cls._resources.get('css', {}).values()) for p in ('base_css', 'css'): css = getattr(theme, p) if css is None: continue css = pathlib.Path(css) if relative_to(css, THEME_CSS): pre.append(f'{CDN_DIST}bundled/theme/{css.name}') elif resolve_custom_path(theme, css): pre.append(component_resource_path(theme, p, css)) else: pre.append(css.read_text(encoding='utf-8')) else: pre = [] modifiers['stylesheets'] = pre + modifiers['stylesheets'] return modifiers, child_modifiers @classmethod def _patch_modifiers(cls, doc, modifiers, cache): if 'stylesheets' in modifiers: stylesheets = [] for sts in modifiers['stylesheets']: if sts.endswith('.css'): if cache and sts in cache: sts = cache[sts] else: sts = ImportedStyleSheet(url=sts) if cache is not None: cache[sts.url] = sts stylesheets.append(sts) modifiers['stylesheets'] = stylesheets @classmethod def _apply_modifiers( cls, viewable: Viewable, mref: str, theme: Theme, isolated: bool, cache={}, document=None ) -> None: if mref not in viewable._models: return model, _ = viewable._models[mref] modifiers, child_modifiers = cls._get_modifiers(viewable, theme, isolated) cls._patch_modifiers(model.document or document, modifiers, cache) if child_modifiers: for child in viewable: cls._apply_params(child, mref, child_modifiers, document) if modifiers: cls._apply_params(viewable, mref, modifiers, document) @classmethod def _apply_params(cls, viewable, mref, modifiers, document=None): # Apply params never sync the modifier values with the Viewable # This should not be a concern since most `Layoutable` properties, # e.g. stylesheets or sizing_mode, are not synced between the # Panel component and the model anyway however in certain edge cases # this may end up causing issues. from ..io.resources import CDN_DIST, patch_stylesheet model, _ = viewable._models[mref] params = { k: v for k, v in modifiers.items() if k != 'children' and getattr(viewable, k) == viewable.param[k].default } if 'stylesheets' in modifiers: params['stylesheets'] = modifiers['stylesheets'] + viewable.stylesheets props = viewable._process_param_change(params) doc = model.document or document if doc and 'dist_url' in doc._template_variables: dist_url = doc._template_variables['dist_url'] else: dist_url = CDN_DIST for stylesheet in props.get('stylesheets', []): if isinstance(stylesheet, ImportedStyleSheet): patch_stylesheet(stylesheet, dist_url) # Do not update stylesheets if they match if 'stylesheets' in props and len(model.stylesheets) == len(props['stylesheets']): all_match = True stylesheets = [] for st1, st2 in zip(model.stylesheets, props['stylesheets']): if st1 == st2: stylesheets.append(st1) continue elif type(st1) is type(st2) and isinstance(st1, ImportedStyleSheet) and st1.url == st2.url: stylesheets.append(st1) continue stylesheets.append(st2) all_match = False if all_match: del props['stylesheets'] else: props['stylesheets'] = stylesheets model.update(**props) if hasattr(viewable, '_synced_properties') and 'objects' in viewable._property_mapping: obj_key = viewable._property_mapping['objects'] child_props = { p: v for p, v in params.items() if p in viewable._synced_properties } for child in getattr(model, obj_key): child.update(**child_props) #---------------------------------------------------------------- # Public API #----------------------------------------------------------------
[docs] def apply(self, viewable: Viewable, root: Model, isolated: bool=True): """ Applies the Design to a Viewable and all it children. Arguments --------- viewable: Viewable The Viewable to apply the Design to. root: Model The root Bokeh model to apply the Design to. isolated: bool Whether the Design is applied to an individual component or embedded in a template that ensures the resources, such as CSS variable definitions and JS are already initialized. """ doc = root.document if not doc: self._reapply(viewable, root, isolated=isolated) return from ..io.state import state if doc in state._stylesheets: cache = state._stylesheets[doc] else: state._stylesheets[doc] = cache = {} with doc.models.freeze(): self._reapply(viewable, root, isolated=isolated, cache=cache) if self.theme and self.theme.bokeh_theme and doc: doc.theme = self.theme.bokeh_theme
[docs] def apply_bokeh_theme_to_model(self, model: Model, theme_override=None): """ Applies the Bokeh theme associated with this Design system to a model. Arguments --------- model: bokeh.model.Model The Model to apply the theme on. theme_override: str | None A different theme to apply. """ theme = theme_override or self.theme.bokeh_theme if isinstance(theme, str): theme = built_in_themes.get(theme) if not theme: return for sm in model.references(): theme.apply_to_model(sm)
[docs] def resolve_resources( self, cdn: bool | Literal['auto'] = 'auto', include_theme: bool = True ) -> ResourceTypes: """ Resolves the resources required for this design component. Arguments --------- cdn: bool | Literal['auto'] Whether to load resources from CDN or local server. If set to 'auto' value will be automatically determine based on global settings. include_theme: bool Whether to include theme resources. Returns ------- Dictionary containing JS and CSS resources. """ resource_types = super().resolve_resources(cdn) if not include_theme: return resource_types dist_path = get_dist_path(cdn=cdn) css_files = resource_types['css'] theme = self.theme for attr in ('base_css', 'css'): css = getattr(theme, attr, None) if css is None: continue basename = os.path.basename(css) key = 'theme_base' if 'base' in attr else 'theme' if relative_to(css, THEME_CSS): css_files[key] = dist_path + f'bundled/theme/{basename}' elif resolve_custom_path(theme, css): owner = type(theme).param[attr].owner css_files[key] = component_resource_path(owner, attr, css) return resource_types
[docs] def params( self, viewable: Viewable, doc: Document | None = None ) -> Tuple[Dict[str, Any], Dict[str, Any]]: """ Provides parameter values to apply the provided Viewable. Arguments --------- viewable: Viewable The Viewable to return modifiers for. doc: Document | None Document the Viewable will be rendered into. Useful for caching any stylesheets that are created. Returns ------- modifiers: Dict[str, Any] Dictionary of parameter values to apply to the Viewable. child_modifiers: Dict[str, Any] Dictionary of parameter values to apply to the children of the Viewable. """ from ..io.state import state if doc is None: cache = {} elif doc in state._stylesheets: cache = state._stylesheets[doc] else: state._stylesheets[doc] = cache = {} modifiers, child_modifiers = self._get_modifiers(viewable, theme=self.theme) self._patch_modifiers(doc, modifiers, cache) return modifiers, child_modifiers
config.param.design.class_ = Design THEMES = { 'default': DefaultTheme, 'dark': DarkTheme }