diff --git a/doc/how_to/interact/interact_abbreviations.md b/doc/how_to/interact/interact_abbreviations.md index b3fa26555d5..e16012d72fe 100644 --- a/doc/how_to/interact/interact_abbreviations.md +++ b/doc/how_to/interact/interact_abbreviations.md @@ -14,7 +14,6 @@ To use `interact`, you need to define a function that you want to explore. Here ```{pyodide} import panel as pn -from panel.interact import interact from panel import widgets pn.extension() # for notebook @@ -26,7 +25,7 @@ def f(x): When you pass this function to `interact` along with `x=10`, a slider is generated and bound to the function parameter, such that when you interact with the widget, the function is called. ```{pyodide} -interact(f, x=10) +pn.interact(f, x=10) ``` When you pass an integer-valued keyword argument of `10` (`x=10`) to `interact`, it generates an integer-valued slider control with a range of `[-10,+3*10]`. In this case, `10` is an *abbreviation* for an actual slider widget: @@ -38,7 +37,7 @@ slider_widget = widgets.IntSlider(start=-10,end=30,step=1,value=10) In fact, we can get the same result if we pass this `IntSlider` as the keyword argument for `x`: ```{pyodide} -interact(f, x=slider_widget) +pn.interact(f, x=slider_widget) ``` This examples clarifies how `interact` processes its keyword arguments: @@ -64,35 +63,35 @@ If a 2-tuple of integers is passed `(min,max)`, an integer-valued slider is prod ```{pyodide} -interact(f, x=(0, 4)) +pn.interact(f, x=(0, 4)) ``` If a 3-tuple of integers is passed `(min,max,step)`, the step size can also be set. ```{pyodide} -interact(f, x=(0, 8, 2)) +pn.interact(f, x=(0, 8, 2)) ``` A float-valued slider is produced if the elements of the tuples are floats. Here the minimum is `0.0`, the maximum is `10.0` and step size is `0.1` (the default). ```{pyodide} -interact(f, x=(0.0, 10.0)) +pn.interact(f, x=(0.0, 10.0)) ``` The step size can be changed by passing a third element in the tuple. ```{pyodide} -interact(f, x=(0.0, 10.0, 0.01)) +pn.interact(f, x=(0.0, 10.0, 0.01)) ``` For both integer and float-valued sliders, you can pick the initial value of the widget by supplying a default keyword argument when you define the underlying Python function. Here we set the initial value of a float slider to `5.5`. ```{pyodide} -@interact(x=(0.0, 20.0, 0.5)) +@pn.interact(x=(0.0, 20.0, 0.5)) def h(x=5.5): return x @@ -103,28 +102,28 @@ You can also set the initial value by passing a fourth element in the tuple. ```{pyodide} -interact(f, x=(0.0, 20.0, 0.5, 5.5)) +pn.interact(f, x=(0.0, 20.0, 0.5, 5.5)) ``` Use `None` as the third element to just set min, max, and value. ```{pyodide} -interact(f, x=(0.0, 20.0, None, 5.5)) +pn.interact(f, x=(0.0, 20.0, None, 5.5)) ``` Dropdown menus are constructed by passing a list of strings. In this case, the strings are both used as the names in the dropdown menu UI and passed to the underlying Python function. ```{pyodide} -interact(f, x=['apples', 'oranges']) +pn.interact(f, x=['apples', 'oranges']) ``` When working with numeric data ``interact`` will automatically add a discrete slider: ```{pyodide} -interact(f, x=dict([('one', 10), ('two', 20)])) +pn.interact(f, x=dict([('one', 10), ('two', 20)])) ``` ## Related Resources diff --git a/doc/how_to/interact/interact_fix_values.md b/doc/how_to/interact/interact_fix_values.md index 6397068935b..a8e9aa7c7f7 100644 --- a/doc/how_to/interact/interact_fix_values.md +++ b/doc/how_to/interact/interact_fix_values.md @@ -12,14 +12,14 @@ First, let's declare a simple function. ```{pyodide} import panel as pn -from panel.interact import fixed +from panel._interact import fixed pn.extension() # for notebook def f(x, y): return x, y ``` -Now, call `interact` using the `panel.interact.fixed` function to fix one of the values: +Now, call `interact` using the `panel._interact.fixed` function to fix one of the values: ```{pyodide} pn.interact(f, x=1, y=fixed(10)) diff --git a/panel/__init__.py b/panel/__init__.py index 1fbfe91a1a3..15a264faf7b 100644 --- a/panel/__init__.py +++ b/panel/__init__.py @@ -58,7 +58,7 @@ from . import ( layout, links, pane, param, pipeline, template, viewable, widgets, ) - from .interact import interact + from ._interact import interact from .io import serve, state from .io.cache import cache from .io.notebook import ( # noqa: F401 @@ -102,7 +102,7 @@ "config": "panel.config:config", "custom": "panel.custom", "indicators": "panel.widgets:indicators", - "interact": "panel.interact:interact", + "interact": "panel._interact:interact", "ipywidget": "panel.io.notebook:ipywidget", "layout": "panel.layout", "links": "panel.links", diff --git a/panel/interact.py b/panel/interact.py deleted file mode 100644 index 810824d3874..00000000000 --- a/panel/interact.py +++ /dev/null @@ -1,375 +0,0 @@ -""" -Interact with functions using widgets. - -The interact Pane implemented in this module mirrors -ipywidgets.interact in its API and implementation. Large parts of the -code were copied directly from ipywidgets: - -Copyright (c) Jupyter Development Team and PyViz Development Team. -Distributed under the terms of the Modified BSD License. -""" -from __future__ import annotations - -import types - -from collections.abc import Iterable, Mapping -from inspect import ( - Parameter, getcallargs, getfullargspec as check_argspec, signature, -) -from typing import TYPE_CHECKING, ClassVar - -import param - -from .layout import Column, Panel, Row -from .pane import HTML, Pane, panel -from .pane.base import ReplacementPane -from .viewable import Viewable -from .widgets import Button, WidgetBase -from .widgets.widget import fixed, widget - -if TYPE_CHECKING: - from bokeh.model import Model - -empty = Parameter.empty - - -def _yield_abbreviations_for_parameter(parameter, kwargs): - """Get an abbreviation for a function parameter.""" - name = parameter.name - kind = parameter.kind - ann = parameter.annotation - default = parameter.default - not_found = (name, empty, empty) - if kind in (Parameter.POSITIONAL_OR_KEYWORD, Parameter.KEYWORD_ONLY): - if name in kwargs: - value = kwargs.pop(name) - elif ann is not empty: - param.main.param.warning("Using function annotations to implicitly specify interactive controls is deprecated. " - "Use an explicit keyword argument for the parameter instead.", DeprecationWarning) - value = ann - elif default is not empty: - value = default - if isinstance(value, (Iterable, Mapping)): - value = fixed(value) - else: - yield not_found - yield (name, value, default) - elif kind == Parameter.VAR_KEYWORD: - # In this case name=kwargs and we yield the items in kwargs with their keys. - for k, v in kwargs.copy().items(): - kwargs.pop(k) - yield k, v, empty - - -class interactive(Pane): - - default_layout = param.ClassSelector(default=Column, class_=(Panel), - is_instance=False) - - manual_update = param.Boolean(default=False, doc=""" - Whether to update manually by clicking on button.""") - - manual_name = param.String(default='Run Interact') - - _pane = param.ClassSelector(class_=Viewable) - - _rename: ClassVar[Mapping[str, str | None]] = {'_pane': None} - - def __init__(self, object, params={}, **kwargs): - if signature is None: - raise ImportError('interact requires either recent Python version ' - '(>=3.3 or IPython to inspect function signatures.') - - super().__init__(object, **params) - - self.throttled = kwargs.pop('throttled', False) - new_kwargs = self.find_abbreviations(kwargs) - # Before we proceed, let's make sure that the user has passed a set of args+kwargs - # that will lead to a valid call of the function. This protects against unspecified - # and doubly-specified arguments. - try: - check_argspec(object) - except TypeError: - # if we can't inspect, we can't validate - pass - else: - getcallargs(object, **{n:v for n,v,_ in new_kwargs}) - - widgets = self.widgets_from_abbreviations(new_kwargs) - if self.manual_update: - widgets.append(('manual', Button(name=self.manual_name))) - self._widgets = dict(widgets) - pane = self.object(**self.kwargs) - if isinstance(pane, Viewable): - self._pane = pane - self._internal = False - else: - self._pane = panel(pane, name=self.name) - self._internal = True - self._inner_layout = Row(self._pane) - widgets = [widget for _, widget in widgets if isinstance(widget, WidgetBase)] - if 'name' in params: - widgets.insert(0, HTML(f'

