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

Various improvements for writing ESM components #7462

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
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
52 changes: 34 additions & 18 deletions panel/custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ def __init__(mcs, name: str, bases: tuple[type, ...], dict_: Mapping[str, Any]):
model_name = f'{name}{ReactiveMetaBase._name_counter[name]}'
ignored = [p for p in Reactive.param if not issubclass(type(mcs.param[p].owner), ReactiveESMMetaclass)]
mcs._data_model = construct_data_model(
mcs, name=model_name, ignore=ignored
mcs, name=model_name, ignore=ignored, extras={'esm_constants': param.Dict}
)


Expand Down Expand Up @@ -216,6 +216,8 @@ class CounterButton(pn.custom.ReactiveESM):

_bundle: ClassVar[str | os.PathLike | None] = None

_constants: ClassVar[dict[str, Any]] = {}

_esm: ClassVar[str | os.PathLike] = ""

# Specifies exports to make available to JS in a bundled file
Expand All @@ -235,15 +237,25 @@ def __init__(self, **params):
self._msg__callbacks = []

@classproperty
def _bundle_path(cls) -> os.PathLike | None:
if config.autoreload and cls._esm:
return
def _module_path(cls):
try:
mod_path = pathlib.Path(inspect.getfile(cls)).parent
return pathlib.Path(inspect.getfile(cls)).parent
except (OSError, TypeError, ValueError):
if not isinstance(cls._bundle, pathlib.PurePath):
return

@classproperty
def _bundle_path(cls) -> os.PathLike | None:
if config.autoreload and cls._esm:
return
mod_path = cls._module_path
if mod_path is None:
return
if cls._bundle:
for scls in cls.__mro__:
if issubclass(scls, ReactiveESM) and cls._bundle == scls._bundle:
cls = scls
mod_path = cls._module_path
bundle = cls._bundle
if isinstance(bundle, pathlib.PurePath):
return bundle
Expand Down Expand Up @@ -300,7 +312,7 @@ def _render_esm(cls, compiled: bool | Literal['compiling'] = True, server: bool
if esm_path:
if esm_path == cls._bundle_path and cls.__module__ in sys.modules and server:
base_cls = cls
for scls in cls.__mro__[1:][::-1]:
for scls in cls.__mro__[1:]:
if not issubclass(scls, ReactiveESM):
continue
if esm_path == scls._esm_path(compiled=compiled is True):
Expand Down Expand Up @@ -391,6 +403,7 @@ def _get_properties(self, doc: Document) -> dict[str, Any]:
else:
bundle_hash = None
data_props = self._process_param_change(data_params)
data_props['esm_constants'] = self._constants
props.update({
'bundle': bundle_hash,
'class_name': camel_to_kebab(cls.__name__),
Expand All @@ -406,24 +419,26 @@ def _get_properties(self, doc: Document) -> dict[str, Any]:
def _process_importmap(cls):
return cls._importmap

def _get_child_model(self, child, doc, root, parent, comm):
if child is None:
return None
ref = root.ref['id']
if isinstance(child, list):
return [
sv._models[ref][0] if ref in sv._models else sv._get_model(doc, root, parent, comm)
for sv in child
]
elif ref in child._models:
return child._models[ref][0]
return child._get_model(doc, root, parent, comm)

def _get_children(self, data_model, doc, root, parent, comm):
children = {}
ref = root.ref['id']
for k, v in self.param.values().items():
p = self.param[k]
if not is_viewable_param(p):
continue
if v is None:
children[k] = None
elif isinstance(v, list):
children[k] = [
sv._models[ref][0] if ref in sv._models else sv._get_model(doc, root, parent, comm)
for sv in v
]
elif ref in v._models:
children[k] = v._models[ref][0]
else:
children[k] = v._get_model(doc, root, parent, comm)
children[k] = self._get_child_model(v, doc, root, parent, comm)
return children

def _setup_autoreload(self):
Expand Down Expand Up @@ -673,6 +688,7 @@ def _process_importmap(cls):
"react-is": f"https://esm.sh/react-is@{v_react}&external=react",
"@emotion/cache": f"https://esm.sh/@emotion/cache?deps=react@{v_react},react-dom@{v_react}",
"@emotion/react": f"https://esm.sh/@emotion/react?deps=react@{v_react},react-dom@{v_react}&external=react,react-is",
"@emotion/styled": f"https://esm.sh/@emotion/styled?deps=react@{v_react},react-dom@{v_react}&external=react,react-is",
})
for k, v in imports.items():
if '?' not in v and 'esm.sh' in v:
Expand Down
8 changes: 7 additions & 1 deletion panel/io/datamodel.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ def bytes_param(p, kwargs):
}


def construct_data_model(parameterized, name=None, ignore=[], types={}):
def construct_data_model(parameterized, name=None, ignore=[], types={}, extras={}):
"""
Dynamically creates a Bokeh DataModel class from a Parameterized
object.
Expand All @@ -132,6 +132,8 @@ def construct_data_model(parameterized, name=None, ignore=[], types={}):
types: dict
A dictionary mapping from parameter name to a Parameter type,
making it possible to override the default parameter types.
extras: dict
Additional properties to define on the DataModel.

Returns
-------
Expand Down Expand Up @@ -163,6 +165,10 @@ def construct_data_model(parameterized, name=None, ignore=[], types={}):
for bkp, convert in accepts:
bk_prop = bk_prop.accepts(bkp, convert)
properties[pname] = bk_prop
for pname, ptype in extras.items():
if issubclass(ptype, pm.Parameter):
ptype = PARAM_MAPPING.get(ptype)(None, {})
properties[pname] = ptype
name = name or parameterized.name
return type(name, (DataModel,), properties)

Expand Down
3 changes: 2 additions & 1 deletion panel/layout/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -564,8 +564,9 @@ def __init__(self, *items: list[Any | tuple[str, Any]], **params: Any):
'as positional arguments or as a keyword, not both.'
)
items = params.pop('objects')
params['objects'], self._names = self._to_objects_and_names(items)
params['objects'], names = self._to_objects_and_names(items)
super().__init__(**params)
self._names = names
self._panels = defaultdict(dict)
self.param.watch(self._update_names, 'objects')
# ALERT: Ensure that name update happens first, should be
Expand Down
2 changes: 1 addition & 1 deletion panel/reactive.py
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ def _process_param_change(self, msg: dict[str, Any]) -> dict[str, Any]:
stylesheets += properties['stylesheets']
wrapped = []
for stylesheet in stylesheets:
if isinstance(stylesheet, str) and stylesheet.split('?')[0].endswith('.css'):
if isinstance(stylesheet, str) and (stylesheet.split('?')[0].endswith('.css') or stylesheet.startswith('http')):
cache = (state._stylesheets if state.curdoc else {}).get(state.curdoc, {})
if stylesheet in cache:
stylesheet = cache[stylesheet]
Expand Down
Loading