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

Speed up Pane.clone with 50% #5848

Merged
merged 7 commits into from
Nov 15, 2023
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
8 changes: 2 additions & 6 deletions panel/pane/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
from ..reactive import Reactive
from ..util import param_reprs, param_watchers
from ..util.checks import is_dataframe, is_series
from ..util.parameters import get_params_to_inherit
from ..viewable import (
Layoutable, ServableMixin, Viewable, Viewer,
)
Expand All @@ -40,7 +41,6 @@
from bokeh.model import Model
from pyviz_comms import Comm


def panel(obj: Any, **kwargs) -> Viewable:
"""
Creates a displayable Panel object given any valid Python object.
Expand Down Expand Up @@ -382,11 +382,7 @@ def clone(self: T, object: Optional[Any] = None, **params) -> T:
-------
Cloned Pane object
"""
inherited = {
p: v for p, v in self.param.values().items()
if not self.param[p].readonly and v is not self.param[p].default
and not (v is None and not self.param[p].allow_None)
}
inherited = get_params_to_inherit(self)
params = dict(inherited, **params)
old_object = params.pop('object', None)
if object is None:
Expand Down
7 changes: 7 additions & 0 deletions panel/tests/pane/test_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,13 @@ def test_pane_clone(pane):
assert ([(k, v) for k, v in sorted(p.param.values().items()) if k not in ('name', '_pane')] ==
[(k, v) for k, v in sorted(clone.param.values().items()) if k not in ('name', '_pane')])

def test_pane_with_non_defaults_clone():
p = Markdown("Hello World", sizing_mode="stretch_width")
clone = p.clone()

assert ([(k, v) for k, v in sorted(p.param.values().items()) if k not in ('name', '_pane')] ==
[(k, v) for k, v in sorted(clone.param.values().items()) if k not in ('name', '_pane')])


@pytest.mark.parametrize('pane', all_panes)
def test_pane_signature(pane):
Expand Down
15 changes: 15 additions & 0 deletions panel/tests/test_viewable.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,3 +100,18 @@ def __panel__(self):
return 42

panel(Example())

@pytest.mark.parametrize('viewable', all_viewables)
def test_clone(viewable):
v = Viewable()
clone = v.clone()

assert ([(k, v) for k, v in sorted(v.param.values().items()) if k not in ('name')] ==
[(k, v) for k, v in sorted(clone.param.values().items()) if k not in ('name')])

def test_clone_with_non_defaults():
v= Viewable(loading=True)
clone = v.clone()

assert ([(k, v) for k, v in sorted(v.param.values().items()) if k not in ('name')] ==
[(k, v) for k, v in sorted(clone.param.values().items()) if k not in ('name')])
94 changes: 5 additions & 89 deletions panel/util/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
import ast
import base64
import datetime as dt
import inspect
import json
import logging
import numbers
Expand All @@ -18,12 +17,11 @@

from collections import OrderedDict, defaultdict
from collections.abc import MutableMapping, MutableSequence
from contextlib import contextmanager
from datetime import datetime
from functools import partial
from html import escape # noqa
from importlib import import_module
from typing import Any, AnyStr, Iterator
from typing import Any, AnyStr

import bleach
import bokeh
Expand All @@ -38,6 +36,10 @@
datetime_types, is_dataframe, is_holoviews, is_number, is_parameterized,
is_series, isdatetime, isfile, isIn, isurl,
)
from .parameters import ( # noqa
edit_readonly, extract_dependencies, get_method_owner, param_watchers,
recursive_parameterized,
)

log = logging.getLogger('panel.util')

Expand Down Expand Up @@ -85,17 +87,6 @@ def param_name(name: str) -> str:
return name[:name.index(match[0])] if match else name


def recursive_parameterized(parameterized: param.Parameterized, objects=None) -> list[param.Parameterized]:
"""
Recursively searches a Parameterized object for other Parmeterized
objects.
"""
objects = [] if objects is None else objects
objects.append(parameterized)
for p in parameterized.param.values().values():
if isinstance(p, param.Parameterized) and not any(p is o for o in objects):
recursive_parameterized(p, objects)
return objects


def abbreviated_repr(value, max_length=25, natural_breaks=(',', ' ')):
Expand Down Expand Up @@ -174,42 +165,6 @@ def full_groupby(l, key=lambda x: x):
return d.items()


def get_method_owner(meth):
"""
Returns the instance owning the supplied instancemethod or
the class owning the supplied classmethod.
"""
if inspect.ismethod(meth):
return meth.__self__


def extract_dependencies(function):
"""
Extract references from a method or function that declares the references.
"""
subparameters = list(function._dinfo['dependencies'])+list(function._dinfo['kw'].values())
params = []
for p in subparameters:
if isinstance(p, str):
owner = get_method_owner(function)
*subps, p = p.split('.')
for subp in subps:
owner = getattr(owner, subp, None)
if owner is None:
raise ValueError('Cannot depend on undefined sub-parameter {p!r}.')
if p in owner.param:
pobj = owner.param[p]
if pobj not in params:
params.append(pobj)
else:
for sp in extract_dependencies(getattr(owner, p)):
if sp not in params:
params.append(sp)
elif p not in params:
params.append(p)
return params


