diff --git a/.gitignore b/.gitignore index 84b10bdae3..0713e04310 100644 --- a/.gitignore +++ b/.gitignore @@ -123,3 +123,4 @@ discover/ node_modules/* /tmp .devcontainer/* +out diff --git a/panel/__init__.py b/panel/__init__.py index 1ddacef788..f0cfce106d 100644 --- a/panel/__init__.py +++ b/panel/__init__.py @@ -1,3 +1,97 @@ +""" +Panel is a high level app and dashboarding framework +==================================================== + +Works with the tools you know and ❤️. + +`Getting Started`_ \| `Discourse`_ \| `Github`_ \| `Twitter`_ \| +`LinkedIn`_ + +Interactive models with ``.bind`` +--------------------------------- + +.. figure:: https://user-images.githubusercontent.com/42288570/150686594-21b03e55-79ef-406b-9e61-1764c6b493c3.gif + :alt: Interactive Model App + + Interactive Model App + +You can use Panels ``.bind`` to bind your models to widgets. + +.. code:: python + + import panel as pn + + color = "#C43F66" + + pn.extension(sizing_mode="stretch_width", template="fast") + + + def model(a, b, emoji): + result = "#" + (emoji * a) + " + " + (emoji * b) + " = " + (emoji * (a + b)) + return result + + pn.pane.Markdown("## Input", margin=(5, 10)).servable(area="sidebar") + input1 = pn.widgets.RadioButtonGroup(value=1, options=[1, 2, 3], button_type="success", name="A").servable(area="sidebar") + input2 = pn.widgets.IntSlider(value=2, start=0, end=3, step=1, margin=(20, 10)).servable(area="sidebar") + + interactive_add = pn.bind(model, a=input1, b=input2, emoji="⭐") + pn.panel(interactive_add).servable(area="main", title="My interactive MODEL") + + pn.state.template.param.update(site="Panel", accent_base_color=color, header_background=color) + +You can serve your app via + +.. code:: bash + + $ panel serve 'script.py' --autoreload + 2022-01-23 15:00:31,373 Starting Bokeh server version 2.4.2 (running on Tornado 6.1) + 2022-01-23 15:00:31,387 User authentication hooks NOT provided (default user enabled) + 2022-01-23 15:00:31,389 Bokeh app running at: http://localhost:5006/script + +The file can be a ``.py`` script or ``.ipynb`` notebook. + +Try changing the return value of the function. Panel will magically ✨ +understand how to show the objects you know and ❤️. + +| This includes `Bokeh`_, +| `HoloViews`_, +| `Matplotlib`_ and +| `Plotly`_ figures. + +Interactive dataframes with ``.interactive`` +-------------------------------------------- + +.. figure:: https://user-images.githubusercontent.com/42288570/150683991-9cece6a1-3751-42d2-8256-505f5deb12be.gif + :alt: Interactive DataFrame App + + Interactive DataFrame App + +You can use `hvplot .interactive`_ to make your dataframes interactive. + +\```python import panel as pn import pandas as pd import hvplot.pandas + +color = “#0072B5” df = pd.DataFrame(data={“x”: [0, 1, 2, 3, 4], “y”: [0, +2, 1, 3, 4]}) + +pn.extension(sizing_mode=“stretch_width”, template=“fast”) + +pn.pane.Markdown(“## Selection”, margin=(10, +10)).servable(area=“sidebar”) count = +pn.widgets.RadioButtonGroup(value=5, options=[3, 4, 5], name=“Count”, +button_type=“success”).servable(area=“sidebar”) + +interactive_df = df.interactive().head + +.. _Discourse: https://discourse.holoviz.org/ +.. _Github: https://github.com/holoviz/panel +.. _Twitter: https://twitter.com/Panel_org +.. _LinkedIn: https://www.linkedin.com/company/79754450 +.. _Bokeh: https://panel.holoviz.org/reference/panes/Bokeh.html#panes-gallery-bokeh +.. _HoloViews: https://panel.holoviz.org/reference/panes/HoloViews.html#panes-gallery-holoviews +.. _Matplotlib: https://panel.holoviz.org/reference/panes/Matplotlib.html#panes-gallery-matplotlib +.. _Plotly: https://panel.holoviz.org/reference/panes/Plotly.html#panes-gallery-plotly +.. _hvplot .interactive: https://hvplot.holoviz.org/user_guide/Interactive.html +""" from . import layout # noqa from . import links # noqa from . import pane # noqa diff --git a/panel/widgets/slider.py b/panel/widgets/slider.py index b63046554b..de2590c662 100644 --- a/panel/widgets/slider.py +++ b/panel/widgets/slider.py @@ -1,7 +1,8 @@ -""" -Defines the Widget base class which provides bi-directional -communication between the rendered dashboard and the Widget -parameters. +"""Sliders allow you to select a value from a defined range of values by moving one or more +handle(s). + +- The `value` will update when a handle is dragged. +- The `value_throttled`will update when a handle is released. """ import param import numpy as np @@ -25,25 +26,24 @@ class _SliderBase(Widget): - bar_color = param.Color(default="#e6e6e6", doc=""" - Color of the slider bar as a hexidecimal RGB value.""") - - direction = param.ObjectSelector(default='ltr', objects=['ltr', 'rtl'], - doc=""" + direction = param.ObjectSelector(default='ltr', objects=['ltr', 'rtl'], doc=""" Whether the slider should go from left-to-right ('ltr') or - right-to-left ('rtl')""") + right-to-left ('rtl').""") - orientation = param.ObjectSelector(default='horizontal', - objects=['horizontal', 'vertical'], doc=""" + orientation = param.ObjectSelector( + default='horizontal', objects=['horizontal', 'vertical'], doc=""" Whether the slider should be oriented horizontally or vertically.""") show_value = param.Boolean(default=True, doc=""" - Whether to show the widget value.""") + Whether to show the widget value as a label or not.""") tooltips = param.Boolean(default=True, doc=""" Whether the slider handle should display tooltips.""") + bar_color = param.Color(default="#e6e6e6", doc=""" + Color of the slider bar as a hexidecimal RGB value.""") + _widget_type = _BkSlider __abstract = True @@ -72,10 +72,10 @@ def _update_model(self, events, msg, root, model, doc, comm): return super()._update_model(events, msg, root, model, doc, comm) -class ContinuousSlider(_SliderBase): +class _ContinuousSlider(_SliderBase): format = param.ClassSelector(class_=(str, TickFormatter,), doc=""" - Allows defining a custom format string or bokeh TickFormatter.""") + A custom format string or Bokeh TickFormatter.""") _supports_embed = True @@ -121,32 +121,53 @@ def _get_embed_state(self, root, values=None, max_opts=3): return (dw, w_model, values, lambda x: x.value, 'value', 'cb_obj.value') -class FloatSlider(ContinuousSlider): +class FloatSlider(_ContinuousSlider): + """The FloatSlider widget allows selecting a floating-point value within a + set of bounds using a slider. - start = param.Number(default=0.0) + Reference: https://panel.holoviz.org/reference/widgets/FloatSlider.html + """ - end = param.Number(default=1.0) + value = param.Number(default=0.0, doc=""" + The selected floating-point value of the slider. Updated when the handle is dragged. + """) + + start = param.Number(default=0.0, doc=""" + The lower bound.""") - value = param.Number(default=0.0) + end = param.Number(default=1.0, doc=""" + The upper bound.""") - value_throttled = param.Number(default=None, constant=True) + step = param.Number(default=0.1, doc=""" + The step size.""") - step = param.Number(default=0.1) + value_throttled = param.Number(default=None, constant=True, doc=""" + The value of the slider. Updated when the handle is released.""") _rename = {'name': 'title'} -class IntSlider(ContinuousSlider): +class IntSlider(_ContinuousSlider): + """The IntSlider widget allows selecting an integer value within a set of bounds + using a slider. + + Reference: https://panel.holoviz.org/reference/widgets/IntSlider.html + """ - value = param.Integer(default=0) + value = param.Integer(default=0, doc=""" + The selected integer value of the slider. Updated when the handle is dragged.""") - value_throttled = param.Integer(default=None, constant=True) + start = param.Integer(default=0, doc=""" + The lower bound.""") - start = param.Integer(default=0) + end = param.Integer(default=1, doc=""" + The upper bound.""") - end = param.Integer(default=1) + step = param.Integer(default=1, doc=""" + The step size.""") - step = param.Integer(default=1) + value_throttled = param.Integer(default=None, constant=True, doc=""" + The value of the slider. Updated when the handle is released""") _rename = {'name': 'title'} @@ -161,14 +182,25 @@ def _process_property_change(self, msg): class DateSlider(_SliderBase): + """The DateSlider widget allows selecting a value within a set of bounds using a slider. + Supports datetime.datetime, datetime.date and np.datetime64 values. + + Reference: https://panel.holoviz.org/reference/widgets/DateSlider.html + """ + + value = param.Date(default=None, doc=""" + The selected date value of the slider. Updated when the slider handle is dragged. Supports + datetime.datetime, datetime.date or np.datetime64 types.""") - value = param.Date(default=None) - value_throttled = param.Date(default=None, constant=True) + start = param.Date(default=None, doc=""" + The lower bound.""") - start = param.Date(default=None) + end = param.Date(default=None, doc=""" + The upper bound.""") - end = param.Date(default=None) + value_throttled = param.Date(default=None, constant=True, doc=""" + The value of the slider. Updated when the slider handle is released.""") _rename = {'name': 'title'} @@ -191,14 +223,24 @@ def _process_property_change(self, msg): class DiscreteSlider(CompositeWidget, _SliderBase): + """The DiscreteSlider widget allows selecting a value from a discrete list or dictionary of + values using a slider. + + Reference: https://panel.holoviz.org/reference/widgets/DiscreteSlider.html + """ + value = param.Parameter(doc=""" + The selected value of the slider. Updated when the handle is dragged. Must be one of + the options.""") + + options = param.ClassSelector(default=[], class_=(dict, list), doc=""" + A list or dictionary of valid options.""") - options = param.ClassSelector(default=[], class_=(dict, list)) - - value = param.Parameter() - - value_throttled = param.Parameter(constant=True) + value_throttled = param.Parameter(constant=True, doc="""The value of the slider. Updated when + the handle is released""") - formatter = param.String(default='%.3g') + # Why do we have both a format and formatter parameter? + formatter = param.String(default='%.3g', doc=""" + A custom format string""") _source_transforms = {'value': None, 'value_throttled': None, 'options': None} @@ -340,6 +382,7 @@ def _get_embed_state(self, root, values=None, max_opts=3): @property def labels(self): + """The list of labels to display""" title = (self.name + ': ' if self.name else '') if isinstance(self.options, dict): return [title + ('%s' % o) for o in self.options] @@ -348,17 +391,19 @@ def labels(self): for o in self.options] @property def values(self): + """The list of option values""" return list(self.options.values()) if isinstance(self.options, dict) else self.options class _RangeSliderBase(_SliderBase): - value = param.Tuple(length=2) + value = param.Tuple(length=2, doc=""" + The selected range of the slider. Updated when a handle is dragged.""") - value_start = param.Parameter(readonly=True) + value_start = param.Parameter(readonly=True, doc="""The lower value of the selected range.""") - value_end = param.Parameter(readonly=True) + value_end = param.Parameter(readonly=True, doc="""The upper value of the selected range.""") __abstract = True @@ -383,26 +428,51 @@ def _process_property_change(self, msg): if 'value_throttled' in msg: msg['value_throttled'] = tuple(msg['value_throttled']) return msg + - +# Why don't we call this a FloatRangeSlider in accordance with FloatSlider? class RangeSlider(_RangeSliderBase): + """ +The RangeSlider widget allows selecting a floating-point range using a slider with +two handles. - format = param.ClassSelector(class_=(str, TickFormatter,), doc=""" - Allows defining a custom format string or bokeh TickFormatter.""") +Reference: https://panel.holoviz.org/reference/widgets/RangeSlider.html - value = param.Range(default=(0, 1)) +Example +------- - value_start = param.Number(default=0, readonly=True) +..code-block:: - value_end = param.Number(default=1, readonly=True) + import panel as pn + pn.extension() + + slider=pn.widgets.RangeSlider(value=(1.0,1.5),start=0.0,end=2.0,step=0.25) + """ - value_throttled = param.Range(default=None, constant=True) + value = param.Range(default=(0, 1), doc= + """The selected range as a tuple of values. Updated when a handle is + dragged.""") - start = param.Number(default=0) + start = param.Number(default=0, doc=""" + The lower bound.""") - end = param.Number(default=1) + end = param.Number(default=1, doc=""" + The upper bound.""") - step = param.Number(default=0.1) + step = param.Number(default=0.1, doc=""" + The step size.""") + + format = param.ClassSelector(class_=(str, TickFormatter,), doc=""" + A format string or bokeh TickFormatter.""") + + value_throttled = param.Range(default=None, constant=True, doc=""" + The selected range as a tuple of floating point values. Updated when a handle is released""") + + value_start = param.Number(default=0, readonly=True, doc=""" + The lower value of the selected range.""") + + value_end = param.Number(default=1, readonly=True, doc=""" + The upper value of the selected range.""") _rename = {'name': 'title', 'value_start': None, 'value_end': None} @@ -417,12 +487,20 @@ def __init__(self, **params): class IntRangeSlider(RangeSlider): + """The IntRangeSlider widget allows selecting an integer range using a slider with + two handles. + + Reference: https://panel.holoviz.org/reference/widgets/IntRangeSlider.html + """ - start = param.Integer(default=0) + start = param.Integer(default=0, doc=""" + The lower bound.""") - end = param.Integer(default=1) + end = param.Integer(default=1, doc=""" + The uppper bound.""") - step = param.Integer(default=1) + step = param.Integer(default=1, doc=""" + The step size""") def _process_property_change(self, msg): msg = super()._process_property_change(msg) @@ -436,20 +514,34 @@ def _process_property_change(self, msg): class DateRangeSlider(_RangeSliderBase): + """The DateRangeSlider widget allows selecting a date range using a slider with + two handles. Supports datetime.datetime, datetime.data and np.datetime64 ranges. + + Reference: https://panel.holoviz.org/reference/widgets/DateRangeSlider.html + """ - value = param.Tuple(default=(None, None), length=2) + value = param.Tuple(default=(None, None), length=2, doc= + """The selected range as a tuple of values. Updated when one of the handles is + dragged. Supports datetime.datetime, datetime.date, np.datetime64 ranges.""") - value_start = param.Date(default=None, readonly=True) + start = param.Date(default=None, doc=""" + The lower bound.""") - value_end = param.Date(default=None, readonly=True) + end = param.Date(default=None, doc=""" + The upper bound.""") - value_throttled = param.Tuple(default=None, length=2, constant=True) + step = param.Number(default=1, doc=""" + The step size""") - start = param.Date(default=None) + value_start = param.Date(default=None, readonly=True, doc=""" + The lower value of the selected range.""") - end = param.Date(default=None) + value_end = param.Date(default=None, readonly=True, doc=""" + The upper value of the selected range.""") - step = param.Number(default=1) + value_throttled = param.Tuple(default=None, length=2, constant=True, doc=""" + The selected range as a tuple of values. Updated one of the handles is released. Supports + datetime.datetime, datetime.date and np.datetime64 ranges""") _source_transforms = {'value': None, 'value_throttled': None, 'start': None, 'end': None, 'step': None} @@ -573,12 +665,25 @@ def _sync_value(self, event): class EditableFloatSlider(_EditableContinuousSlider, FloatSlider): + """ + The EditableFloatSlider widget allows selecting selecting a numeric floating-point value + within a set of bounds using a slider and for more precise control offers an editable number + input box. + + Reference: https://panel.holoviz.org/reference/widgets/EditableFloatSlider.html + """ _slider_widget = FloatSlider _input_widget = FloatInput class EditableIntSlider(_EditableContinuousSlider, IntSlider): + """ + The EditableIntSlider widget allows selecting selecting an integer value within a set of bounds + using a slider and for more precise control offers an editable integer input box. + + Reference: https://panel.holoviz.org/reference/widgets/EditableIntSlider.html + """ _slider_widget = IntSlider _input_widget = IntInput @@ -586,15 +691,22 @@ class EditableIntSlider(_EditableContinuousSlider, IntSlider): class EditableRangeSlider(CompositeWidget, _SliderBase): """ - The EditableRangeSlider extends the RangeSlider by adding text - input fields to manually edit the range and potentially override - the bounds. + The EditableRangeSlider widget allows selecting a floating-point range using a slider with two + handles and for more precise control also offers a set of number input boxes. + + Reference: https://panel.holoviz.org/reference/widgets/EditableRangeSlider.html """ - editable = param.Tuple(default=(True, True), doc=""" - Whether the lower and upper values are editable.""") + value = param.Range(default=(0, 1), doc="Current range value. Updated when a handle is dragged") + + start = param.Number(default=0., doc="Lower bound of the range.") end = param.Number(default=1., doc="Upper bound of the range.") + + step = param.Number(default=0.1, doc="Slider and number input step.") + + editable = param.Tuple(default=(True, True), doc=""" + Whether the lower and upper values are editable.""") format = param.ClassSelector(default='0.0[0000]', class_=(str, TickFormatter,), doc=""" Allows defining a custom format string or bokeh TickFormatter.""") @@ -602,12 +714,6 @@ class EditableRangeSlider(CompositeWidget, _SliderBase): show_value = param.Boolean(default=False, readonly=True, precedence=-1, doc=""" Whether to show the widget value.""") - start = param.Number(default=0., doc="Lower bound of the range.") - - step = param.Number(default=0.1, doc="Slider and number input step.") - - value = param.Range(default=(0, 1), doc="Current range value.") - value_throttled = param.Range(default=None, constant=True) _composite_type = Column diff --git a/panel/widgets/slider.pyi b/panel/widgets/slider.pyi new file mode 100644 index 0000000000..f712496020 --- /dev/null +++ b/panel/widgets/slider.pyi @@ -0,0 +1,383 @@ +from ..config import config as config +from ..io import state as state +from ..layout import Column as Column, Row as Row +from ..util import edit_readonly as edit_readonly, param_reprs as param_reprs, value_as_date as value_as_date, value_as_datetime as value_as_datetime +from ..viewable import Layoutable as Layoutable +from .base import CompositeWidget as CompositeWidget, Widget as Widget +from .input import FloatInput as FloatInput, IntInput as IntInput, StaticText as StaticText +from typing import Any, Optional + +class _SliderBase(Widget): + bar_color: str + direction: str + orientation: str + show_value: bool + tooltips: bool + + def __init__(self, **params) -> None: ... + +class ContinuousSlider(_SliderBase): + format: Any + def __init__(self, **params) -> None: ... + + +class FloatSlider(ContinuousSlider): + start: Any + end: Any + value: Any + value_throttled: Any + step: Any + +from bokeh.models import TickFormatter +from typing import Union, NoneType + +class IntSlider(ContinuousSlider): + value: int=0 + value_throttled: Optional[int]=None + start: int=0 + end: int=1 + step: int=1 + + def __init__(self, + value: int=0, + start: int=0, + end: int=1, + step: int=1, + value_throttled: Optional[int]=None, + format: Optional[Union[str,TickFormatter]]=None, + bar_color: str="#e6e6e6", + **params + ): + """The IntSlider widget allows selecting selecting an integer value within a set bounds + using a slider. + + See https://panel.holoviz.org/reference/widgets/IntSlider.html + + Args: + align: Whether the object should be aligned with the start, end or center of its container. If set as a tuple it will declare (vertical, horizontal) alignment. + aspect_ratio: Describes the proportional relationship between component's width and height. This works if any of component's dimensions are flexible in size. If set to a number, ``width / height = aspect_ratio`` relationship will be maintained. Otherwise, if set to ``"auto"``, component's preferred width and height will be used to determine the aspect (if not set, no aspect will be preserved). + background: Background color of the component. + bar_color: Color of the slider bar as a hexidecimal RGB value. + css_classes: CSS classes to apply to the layout. + direction: Whether the slider should go from left-to-right ('ltr') or right-to-left ('rtl') + disabled: Whether the widget is disabled. + end: The upper bound + format: Allows defining a custom format string or bokeh TickFormatter. + height: The height of the component (in pixels). This can be either fixed or preferred height, depending on height sizing policy. + height_policy: Describes how the component should maintain its height. + loading: Whether or not the Viewable is loading. If True a loading spinner is shown on top of the Viewable. + margin: Allows to create additional space around the component. May be specified as a two-tuple of the form (vertical, horizontal) or a four-tuple (top, right, bottom, left). + max_height: Minimal height of the component (in pixels) if height is adjustable. + max_width: Minimal width of the component (in pixels) if width is adjustable. + min_height: Minimal height of the component (in pixels) if height is adjustable. + min_width: Minimal width of the component (in pixels) if width is adjustable. + name: String identifier for this object. + orientation: Whether the slider should be oriented horizontally or vertically. + show_value: Whether to show the widget value. + sizing_mode: How the component should size itself. + start: The lower bound + step: The step size + tooltips: Whether the slider handle should display tooltips. + value: The value of the widget. Updated when the slider is dragged + value_throttled: The value of the widget. Updated when the mouse is no longer clicked + visible: Whether the component is visible. Setting visible to false will hide the component entirely. + width: The width of the component (in pixels). This can be either fixed or preferred width, depending on width sizing policy. + width_policy: Describes how the component should maintain its width. + """ + +class DateSlider(_SliderBase): + value: Any + value_throttled: Any + start: Any + end: Any + def __init__(self, **params) -> None: ... + +class DiscreteSlider(CompositeWidget, _SliderBase): + options: Any + value: Any + value_throttled: Any + formatter: Any + def __init__(self, **params) -> None: ... + @property + def labels(self): ... + @property + def values(self): ... + +class _RangeSliderBase(_SliderBase): + value: Any + value_start: Any + value_end: Any + def __init__(self, **params) -> None: ... + +class RangeSlider(_RangeSliderBase): + format: Any + value: Any + value_start: Any + value_end: Any + value_throttled: Any + start: Any + end: Any + step: Any + def __init__(self, **params) -> None: ... + +class IntRangeSlider(RangeSlider): + start: Any + end: Any + step: Any + +class DateRangeSlider(_RangeSliderBase): + value: tuple=(None, None) + value_start: Optional[Union[datetime,date,np.datetime64]]=None + value_end: Optional[Union[datetime,date,np.datetime64]]=None + value_throttled: Optional[tuple]=None + start: Optional[Union[datetime,date,np.datetime64]]=None + end: Optional[Union[datetime,date,np.datetime64]]=None + step: Number=1 + + def __init__(self, + value_throttled: Optional[tuple]=None, + start: Optional[Union[datetime,date,np.datetime64]]=None, + end: Optional[Union[datetime,date,np.datetime64]]=None, + step: Number=1, + value: tuple=(None, None), + value_start: Optional[Union[datetime,date,np.datetime64]]=None, + value_end: Optional[Union[datetime,date,np.datetime64]]=None, + bar_color: str="#e6e6e6", + direction: str="ltr", + orientation: str="horizontal", + show_value: bool=True, + tooltips: bool=True, + disabled: bool=False, + loading: bool=False, + align: Union[str,tuple]="start", + aspect_ratio: Any=None, + background: Any=None, + css_classes: Optional[list]=None, + width: Optional[int]=None, + height: Optional[int]=None, + min_width: Optional[int]=None, + min_height: Optional[int]=None, + max_width: Optional[int]=None, + max_height: Optional[int]=None, + margin: Any=(5, 10), + width_policy: str="auto", + height_policy: str="auto", + sizing_mode: Union[NoneType,str]=None, + visible: bool=True, + name: str="DateRangeSlider", + ): + """Parameters of 'DateRangeSlider' +=============================== + +Parameters changed from their default values are marked in red. +Soft bound values are marked in cyan. +C/V= Constant/Variable, RO/RW = ReadOnly/ReadWrite, AN=Allow None + +Name Value Type Bounds Mode + +align 'start' ClassSelector V RW +aspect_ratio None Parameter V RW AN +background None Parameter V RW AN +css_classes None List (0, None) V RW AN +width None Integer (0, None) V RW AN +height None Integer (0, None) V RW AN +min_width None Integer (0, None) V RW AN +min_height None Integer (0, None) V RW AN +max_width None Integer (0, None) V RW AN +max_height None Integer (0, None) V RW AN +margin (5, 10) Parameter V RW +width_policy 'auto' ObjectSelector V RW +height_policy 'auto' ObjectSelector V RW +sizing_mode None ObjectSelector V RW +visible True Boolean (0, 1) V RW +loading False Boolean (0, 1) V RW +disabled False Boolean (0, 1) V RW +bar_color '#e6e6e6' Color V RW +direction 'ltr' ObjectSelector V RW +orientation 'horizontal' ObjectSelector V RW +show_value True Boolean (0, 1) V RW +tooltips True Boolean (0, 1) V RW +value (None, None) Tuple V RW +value_start None Date C RO AN +value_end None Date C RO AN +value_throttled None Tuple C RW AN +start None Date V RW AN +end None Date V RW AN +step 1 Number V RW + +Parameter docstrings: +===================== + +align: Whether the object should be aligned with the start, end or + center of its container. If set as a tuple it will declare + (vertical, horizontal) alignment. +aspect_ratio: Describes the proportional relationship between component's + width and height. This works if any of component's dimensions + are flexible in size. If set to a number, ``width / height = + aspect_ratio`` relationship will be maintained. Otherwise, if + set to ``"auto"``, component's preferred width and height will + be used to determine the aspect (if not set, no aspect will be + preserved). +background: Background color of the component. +css_classes: CSS classes to apply to the layout. +width: The width of the component (in pixels). This can be either + fixed or preferred width, depending on width sizing policy. +height: The height of the component (in pixels). This can be either + fixed or preferred height, depending on height sizing policy. +min_width: Minimal width of the component (in pixels) if width is adjustable. +min_height: Minimal height of the component (in pixels) if height is adjustable. +max_width: Minimal width of the component (in pixels) if width is adjustable. +max_height: Minimal height of the component (in pixels) if height is adjustable. +margin: Allows to create additional space around the component. May + be specified as a two-tuple of the form (vertical, horizontal) + or a four-tuple (top, right, bottom, left). +width_policy: Describes how the component should maintain its width. + + ``"auto"`` + Use component's preferred sizing policy. + + ``"fixed"`` + Use exactly ``width`` pixels. Component will overflow if + it can't fit in the available horizontal space. + + ``"fit"`` + Use component's preferred width (if set) and allow it to + fit into the available horizontal space within the minimum + and maximum width bounds (if set). Component's width + neither will be aggressively minimized nor maximized. + + ``"min"`` + Use as little horizontal space as possible, not less than + the minimum width (if set). The starting point is the + preferred width (if set). The width of the component may + shrink or grow depending on the parent layout, aspect + management and other factors. + + ``"max"`` + Use as much horizontal space as possible, not more than + the maximum width (if set). The starting point is the + preferred width (if set). The width of the component may + shrink or grow depending on the parent layout, aspect + management and other factors. +height_policy: Describes how the component should maintain its height. + + ``"auto"`` + Use component's preferred sizing policy. + + ``"fixed"`` + Use exactly ``height`` pixels. Component will overflow if + it can't fit in the available vertical space. + + ``"fit"`` + Use component's preferred height (if set) and allow to fit + into the available vertical space within the minimum and + maximum height bounds (if set). Component's height neither + will be aggressively minimized nor maximized. + + ``"min"`` + Use as little vertical space as possible, not less than + the minimum height (if set). The starting point is the + preferred height (if set). The height of the component may + shrink or grow depending on the parent layout, aspect + management and other factors. + + ``"max"`` + Use as much vertical space as possible, not more than the + maximum height (if set). The starting point is the + preferred height (if set). The height of the component may + shrink or grow depending on the parent layout, aspect + management and other factors. +sizing_mode: How the component should size itself. + + This is a high-level setting for maintaining width and height + of the component. To gain more fine grained control over + sizing, use ``width_policy``, ``height_policy`` and + ``aspect_ratio`` instead (those take precedence over + ``sizing_mode``). + + ``"fixed"`` + Component is not responsive. It will retain its original + width and height regardless of any subsequent browser + window resize events. + + ``"stretch_width"`` + Component will responsively resize to stretch to the + available width, without maintaining any aspect ratio. The + height of the component depends on the type of the + component and may be fixed or fit to component's contents. + + ``"stretch_height"`` + Component will responsively resize to stretch to the + available height, without maintaining any aspect + ratio. The width of the component depends on the type of + the component and may be fixed or fit to component's + contents. + + ``"stretch_both"`` + Component is completely responsive, independently in width + and height, and will occupy all the available horizontal + and vertical space, even if this changes the aspect ratio + of the component. + + ``"scale_width"`` + Component will responsively resize to stretch to the + available width, while maintaining the original or + provided aspect ratio. + + ``"scale_height"`` + Component will responsively resize to stretch to the + available height, while maintaining the original or + provided aspect ratio. + + ``"scale_both"`` + Component will responsively resize to both the available + width and height, while maintaining the original or + provided aspect ratio. +visible: Whether the component is visible. Setting visible to false will + hide the component entirely. +loading: Whether or not the Viewable is loading. If True a loading spinner + is shown on top of the Viewable. +disabled: Whether the widget is disabled. +bar_color: Color of the slider bar as a hexidecimal RGB value. +direction: Whether the slider should go from left-to-right ('ltr') or + right-to-left ('rtl') +orientation: Whether the slider should be oriented horizontally or + vertically. +show_value: Whether to show the widget value. +tooltips: Whether the slider handle should display tooltips. +value: < No docstring available > +value_start: < No docstring available > +value_end: < No docstring available > +value_throttled: < No docstring available > +start: < No docstring available > +end: < No docstring available > +step: < No docstring available +""" + + + +class _EditableContinuousSlider(CompositeWidget): + editable: Any + show_value: Any + def __init__(self, **params) -> None: ... + +class EditableFloatSlider(_EditableContinuousSlider, FloatSlider): ... +class EditableIntSlider(_EditableContinuousSlider, IntSlider): ... + +class EditableRangeSlider(CompositeWidget, _SliderBase): + """ + + + Args: + CompositeWidget ([type]): [description] + _SliderBase ([type]): [description] + """ + editable: Any + end: Any + format: Any + show_value: Any + start: Any + step: Any + value: Any + value_throttled: Any + def __init__(self, **params) -> None: ... diff --git a/param_stubgen.py b/param_stubgen.py new file mode 100644 index 0000000000..7f2fd87d36 --- /dev/null +++ b/param_stubgen.py @@ -0,0 +1,341 @@ +import glob +import importlib +import importlib.util +import inspect +import logging +import os +import pathlib +import re +from types import ModuleType +from typing import Type +import datetime + +import param + +import panel as pn + +logger = logging.getLogger("param_stubgen") + + +def _get_parameterized_classes(mod: ModuleType): + """Returns an iterator of the Parameterized classes of a module to be included in a stub file""" + module_name = mod.__name__ + module_path = mod.__file__ + + spec = importlib.util.spec_from_file_location(module_name, module_path) + foo = importlib.util.module_from_spec(spec) + try: + spec.loader.exec_module(foo) + except Warning as exc_info: + # Todo: Learn to handle + logger.warning("Could not _get_parameterized_classes of %s", mod, exc_info=exc_info) + return + for value in foo.__dict__.values(): + try: + if issubclass(value, param.Parameterized) and value.__module__ == module_name: + yield (value) + except: + pass + + +def _to_class_name(parameterized): + return parameterized.__name__ + + +def _to_class_str(object) -> str: + return str(object.__name__) + + +def _to_bases(parameterized): + return ", ".join(item.__name__ for item in parameterized.__bases__) + + +PARAMETER_TO_TYPE = { + param.Boolean: "bool", + param.Color: "str", + param.Integer: "int", + param.Parameter: "Any", + param.String: "str", + param.Number: "Number", + param.Date: "Union[datetime.datetime,datetime.date,np.datetime64]", + param.Tuple: "tuple", + param.Range: "Tuple[Number,Number]", + param.Callable: "Callable", + param.Action: "Callable", + param.Filename: "Union[str,pathlib.Path]", + param.Event: "bool", + param.CalendarDate: "datetime.date", + param.DateRange: "Tuple[Union[datetime.datetime,datetime.date,np.datetime64],Union[datetime.datetime,datetime.date,np.datetime64]]" +} + + +def _default_to_string(default): + if default is None: + return "None" + elif callable(default): + return "..." + elif isinstance(default, (pathlib.Path, datetime.date)): + return f'...' + elif isinstance(default, str): + return f'"{default}"' + else: + return str(default) + + +def _to_typehint(parameter: param.Parameter) -> str: + """Returns the typehint as a string of a Parameter + + Example: + + Optional[int]=None + """ + if isinstance(parameter, param.ClassSelector): + class_ = parameter.class_ + if isinstance(class_, (list, tuple, set)): + tpe = ( + "Union[" + + ",".join(sorted((item.__name__ for item in class_), key=str.casefold)) + + "]" + ) + else: + tpe = class_.__name__ + elif isinstance(parameter, (param.ObjectSelector, param.Selector)): + types = set(type(item).__name__ for item in parameter.objects) + if len(types) == 0: + tpe = "Any" + elif len(types) == 1: + tpe = types.pop() + else: + tpe = "Union[" + ",".join(sorted(types, key=str.casefold)) + "]" + elif isinstance(parameter, param.List): + class_ = parameter.class_ + if isinstance(class_, (list, tuple, set)): + tpe = "List[" + ",".join(item.__name__ for item in class_) + "]" + elif class_: + tpe = f"List[{class_.__name__}]" + else: + tpe = "list" + elif parameter.__class__ in PARAMETER_TO_TYPE: + tpe = PARAMETER_TO_TYPE[parameter.__class__] + else: + if parameter.owner: + parameter_class = f"{parameter.owner}".split("'")[1] + else: + parameter_class = "" + parameter_type = f"{type(parameter)}".split("'")[1] + raise NotImplementedError(f"Converting parameter type '{parameter_type}' is not implemented. Found in parameter '{parameter.name}' on the class '{parameter_class}'") + + if parameter.allow_None and not tpe == "Any": + tpe = f"Optional[{tpe}]" + + default_str = _default_to_string(parameter.default) + tpe += f"={default_str}" + + return tpe + + +def _to_type_hints(parameterized: Type[param.Parameterized]) -> dict: + """Returns a dictionary of parameter names and typehints with default values + + Example: + + { + "value": "int=0", + "value_throttled": "Optional[int]=None", + } + """ + typed_attributes = {} + for parameter_name in parameterized.param: + parameter = parameterized.param[parameter_name] + if not parameter_name.startswith("_"): + typehint = _to_typehint(parameter) + typed_attributes[parameter] = typehint + return typed_attributes + + +def _to_typed_attributes(parameterized: Type[param.Parameterized]) -> str: + """Returns a string of typed attributes + + Example: + + value: int=0 + value_throttled: Optional[int]=None + ...""" + typed_attributes = "" + for parameter, typehint in _to_type_hints(parameterized).items(): + if not parameter.name == "name" and parameter.owner is parameterized: + if typed_attributes: + typed_attributes += "\n" + typed_attributes += f" {parameter.name}: {typehint}" + return typed_attributes + + +def _sorted_parameter_names(parameterized: Type[param.Parameterized]) -> str: + "Returns a list of parameter names sorted by 'relevance'. Most relevant parameters first." + parameters = [] + for class_ in reversed(parameterized.mro()): + if issubclass(class_, param.Parameterized): + for parameter in reversed(list(class_.param)): + if not parameter in parameters: + parameters.append(parameter) + return list(reversed(parameters)) + + +def _sorted_parameters(parameterized): + "Returns a list of parameter names sorted by 'relevance'. Most relevant parameters first." + return [parameterized.param[name] for name in _sorted_parameter_names(parameterized)] + + +def _to_init(parameterized: Type[param.Parameterized]) -> str: + """Returns the __init__ signature with typed arguments""" + typed_attributes = "" + type_hints = _to_type_hints(parameterized) + for parameter in _sorted_parameters(parameterized): + if not parameter in type_hints: + continue + typehint = type_hints[parameter] + if typed_attributes: + typed_attributes += "\n" + typed_attributes += f" {parameter.name}: {typehint}," + return f"""\ + def __init__(self, +{typed_attributes} + ):""" + + +ANSI_ESCAPE = re.compile( + r""" + \x1B # ESC + (?: # 7-bit C1 Fe (except CSI) + [@-Z\\-_] + | # or [ for CSI, followed by a control sequence + \[ + [0-?]* # Parameter bytes + [ -/]* # Intermediate bytes + [@-~] # Final byte + ) +""", + re.VERBOSE, +) + +import ast + +# fname = "panel/widgets/slider.py" +# with open(fname, 'r') as f: +# tree = ast.parse(f.read()) + +# for t in tree.body: +# print(t.__dict__) +# try: +# docstring = ast.get_docstring(t) +# print(docstring) +# except Exception: +# pass + +def _get_node(tree, parameterized: Type[param.Parameterized]): + for t in tree.body: + try: + if t.name==parameterized.name: + return t + except: + pass + return None + +def _get_original_docstring(parameterized: Type[param.Parameterized]) -> str: + """Returns the original docstring of a Parameterized class""" + tree = ast.parse(inspect.getsource(parameterized)) + node=_get_node(tree, parameterized) + if node: + doc=ast.get_docstring(node) + else: + raise NotImplementedError() + if not doc: + logger.warning( + "%s has no doc string: %s:%s", + parameterized.__name__, + inspect.getfile(parameterized), + inspect.findsource(parameterized)[1] + 1, + ) + + return doc + + +def _get_args(parameterized: Type[param.Parameterized]) -> str: + """Returns a string of arguments with docstrings + + Example: + + value: The slider value. Updated when the slider is dragged + value_throttled: The slider value. Updated on mouse up. + """ + args = "" + for name in sorted(parameterized.param): + parameter = parameterized.param[name] + if not name.startswith("_"): + if args: + args += "\n" + if parameter.doc: + doc = parameter.doc + else: + doc = "" + logger.warning( + "%s.%s has no doc string: %s:%s", + parameterized.__name__, + parameter.name, + inspect.getfile(parameterized), + inspect.findsource(parameterized)[1] + 1, + ) + doc = doc.lstrip("\n").lstrip() + if "\n\n" in doc: + doc = doc.split("\n\n")[0] # We simplify the docstring for now + doc = doc.replace("\n", " ") # Move to one line + doc = re.sub(" +", " ", doc) + args += f" {name}: {doc}" + return args + + +def parameterized_to_stub(parameterized: Type[param.Parameterized]) -> str: + """Returns the stub of a Parameterized class""" + class_name = _to_class_name(parameterized) + bases = _to_bases(parameterized) + typed_parameters = _to_typed_attributes(parameterized) + init = _to_init(parameterized) + original_doc = _get_original_docstring(parameterized) + args = _get_args(parameterized) + + return f'''class {class_name}({bases}): +{typed_parameters} + +{init} + """{original_doc} + + Args: +{args} +""" +''' + + +def module_to_stub(module: ModuleType) -> str: + stub = "" + for parameterized in _get_parameterized_classes(module): + stub += "\n\n" + parameterized_to_stub(parameterized) + return stub + +def get_package_folder(package) -> pathlib.Path: + return pathlib.Path(package.__file__).parent + +def to_module(path, parent, package_name="panel"): + relative = str(pathlib.Path(path).relative_to(parent)) + + module_name = package_name + "." + str(relative).replace(".py", "").replace(os.path.sep, ".") + try: + return importlib.import_module(module_name) + except Exception as exc_info: + logger.warning("Error. Cannot import %s", path, exc_info=exc_info) + +def get_modules(package): + package_path = get_package_folder(package) + for filename in glob.iglob(str(package_path.resolve()) + '/**/*.py', recursive=True): + module = to_module(filename, package_path) + if module: + yield(module, filename) \ No newline at end of file diff --git a/script.py b/script.py new file mode 100644 index 0000000000..f9194708c0 --- /dev/null +++ b/script.py @@ -0,0 +1,13 @@ +import panel as pn + +pn.extension(sizing_mode="stretch_width", template="fast") + +import param + +class Hello(param.Parameterized): + value = param.Parameter("test", readonly=True) + +Hello(value="this") + +pn.widgets.EditableRangeSlider + diff --git a/test_param_stubgen.py b/test_param_stubgen.py new file mode 100644 index 0000000000..5096bbafa7 --- /dev/null +++ b/test_param_stubgen.py @@ -0,0 +1,237 @@ +import datetime +import logging +import pathlib +from inspect import ismodule + +import numpy as np +import param +import pytest + +import panel as pn +from panel.widgets import slider +from param_stubgen import (_default_to_string, _get_original_docstring, + _get_parameterized_classes, _sorted_parameter_names, + _to_bases, _to_class_name, _to_init, + _to_typed_attributes, _to_typehint, get_modules, + module_to_stub, parameterized_to_stub, to_module) + +FORMAT = '%(asctime)s %(message)s' +logging.basicConfig(format=FORMAT) + +class Parent(param.Parameterized): + """The Parent class provides ...""" + a = param.String(doc="A string parameter") + c = param.Integer(doc="An int parameter") + +class Child(Parent): + """The Child class provides ...""" + b = param.List(doc="A list parameter") + +def test_can_get_parameterized_classes(): + result = _get_parameterized_classes(slider) + result_set = set(item.__name__ for item in result) + assert result_set == { + "_SliderBase", + "ContinuousSlider", + "FloatSlider", + "IntSlider", + "DateSlider", + "DiscreteSlider", + "_RangeSliderBase", + "RangeSlider", + "IntRangeSlider", + "DateRangeSlider", + "_EditableContinuousSlider", + "EditableFloatSlider", + "EditableIntSlider", + "EditableRangeSlider", + } + + +def test_to_class_name(): + parameterized = slider.IntSlider + assert _to_class_name(parameterized) == "IntSlider" + + +def test_to_bases(): + parameterized = slider.IntSlider + assert _to_bases(parameterized) == "ContinuousSlider" + + +@pytest.mark.parametrize( + ["parameter", "expected"], + [ + (param.String(default="a"), '"a"'), + (param.String(default=None), "None"), + (param.Integer(default=2), "2"), + (param.Callable(default=print), "print"), + ], +) +def test_default_to_string(parameter, expected): + assert _default_to_string(parameter.default) == expected + + +@pytest.mark.parametrize( + ["parameter", "typehint"], + [ + ( + param.Boolean(), + "bool=False", + ), + ( + param.Boolean(default=None), + "Optional[bool]=None", + ), + ( + param.Color(), + "Optional[str]=None", + ), + ( + param.Color("#aabbcc", allow_None=False), + 'str="#aabbcc"', + ), + (param.Integer(), "int=0"), + (param.List(), "list=[]"), + (param.List(class_=str), "List[str]=[]"), + (param.List(class_=(str, int)), "List[str,int]=[]"), + (param.Parameter(), "Any=None"), + (param.String(), 'str=""'), + (param.ClassSelector(class_=bool), "Optional[bool]=None"), + (param.ClassSelector(default=True, class_=bool), "bool=True"), + (param.ClassSelector(default=True, class_=bool, allow_None=True), "Optional[bool]=True"), + (param.ClassSelector(class_=(str, int)), "Optional[Union[int,str]]=None"), + (param.ClassSelector(default="test", class_=(str, int)), 'Union[int,str]="test"'), + ( + param.ClassSelector(default="test", class_=(str, int), allow_None=True), + 'Optional[Union[int,str]]="test"', + ), + (slider.Widget.param.sizing_mode, "Union[NoneType,str]=None"), + (param.Number(), 'Number=0.0'), + (param.Date(), 'Optional[Union[datetime.datetime,datetime.date,np.datetime64]]=None'), + (param.Tuple(), "tuple=(0, 0)"), + (param.Range(), "Optional[Tuple[Number,Number]]=None"), + (param.ObjectSelector(), "Any=None"), + (param.Selector() ,"Any=None"), + (param.Callable(), "Optional[Callable]=None"), + (param.Callable(print), "Callable=..."), + (param.Action(), "Optional[Callable]=None"), + (param.Action(default=lambda x: print(x)), "Callable=..."), + (param.Filename(), "Optional[Union[str,pathlib.Path]]=None"), + (param.Filename(default=pathlib.Path(__file__)), 'Union[str,pathlib.Path]=...'), + (param.Filename(default='/home/'), 'Union[str,pathlib.Path]="/home/"'), + (param.Event(), "bool=False"), + (param.CalendarDate(), "Optional[datetime.date]=None"), + (param.CalendarDate(default=datetime.date(2020,2,3)), "datetime.date=..."), + (param.DateRange(),"Optional[Tuple[Union[datetime.datetime,datetime.date,np.datetime64],Union[datetime.datetime,datetime.date,np.datetime64]]]=None"), + (param.DateRange((datetime.date(2020,2,3), datetime.date(2021,2,3))),"Tuple[Union[datetime.datetime,datetime.date,np.datetime64],Union[datetime.datetime,datetime.date,np.datetime64]]=..."), + ], +) +def test_to_type_hint(parameter, typehint): + assert _to_typehint(parameter) == typehint + + +def test_to_typed_attributes(): + parameterized = slider.IntSlider + assert ( + _to_typed_attributes(parameterized) + == """\ + value: int=0 + start: int=0 + end: int=1 + step: int=1 + value_throttled: Optional[int]=None""" + ) + +def test_get_original_docstring(): + expected = 'The Child class provides ...' + assert _get_original_docstring(Child)==expected + + +def test_sort_parameters(): + actual = _sorted_parameter_names(Child) + assert actual==['b', 'a', 'c', 'name'] + + +def test_to_init(): + parameterized = slider.IntSlider + expected = """\ + def __init__(self, + value: int=0, + start: int=0, + end: int=1, + step: int=1, + value_throttled: Optional[int]=None, + format: Optional[Union[str,TickFormatter]]=None, + bar_color: str="#e6e6e6", + direction: str="ltr", + orientation: str="horizontal", + show_value: bool=True, + tooltips: bool=True, + disabled: bool=False, + loading: bool=False, + align: Union[str,tuple]="start", + aspect_ratio: Any=None, + background: Any=None, + css_classes: Optional[list]=None, + width: Optional[int]=None, + height: Optional[int]=None, + min_width: Optional[int]=None, + min_height: Optional[int]=None, + max_width: Optional[int]=None, + max_height: Optional[int]=None, + margin: Any=(5, 10), + width_policy: str="auto", + height_policy: str="auto", + sizing_mode: Union[NoneType,str]=None, + visible: bool=True, + name: str="IntSlider", + ):""" + actual = _to_init(parameterized) + assert len(actual) == len(expected) + assert actual == expected + +def test_to_stub_intslider_without_exceptions(): + assert parameterized_to_stub(slider.IntSlider) + +def test_to_original_docstring(): + _get_original_docstring + +def test_to_stub_basic_parameterized(): + # Todo: indent Args + expected = '''\ +class Child(Parent): + b: list=[] + + def __init__(self, + b: list=[], + a: str="", + c: int=0, + name: Optional[str]="Child", + ): + """The Child class provides ... + + Args: + a: A string parameter + b: A list parameter + c: An int parameter + name: String identifier for this object. +""" +''' + assert parameterized_to_stub(Child) == expected + +def test_module_to_stub(): + """Can create stub from module""" + assert module_to_stub(slider) + +def test_to_module(): + to_module(path="", parent="pathlib.Path(pn.__file__).parent") + +def test_get_modules(): + modules = list(get_modules(pn)) + assert modules + assert ismodule(modules[0][0]) + assert isinstance(modules[0][1], str) + +def test_can_stub_panel(): + for module, path in get_modules(pn): + module_to_stub(module)