{self.name}

')) - self.widget_box = Column(*widgets) - self.layout.objects = [self.widget_box, self._inner_layout] - self._link_widgets() - self._sync_layout() - - #---------------------------------------------------------------- - # Model API - #---------------------------------------------------------------- - - def _get_model(self, doc, root=None, parent=None, comm=None): - return self._inner_layout._get_model(doc, root, parent, comm) - - @param.depends('_pane', '_pane.sizing_mode', '_pane.width_policy', '_pane.height_policy', watch=True) - def _sync_layout(self): - if not hasattr(self, '_inner_layout'): - return - opts = { - k: v for k, v in self._pane.param.values().items() - if k in ('sizing_mode', 'width_policy', 'height_policy') - } - self._inner_layout.param.update(opts) - self.layout.param.update(opts) - - #---------------------------------------------------------------- - # Callback API - #---------------------------------------------------------------- - - @property - def _synced_params(self): - return [] - - def _link_widgets(self): - if self.manual_update: - widgets = [('manual', self._widgets['manual'])] - else: - widgets = self._widgets.items() - - for name, widget_obj in widgets: - def update_pane(change): - # Try updating existing pane - new_object = self.object(**self.kwargs) - new_pane, internal = ReplacementPane._update_from_object( - new_object, self._pane, self._internal - ) - if new_pane is None: - return - - # Replace pane entirely - self._pane = new_pane - self._inner_layout[0] = new_pane - self._internal = internal - - if self.throttled and hasattr(widget_obj, 'value_throttled'): - v = 'value_throttled' - else: - v = 'value' - - pname = 'clicks' if name == 'manual' else v - watcher = widget_obj.param.watch(update_pane, pname) - self._internal_callbacks.append(watcher) - - def _cleanup(self, root: Model | None = None) -> None: - self._inner_layout._cleanup(root) - super()._cleanup(root) - - #---------------------------------------------------------------- - # Public API - #---------------------------------------------------------------- - - @property - def kwargs(self): - return {k: widget.value for k, widget in self._widgets.items() - if k != 'manual'} - - def signature(self): - return signature(self.object) - - def find_abbreviations(self, kwargs): - """Find the abbreviations for the given function and kwargs. - Return (name, abbrev, default) tuples. - """ - new_kwargs = [] - try: - sig = self.signature() - except (ValueError, TypeError): - # can't inspect, no info from function; only use kwargs - return [ (key, value, value) for key, value in kwargs.items() ] - - for parameter in sig.parameters.values(): - for name, value, default in _yield_abbreviations_for_parameter(parameter, kwargs): - if value is empty: - raise ValueError(f'cannot find widget or abbreviation for argument: {name!r}') - new_kwargs.append((name, value, default)) - return new_kwargs - - def widgets_from_abbreviations(self, seq): - """Given a sequence of (name, abbrev, default) tuples, return a sequence of Widgets.""" - result = [] - for name, abbrev, default in seq: - if isinstance(abbrev, fixed): - widget_obj = abbrev - else: - widget_obj = widget(abbrev, name=name, default=default) - if not (isinstance(widget_obj, WidgetBase) or isinstance(widget_obj, fixed)): - if widget_obj is None: - continue - else: - raise TypeError(f"{widget!r} is not a ValueWidget") - result.append((name, widget_obj)) - return result - - @classmethod - def applies(cls, object): - return isinstance(object, types.FunctionType) - - # Return a factory for interactive functions - @classmethod - def factory(cls): - options = dict(manual_update=False, manual_name="Run Interact") - return _InteractFactory(cls, options) - - -class _InteractFactory: - """ - Factory for instances of :class:`interactive`. - - Arguments - --------- - cls: class - The subclass of :class:`interactive` to construct. - options: dict - A dict of options used to construct the interactive - function. By default, this is returned by - ``cls.default_options()``. - kwargs: dict - A dict of **kwargs to use for widgets. - """ - def __init__(self, cls, options, kwargs=None): - self.cls = cls - self.opts = options - self.kwargs = kwargs or {} - - def widget(self, f): - """ - Return an interactive function widget for the given function. - The widget is only constructed, not displayed nor attached to - the function. - Returns - ------- - An instance of ``self.cls`` (typically :class:`interactive`). - Parameters - ---------- - f : function - The function to which the interactive widgets are tied. - """ - return self.cls(f, self.opts, **self.kwargs) - - def __call__(self, __interact_f=None, **kwargs): - """ - Make the given function interactive by adding and displaying - the corresponding :class:`interactive` widget. - Expects the first argument to be a function. Parameters to this - function are widget abbreviations passed in as keyword arguments - (``**kwargs``). Can be used as a decorator (see examples). - Returns - ------- - f : __interact_f with interactive widget attached to it. - Parameters - ---------- - __interact_f : function - The function to which the interactive widgets are tied. The `**kwargs` - should match the function signature. Passed to :func:`interactive()` - **kwargs : various, optional - An interactive widget is created for each keyword argument that is a - valid widget abbreviation. Passed to :func:`interactive()` - Examples - -------- - Render an interactive text field that shows the greeting with the passed in - text:: - # 1. Using interact as a function - def greeting(text="World"): - print("Hello {}".format(text)) - interact(greeting, text="IPython Widgets") - # 2. Using interact as a decorator - @interact - def greeting(text="World"): - print("Hello {}".format(text)) - # 3. Using interact as a decorator with named parameters - @interact(text="IPython Widgets") - def greeting(text="World"): - print("Hello {}".format(text)) - Render an interactive slider widget and prints square of number:: - # 1. Using interact as a function - def square(num=1): - print("{} squared is {}".format(num, num*num)) - interact(square, num=5) - # 2. Using interact as a decorator - @interact - def square(num=2): - print("{} squared is {}".format(num, num*num)) - # 3. Using interact as a decorator with named parameters - @interact(num=5) - def square(num=2): - print("{} squared is {}".format(num, num*num)) - """ - # If kwargs are given, replace self by a new - # _InteractFactory with the updated kwargs - if kwargs: - params = list(interactive.param) - kw = dict(self.kwargs) - kw.update({k: v for k, v in kwargs.items() if k not in params}) - opts = dict(self.opts, **{k: v for k, v in kwargs.items() if k in params}) - self = type(self)(self.cls, opts, kw) - - f = __interact_f - if f is None: - # This branch handles the case 3 - # @interact(a=30, b=40) - # def f(*args, **kwargs): - # ... - # - # Simply return the new factory - return self - elif 'throttled' in check_argspec(f).args: - raise ValueError('A function cannot have "throttled" as an argument') - - # positional arg support in: https://gist.github.com/8851331 - # Handle the cases 1 and 2 - # 1. interact(f, **kwargs) - # 2. @interact - # def f(*args, **kwargs): - # ... - w = self.widget(f) - try: - f.widget = w - except AttributeError: - # some things (instancemethods) can't have attributes attached, - # so wrap in a lambda - f = lambda *args, **kwargs: __interact_f(*args, **kwargs) - f.widget = w - return w.layout - - def options(self, **kwds): - """ - Change options for interactive functions. - Returns - ------- - A new :class:`_InteractFactory` which will apply the - options when called. - """ - opts = dict(self.opts) - for k in kwds: - if k not in opts: - raise ValueError(f"invalid option {k!r}") - opts[k] = kwds[k] - return type(self)(self.cls, opts, self.kwargs) - - -interact = interactive.factory() -interact_manual = interact.options(manual_update=True, manual_name="Run Interact") - - -__all__ = ["interact", "interact_manual", "interactive"] diff --git a/panel/tests/pane/test_base.py b/panel/tests/pane/test_base.py index 1173831a138..f49ba6eb96a 100644 --- a/panel/tests/pane/test_base.py +++ b/panel/tests/pane/test_base.py @@ -3,9 +3,9 @@ import panel as pn +from panel._interact import interactive from panel.chat import ChatMessage from panel.config import config -from panel.interact import interactive from panel.io.loading import LOADING_INDICATOR_CSS_CLASS from panel.layout import Row from panel.links import CallbackGenerator diff --git a/panel/tests/test_interact.py b/panel/tests/test_interact.py index ae3ae768dab..097ec3f5efd 100644 --- a/panel/tests/test_interact.py +++ b/panel/tests/test_interact.py @@ -3,7 +3,7 @@ from bokeh.models import Column as BkColumn, Div as BkDiv from panel import widgets -from panel.interact import interactive +from panel._interact import interactive from panel.models import HTML as BkHTML from panel.pane import HTML diff --git a/panel/tests/test_viewable.py b/panel/tests/test_viewable.py index 9aef6bcd626..3456250a750 100644 --- a/panel/tests/test_viewable.py +++ b/panel/tests/test_viewable.py @@ -4,7 +4,7 @@ import panel.custom # To get the custom Viewable from panel import config -from panel.interact import interactive +from panel._interact import interactive from panel.pane import Markdown, Str, panel from panel.param import ParamMethod from panel.viewable import Viewable, Viewer