def value_as_datetime(value):
"""
Retrieve the value tuple as a tuple of datetime objects.
Expand Down Expand Up @@ -313,31 +268,6 @@ def url_path(url: str) -> str:
return '/'.join('/'.join(subpaths).split('/')[1:])


# This functionality should be contributed to param
# See https://github.com/holoviz/param/issues/379
@contextmanager
def edit_readonly(parameterized: param.Parameterized) -> Iterator:
"""
Temporarily set parameters on Parameterized object to readonly=False
to allow editing them.
"""
params = parameterized.param.objects("existing").values()
readonlys = [p.readonly for p in params]
constants = [p.constant for p in params]
for p in params:
p.readonly = False
p.constant = False
try:
yield
except Exception:
raise
finally:
for (p, readonly) in zip(params, readonlys):
p.readonly = readonly
for (p, constant) in zip(params, constants):
p.constant = constant


def lazy_load(module, model, notebook=False, root=None, ext=None):
from ..config import panel_extension as extension
from ..io.state import state
Expand Down Expand Up @@ -471,20 +401,6 @@ def relative_to(path, other_path):
except Exception:
return False

_unset = object()

def param_watchers(parameterized, value=_unset):
if Version(param.__version__) <= Version('2.0.0a2'):
if value is not _unset:
parameterized._param_watchers = value
else:
return parameterized._param_watchers
else:
if value is not _unset:
parameterized.param.watchers = value
else:
return parameterized.param.watchers


def flatten(line):
"""
Expand Down
111 changes: 111 additions & 0 deletions panel/util/parameters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
from __future__ import annotations

import inspect

from contextlib import contextmanager
from typing import Any, Dict, Iterator

import param

from packaging.version import Version

_unset = object()


def should_inherit(parameterized: param.Parameterized, p: str, v: Any) -> Any:
pobj = parameterized.param[p]
return v is not pobj.default and not pobj.readonly and (v is not None or pobj.allow_None)


def get_params_to_inherit(parameterized: param.Parameterized) -> Dict:
return {
p: v for p, v in parameterized.param.values().items()
if should_inherit(parameterized, p, v)
}


def get_method_owner(meth):
"""
Returns the instance owning the supplied instancemethod or
the class owning the supplied classmethod.
"""
if inspect.ismethod(meth):
return meth.__self__


# This functionality should be contributed to param
# See https://github.com/holoviz/param/issues/379
@contextmanager
def edit_readonly(parameterized: param.Parameterized) -> Iterator:
"""
Temporarily set parameters on Parameterized object to readonly=False
to allow editing them.
"""
params = parameterized.param.objects("existing").values()
readonlys = [p.readonly for p in params]
constants = [p.constant for p in params]
for p in params:
p.readonly = False
p.constant = False
try:
yield
except Exception:
raise
finally:
for (p, readonly) in zip(params, readonlys):
p.readonly = readonly
for (p, constant) in zip(params, constants):
p.constant = constant


def extract_dependencies(function):
"""
Extract references from a method or function that declares the references.
"""
subparameters = list(function._dinfo['dependencies'])+list(function._dinfo['kw'].values())
params = []
for p in subparameters:
if isinstance(p, str):
owner = get_method_owner(function)
*subps, p = p.split('.')
for subp in subps:
owner = getattr(owner, subp, None)
if owner is None:
raise ValueError('Cannot depend on undefined sub-parameter {p!r}.')
if p in owner.param:
pobj = owner.param[p]
if pobj not in params:
params.append(pobj)
else:
for sp in extract_dependencies(getattr(owner, p)):
if sp not in params:
params.append(sp)
elif p not in params:
params.append(p)
return params


def param_watchers(parameterized: param.Parameterized, value=_unset):
if Version(param.__version__) <= Version('2.0.0a2'):
if value is not _unset:
parameterized._param_watchers = value
else:
return parameterized._param_watchers
else:
if value is not _unset:
parameterized.param.watchers = value
else:
return parameterized.param.watchers


def recursive_parameterized(parameterized: param.Parameterized, objects=None) -> list[param.Parameterized]:
"""
Recursively searches a Parameterized object for other Parmeterized
objects.
"""
objects = [] if objects is None else objects
objects.append(parameterized)
for p in parameterized.param.values().values():
if isinstance(p, param.Parameterized) and not any(p is o for o in objects):
recursive_parameterized(p, objects)
return objects
8 changes: 2 additions & 6 deletions panel/viewable.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
from .io.save import save
from .io.state import curdoc_locked, set_curdoc, state
from .util import escape, param_reprs
from .util.parameters import get_params_to_inherit
from .util.warnings import deprecated

if TYPE_CHECKING:
Expand Down Expand Up @@ -672,7 +673,6 @@ def get_root(
state._views[ref] = (root_view, root, doc, comm)
return root


class Viewable(Renderable, Layoutable, ServableMixin):
"""
Viewable is the baseclass all visual components in the panel
Expand Down Expand Up @@ -851,11 +851,7 @@ def clone(self, **params) -> 'Viewable':
-------
Cloned Viewable object
"""
inherited = {
p: v for p, v in self.param.values().items()
if not self.param[p].readonly and v is not self.param[p].default
and not (v is None and not self.param[p].allow_None)
}
inherited = get_params_to_inherit(self)
return type(self)(**dict(inherited, **params))

def pprint(self) -> None:
Expand Down
Loading