From 7cb9dbf8c865017d3a3522f6bb26a737167f43e0 Mon Sep 17 00:00:00 2001 From: Marc Skov Madsen Date: Sat, 22 Jan 2022 11:19:41 +0100 Subject: [PATCH 1/6] proof of concept - autogenerate stubbs --- .gitignore | 1 + panel/widgets/slider.py | 20 +++-- panel/widgets/slider.pyi | 118 ++++++++++++++++++++++++++ param_stubgen.py | 138 +++++++++++++++++++++++++++++++ script.py | 9 ++ test_param_stubgen.py | 174 +++++++++++++++++++++++++++++++++++++++ 6 files changed, 455 insertions(+), 5 deletions(-) create mode 100644 panel/widgets/slider.pyi create mode 100644 param_stubgen.py create mode 100644 script.py create mode 100644 test_param_stubgen.py 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/widgets/slider.py b/panel/widgets/slider.py index b63046554b..ecdbf25dcd 100644 --- a/panel/widgets/slider.py +++ b/panel/widgets/slider.py @@ -137,16 +137,26 @@ class FloatSlider(ContinuousSlider): class IntSlider(ContinuousSlider): + """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 + """ - value = param.Integer(default=0) + value = param.Integer(default=0, doc=""" + The value of the widget. Updated when the slider is dragged""") - value_throttled = param.Integer(default=None, constant=True) + value_throttled = param.Integer(default=None, constant=True, doc="""" + The value of the widget. Updated when the mouse is no longer clicked""") - 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 upper bound""") - step = param.Integer(default=1) + step = param.Integer(default=1, doc=""" + The step size""") _rename = {'name': 'title'} diff --git a/panel/widgets/slider.pyi b/panel/widgets/slider.pyi new file mode 100644 index 0000000000..d48c6c6bb5 --- /dev/null +++ b/panel/widgets/slider.pyi @@ -0,0 +1,118 @@ +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 + +class IntSlider(ContinuousSlider): + value: int + value_throttled: Optional[int] + start: int + end: int + step: int + + def __init__(self, value: int=0, value_throttled: Optional[int]=None, start: int=0, end: int=1, step: int=1, **params) -> None: + """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: + 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 + start: The lower bound + end: The upper bound + step: The step size + """ + +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: Any + value_start: Any + value_end: Any + value_throttled: Any + start: Any + end: Any + step: Any + +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): + 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..dd26c9b9e7 --- /dev/null +++ b/param_stubgen.py @@ -0,0 +1,138 @@ +import importlib.util + +import param +import re + +def get_parameterized_classes(module_name, module_path): + spec = importlib.util.spec_from_file_location(module_name, module_path) + foo = importlib.util.module_from_spec(spec) + spec.loader.exec_module(foo) + 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_bases(parameterized): + return ", ".join(item.__name__ for item in parameterized.__bases__) + +MAP = { + param.Boolean: 'bool', + param.Color: 'str', + param.Integer: 'int', + param.Parameter: 'Any', + param.String: 'str', +} + +def _default_to_string(default): + if default is None: + return 'None' + elif isinstance(default, str): + return f'"{default}"' + else: + return str(default) + return + +def _to_typehint(parameter: param.Parameter) -> str: + 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): + 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 MAP: + tpe = MAP[parameter.__class__] + else: + raise NotImplementedError(parameter) + + 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) -> dict: + 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): + 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 _to_init(parameterized): + typed_attributes = "" + type_hints=_to_type_hints(parameterized) + for parameter in sorted(type_hints.keys(), key=lambda x: x.name): + 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) + +def _get_original_docstring(parameterized): + doc=ansi_escape.sub('', parameterized.__doc__) + doc2=doc[doc.find("\n")+1:] + doc3=doc2[:doc2.find("\nParameters of \'")] + return doc3 + +def to_stub(parameterized: param.Parameterized): + 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) + return f'''Class {class_name}({bases}): +{_typed_parameters} + +{_init} + """{original_doc}""" +''' + + \ No newline at end of file diff --git a/script.py b/script.py new file mode 100644 index 0000000000..3e59dc8608 --- /dev/null +++ b/script.py @@ -0,0 +1,9 @@ +import panel as pn + +islider = pn.widgets.IntSlider() + + +fslider = pn.widgets.slider.FloatSlider() +islider.value +fslider.value + diff --git a/test_param_stubgen.py b/test_param_stubgen.py new file mode 100644 index 0000000000..117253a449 --- /dev/null +++ b/test_param_stubgen.py @@ -0,0 +1,174 @@ +from param_stubgen import get_parameterized_classes, _to_bases, _to_class_name, to_stub, _to_typed_attributes, _to_typehint, _default_to_string, _to_init +from panel.widgets import slider +import pytest +import param + + +def test_can_get_parameterized_classes(): + result = get_parameterized_classes(slider.__name__, slider.__file__) + 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'), +]) +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"), +]) +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 + value_throttled: Optional[int]=None + start: int=0 + end: int=1 + step: int=1""" + +def test_to_init(): + parameterized = slider.IntSlider + expected="""\ + def __init__(self, + align: Union[str,tuple]="start", + aspect_ratio: Any=None, + background: Any=None, + bar_color: str="#e6e6e6", + css_classes: Optional[list]=None, + direction: str="ltr", + disabled: bool=False, + end: int=1, + format: Optional[Union[str,TickFormatter]]=None, + height: Optional[int]=None, + height_policy: str="auto", + loading: bool=False, + margin: Any=(5, 10), + max_height: Optional[int]=None, + max_width: Optional[int]=None, + min_height: Optional[int]=None, + min_width: Optional[int]=None, + name: str="IntSlider", + orientation: str="horizontal", + show_value: bool=True, + sizing_mode: Union[NoneType,str]=None, + start: int=0, + step: int=1, + tooltips: bool=True, + value: int=0, + value_throttled: Optional[int]=None, + visible: bool=True, + width: Optional[int]=None, + width_policy: str="auto", + ):""" + actual = _to_init(parameterized) + assert len(actual)==len(expected) + first_diff = -1 + for index, (a, e) in enumerate(zip(actual, expected)): + if a!=e: + first_diff=index + break + assert actual==expected + +def test_to_stub(): + parameterized = slider.IntSlider + stub = '''\ +Class IntSlider(ContinuousSlider): + value: int=0 + value_throttled: Optional[int]=None + start: int=0 + end: int=1 + step: int=1 + + def __init__(self, + align: Union[str,tuple]="start", + aspect_ratio: Any=None, + background: Any=None, + bar_color: str="#e6e6e6", + css_classes: Optional[list]=None, + direction: str="ltr", + disabled: bool=False, + end: int=1, + format: Optional[Union[str,TickFormatter]]=None, + height: Optional[int]=None, + height_policy: str="auto", + loading: bool=False, + margin: Any=(5, 10), + max_height: Optional[int]=None, + max_width: Optional[int]=None, + min_height: Optional[int]=None, + min_width: Optional[int]=None, + name: str="IntSlider", + orientation: str="horizontal", + show_value: bool=True, + sizing_mode: Union[NoneType,str]=None, + start: int=0, + step: int=1, + tooltips: bool=True, + value: int=0, + value_throttled: Optional[int]=None, + visible: bool=True, + width: Optional[int]=None, + width_policy: str="auto", + ) -> None: + """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: + 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 + start: The lower bound + end: The upper bound + step: The step size + """ +''' + assert to_stub(parameterized)==stub + + From e9ac12638657a2c57d58484f6cc2103b04206a3d Mon Sep 17 00:00:00 2001 From: Marc Skov Madsen Date: Sat, 22 Jan 2022 20:08:00 +0100 Subject: [PATCH 2/6] proof of concept --- panel/widgets/slider.py | 6 +- panel/widgets/slider.pyi | 64 ++++++++++---- param_stubgen.py | 37 ++++++++- script.py | 4 +- test_param_stubgen.py | 175 ++++++++++++++++++++++++--------------- 5 files changed, 199 insertions(+), 87 deletions(-) diff --git a/panel/widgets/slider.py b/panel/widgets/slider.py index ecdbf25dcd..2bbe5ea384 100644 --- a/panel/widgets/slider.py +++ b/panel/widgets/slider.py @@ -146,9 +146,6 @@ class IntSlider(ContinuousSlider): value = param.Integer(default=0, doc=""" The value of the widget. Updated when the slider is dragged""") - value_throttled = param.Integer(default=None, constant=True, doc="""" - The value of the widget. Updated when the mouse is no longer clicked""") - start = param.Integer(default=0, doc=""" The lower bound""") @@ -158,6 +155,9 @@ class IntSlider(ContinuousSlider): step = param.Integer(default=1, doc=""" The step size""") + value_throttled = param.Integer(default=None, constant=True, doc=""" + The value of the widget. Updated on mouse up""") + _rename = {'name': 'title'} def _process_property_change(self, msg): diff --git a/panel/widgets/slider.pyi b/panel/widgets/slider.pyi index d48c6c6bb5..333f9cd9d0 100644 --- a/panel/widgets/slider.pyi +++ b/panel/widgets/slider.pyi @@ -27,26 +27,62 @@ class FloatSlider(ContinuousSlider): value: Any value_throttled: Any step: Any + +from bokeh.models import TickFormatter +from typing import Union, NoneType class IntSlider(ContinuousSlider): - value: int - value_throttled: Optional[int] - start: int - end: int - step: int - - def __init__(self, value: int=0, value_throttled: Optional[int]=None, start: int=0, end: int=1, step: int=1, **params) -> None: - """The IntSlider widget allows selecting selecting an integer value within a set bounds + 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: - 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 - start: The lower bound - end: The upper bound - step: The step size + 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): diff --git a/param_stubgen.py b/param_stubgen.py index dd26c9b9e7..d029c6f4c1 100644 --- a/param_stubgen.py +++ b/param_stubgen.py @@ -91,10 +91,24 @@ def _to_typed_attributes(parameterized): typed_attributes += f" {parameter.name}: {typehint}" return typed_attributes +def _sorted_parameter_names(parameterized): + 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): + return [parameterized.param[name] for name in _sorted_parameter_names(parameterized)] + def _to_init(parameterized): typed_attributes = "" type_hints=_to_type_hints(parameterized) - for parameter in sorted(type_hints.keys(), key=lambda x: x.name): + for parameter in _sorted_parameters(parameterized): + if not parameter in type_hints: + continue typehint=type_hints[parameter] if typed_attributes: typed_attributes += "\n" @@ -122,17 +136,36 @@ def _get_original_docstring(parameterized): doc3=doc2[:doc2.find("\nParameters of \'")] return doc3 +def _get_args(parameterized): + args = "" + for name in sorted(parameterized.param): + parameter = parameterized.param[name] + if not name.startswith("_"): + if args: + args += "\n" + doc = parameter.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 to_stub(parameterized: param.Parameterized): 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) - return f'''Class {class_name}({bases}): + args = _get_args(parameterized) + return f'''class {class_name}({bases}): {_typed_parameters} {_init} """{original_doc}""" + + Args: +{args} ''' \ No newline at end of file diff --git a/script.py b/script.py index 3e59dc8608..036245cde8 100644 --- a/script.py +++ b/script.py @@ -3,7 +3,5 @@ islider = pn.widgets.IntSlider() -fslider = pn.widgets.slider.FloatSlider() -islider.value -fslider.value + diff --git a/test_param_stubgen.py b/test_param_stubgen.py index 117253a449..0121ffa213 100644 --- a/test_param_stubgen.py +++ b/test_param_stubgen.py @@ -1,4 +1,14 @@ -from param_stubgen import get_parameterized_classes, _to_bases, _to_class_name, to_stub, _to_typed_attributes, _to_typehint, _default_to_string, _to_init +from param_stubgen import ( + get_parameterized_classes, + _to_bases, + _to_class_name, + to_stub, + _to_typed_attributes, + _to_typehint, + _default_to_string, + _to_init, + _sorted_parameter_names, +) from panel.widgets import slider import pytest import param @@ -24,101 +34,138 @@ def test_can_get_parameterized_classes(): "EditableRangeSlider", } + def test_to_class_name(): parameterized = slider.IntSlider - assert _to_class_name(parameterized)=="IntSlider" + assert _to_class_name(parameterized) == "IntSlider" + def test_to_bases(): parameterized = slider.IntSlider - assert _to_bases(parameterized)=="ContinuousSlider" + 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'), -]) + +@pytest.mark.parametrize( + ["parameter", "expected"], + [ + (param.String(default="a"), '"a"'), + (param.String(default=None), "None"), + (param.Integer(default=2), "2"), + ], +) 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"), -]) + 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"), + ], +) def test_to_type_hint(parameter, typehint): - assert _to_typehint(parameter)==typehint + assert _to_typehint(parameter) == typehint + def test_to_typed_attributes(): parameterized = slider.IntSlider - assert _to_typed_attributes(parameterized)=="""\ + assert ( + _to_typed_attributes(parameterized) + == """\ value: int=0 value_throttled: Optional[int]=None start: int=0 end: int=1 step: int=1""" + ) + + +def test_sort_parameters(): + class Parent(param.Parameterized): + a = param.Parameter() + c = param.Parameter() + + class Child(Parent): + b = param.Parameter() + + actual = _sorted_parameter_names(Child) + assert actual==['b', 'a', 'c', 'name'] + def test_to_init(): parameterized = slider.IntSlider - expected="""\ + 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, - bar_color: str="#e6e6e6", css_classes: Optional[list]=None, - direction: str="ltr", - disabled: bool=False, - end: int=1, - format: Optional[Union[str,TickFormatter]]=None, + width: Optional[int]=None, height: Optional[int]=None, - height_policy: str="auto", - loading: bool=False, - margin: Any=(5, 10), - max_height: Optional[int]=None, - max_width: Optional[int]=None, - min_height: Optional[int]=None, min_width: Optional[int]=None, - name: str="IntSlider", - orientation: str="horizontal", - show_value: bool=True, + 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, - start: int=0, - step: int=1, - tooltips: bool=True, - value: int=0, - value_throttled: Optional[int]=None, visible: bool=True, - width: Optional[int]=None, - width_policy: str="auto", + name: str="IntSlider", ):""" actual = _to_init(parameterized) - assert len(actual)==len(expected) - first_diff = -1 - for index, (a, e) in enumerate(zip(actual, expected)): - if a!=e: - first_diff=index - break - assert actual==expected + assert len(actual) == len(expected) + assert actual == expected + def test_to_stub(): parameterized = slider.IntSlider stub = '''\ -Class IntSlider(ContinuousSlider): +class IntSlider(ContinuousSlider): value: int=0 value_throttled: Optional[int]=None start: int=0 @@ -169,6 +216,4 @@ def __init__(self, step: The step size """ ''' - assert to_stub(parameterized)==stub - - + assert to_stub(parameterized) == stub From 43bc4de41c54a0b8da924bc538a24c0e28686b9d Mon Sep 17 00:00:00 2001 From: Marc Skov Madsen Date: Sat, 22 Jan 2022 20:21:07 +0100 Subject: [PATCH 3/6] improve docstrings --- param_stubgen.py | 28 +++++++++++-- test_param_stubgen.py | 92 +++++++++++++++++++++++++++---------------- 2 files changed, 83 insertions(+), 37 deletions(-) diff --git a/param_stubgen.py b/param_stubgen.py index d029c6f4c1..cd66c11102 100644 --- a/param_stubgen.py +++ b/param_stubgen.py @@ -3,7 +3,8 @@ import param import re -def get_parameterized_classes(module_name, module_path): +def _get_parameterized_classes(module_name, module_path): + """Returns an iterator of the Parameterized classes of a module to be included in a stub file""" spec = importlib.util.spec_from_file_location(module_name, module_path) foo = importlib.util.module_from_spec(spec) spec.loader.exec_module(foo) @@ -35,9 +36,9 @@ def _default_to_string(default): return f'"{default}"' else: return str(default) - return def _to_typehint(parameter: param.Parameter) -> str: + """Returns the typehint as a string of a Parameter""" if isinstance(parameter, param.ClassSelector): class_ = parameter.class_ if isinstance(class_, (list, tuple, set)): @@ -74,6 +75,7 @@ def _to_typehint(parameter: param.Parameter) -> str: return tpe def _to_type_hints(parameterized) -> dict: + """Returns a dictionary of parameter names and typehints""" typed_attributes = {} for parameter_name in parameterized.param: parameter=parameterized.param[parameter_name] @@ -83,6 +85,14 @@ def _to_type_hints(parameterized) -> dict: return typed_attributes def _to_typed_attributes(parameterized): + """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: @@ -92,6 +102,7 @@ def _to_typed_attributes(parameterized): return typed_attributes def _sorted_parameter_names(parameterized): + "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): @@ -101,9 +112,11 @@ def _sorted_parameter_names(parameterized): 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): + """Returns the __init__ signature with typed arguments""" typed_attributes = "" type_hints=_to_type_hints(parameterized) for parameter in _sorted_parameters(parameterized): @@ -131,12 +144,20 @@ def __init__(self, ''', re.VERBOSE) def _get_original_docstring(parameterized): + """Returns the original docstring of a Parameterized class""" doc=ansi_escape.sub('', parameterized.__doc__) doc2=doc[doc.find("\n")+1:] doc3=doc2[:doc2.find("\nParameters of \'")] return doc3 def _get_args(parameterized): + """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] @@ -151,7 +172,8 @@ def _get_args(parameterized): args += f" {name}: {doc}" return args -def to_stub(parameterized: param.Parameterized): +def to_stub(parameterized): + """Returns the stub of a Parameterized class""" class_name = _to_class_name(parameterized) bases = _to_bases(parameterized) _typed_parameters = _to_typed_attributes(parameterized) diff --git a/test_param_stubgen.py b/test_param_stubgen.py index 0121ffa213..9543a7d50a 100644 --- a/test_param_stubgen.py +++ b/test_param_stubgen.py @@ -1,5 +1,5 @@ from param_stubgen import ( - get_parameterized_classes, + _get_parameterized_classes, _to_bases, _to_class_name, to_stub, @@ -15,7 +15,7 @@ def test_can_get_parameterized_classes(): - result = get_parameterized_classes(slider.__name__, slider.__file__) + result = _get_parameterized_classes(slider.__name__, slider.__file__) result_set = set(item.__name__ for item in result) assert result_set == { "_SliderBase", @@ -104,10 +104,10 @@ def test_to_typed_attributes(): _to_typed_attributes(parameterized) == """\ value: int=0 - value_throttled: Optional[int]=None start: int=0 end: int=1 - step: int=1""" + step: int=1 + value_throttled: Optional[int]=None""" ) @@ -167,53 +167,77 @@ def test_to_stub(): stub = '''\ class IntSlider(ContinuousSlider): value: int=0 - value_throttled: Optional[int]=None start: int=0 end: int=1 step: int=1 - + value_throttled: Optional[int]=None + 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, - bar_color: str="#e6e6e6", css_classes: Optional[list]=None, - direction: str="ltr", - disabled: bool=False, - end: int=1, - format: Optional[Union[str,TickFormatter]]=None, + width: Optional[int]=None, height: Optional[int]=None, - height_policy: str="auto", - loading: bool=False, - margin: Any=(5, 10), - max_height: Optional[int]=None, - max_width: Optional[int]=None, - min_height: Optional[int]=None, min_width: Optional[int]=None, - name: str="IntSlider", - orientation: str="horizontal", - show_value: bool=True, + 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, - start: int=0, - step: int=1, - tooltips: bool=True, - value: int=0, - value_throttled: Optional[int]=None, visible: bool=True, - width: Optional[int]=None, - width_policy: str="auto", - ) -> None: - """The IntSlider widget allows selecting selecting an integer value within a set bounds + name: str="IntSlider", + ): + """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: - 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 - start: The lower bound + 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 on mouse up + 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. +""" ''' assert to_stub(parameterized) == stub From 317f50a80aa9e8aae8714f8277d53f4606746420 Mon Sep 17 00:00:00 2001 From: Marc Skov Madsen Date: Sun, 23 Jan 2022 17:06:15 +0100 Subject: [PATCH 4/6] more work --- panel/__init__.py | 92 +++++++++++++++ panel/widgets/slider.pyi | 236 +++++++++++++++++++++++++++++++++++++-- param_stubgen.py | 216 +++++++++++++++++++++++++---------- script.py | 6 +- test_param_stubgen.py | 155 ++++++++++--------------- 5 files changed, 542 insertions(+), 163 deletions(-) diff --git a/panel/__init__.py b/panel/__init__.py index 1ddacef788..14fed0fc2c 100644 --- a/panel/__init__.py +++ b/panel/__init__.py @@ -1,3 +1,95 @@ +"""# Panel is a high level app and dashboarding framework + +Works with the tools you know and ❤️. Welcome to our community 👪 + +- Get Started: https://panel.holoviz.org/ +- Join the Community: https://discourse.holoviz.org/ +- Contribute: https://github.com/holoviz/panel + +Follow us on [Twitter](https://twitter.com/Panel_org) or [LinkedIn](https://www.linkedin.com/company/79754450). +¨ + +## Interactive models with `.bind` + +![Interactive Model App](https://user-images.githubusercontent.com/42288570/150686594-21b03e55-79ef-406b-9e61-1764c6b493c3.gif) + +You can use Panels `.bind` to bind your models to widgets. + +```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 + +```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](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) and \ +[Plotly](https://panel.holoviz.org/reference/panes/Plotly.html#panes-gallery-plotly) figures. + +## Interactive dataframes with `.interactive` + +![Interactive DataFrame App](https://user-images.githubusercontent.com/42288570/150683991-9cece6a1-3751-42d2-8256-505f5deb12be.gif) + +You can use [hvplot .interactive](https://hvplot.holoviz.org/user_guide/Interactive.html) 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(n=count) +pn.panel(interactive_df.panel(height=200)).servable(area="main") + +interactive_plot = interactive_df.hvplot(x="x", y="y").opts(line_width=6, color=color) +pn.panel(interactive_plot.panel(height=200)).servable(area="main", title="My interactive DATAFRAME") + +pn.state.template.param.update(site="Panel", accent_base_color=color, header_background=color) +``` + +The DataFrames can be any of Pandas, Dask, CuDF and Xarray dataframes. + +For more about hvplot `.interactive` check out this [blog post](https://towardsdatascience.com/the-easiest-way-to-create-an-interactive-dashboard-in-python-77440f2511d1). +""" from . import layout # noqa from . import links # noqa from . import pane # noqa diff --git a/panel/widgets/slider.pyi b/panel/widgets/slider.pyi index 333f9cd9d0..2993c6b150 100644 --- a/panel/widgets/slider.pyi +++ b/panel/widgets/slider.pyi @@ -126,13 +126,235 @@ class IntRangeSlider(RangeSlider): step: Any class DateRangeSlider(_RangeSliderBase): - value: Any - value_start: Any - value_end: Any - value_throttled: Any - start: Any - end: Any - step: Any + 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 diff --git a/param_stubgen.py b/param_stubgen.py index cd66c11102..4690a917f8 100644 --- a/param_stubgen.py +++ b/param_stubgen.py @@ -1,55 +1,88 @@ import importlib.util +from types import ModuleType +import inspect import param import re +from typing import Type + +import logging + +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__ -def _get_parameterized_classes(module_name, module_path): - """Returns an iterator of the Parameterized classes of a module to be included in a stub file""" spec = importlib.util.spec_from_file_location(module_name, module_path) foo = importlib.util.module_from_spec(spec) spec.loader.exec_module(foo) for value in foo.__dict__.values(): try: - if issubclass(value, param.Parameterized) and value.__module__==module_name: - yield(value) + 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__) - -MAP = { - param.Boolean: 'bool', - param.Color: 'str', - param.Integer: 'int', - param.Parameter: 'Any', - param.String: 'str', + 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,date,np.datetime64]", + param.Tuple: "tuple", + param.Range: "Tuple[Number,Number]", } + def _default_to_string(default): if default is None: - return 'None' + return "None" 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""" + """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)) + "]" + tpe = ( + "Union[" + + ",".join(sorted((item.__name__ for item in class_), key=str.casefold)) + + "]" + ) else: tpe = class_.__name__ elif isinstance(parameter, param.ObjectSelector): types = set(type(item).__name__ for item in parameter.objects) - if len(types)==0: + if len(types) == 0: tpe = "Any" - elif len(types)==1: + elif len(types) == 1: tpe = types.pop() else: tpe = "Union[" + ",".join(sorted(types, key=str.casefold)) + "]" @@ -61,47 +94,57 @@ def _to_typehint(parameter: param.Parameter) -> str: tpe = f"List[{class_.__name__}]" else: tpe = "list" - elif parameter.__class__ in MAP: - tpe = MAP[parameter.__class__] + elif parameter.__class__ in PARAMETER_TO_TYPE: + tpe = PARAMETER_TO_TYPE[parameter.__class__] else: raise NotImplementedError(parameter) - - if parameter.allow_None and not tpe=='Any': + + if parameter.allow_None and not tpe == "Any": tpe = f"Optional[{tpe}]" default_str = _default_to_string(parameter.default) - tpe +=f"={default_str}" + tpe += f"={default_str}" return tpe -def _to_type_hints(parameterized) -> dict: - """Returns a dictionary of parameter names and typehints""" + +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] + parameter = parameterized.param[parameter_name] if not parameter_name.startswith("_"): typehint = _to_typehint(parameter) - typed_attributes[parameter]=typehint + typed_attributes[parameter] = typehint return typed_attributes -def _to_typed_attributes(parameterized): + +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 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): + +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()): @@ -111,18 +154,20 @@ def _sorted_parameter_names(parameterized): 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): + +def _to_init(parameterized: Type[param.Parameterized]) -> str: """Returns the __init__ signature with typed arguments""" typed_attributes = "" - type_hints=_to_type_hints(parameterized) + type_hints = _to_type_hints(parameterized) for parameter in _sorted_parameters(parameterized): if not parameter in type_hints: continue - typehint=type_hints[parameter] + typehint = type_hints[parameter] if typed_attributes: typed_attributes += "\n" typed_attributes += f" {parameter.name}: {typehint}," @@ -131,7 +176,9 @@ def __init__(self, {typed_attributes} ):""" -ansi_escape = re.compile(r''' + +ANSI_ESCAPE = re.compile( + r""" \x1B # ESC (?: # 7-bit C1 Fe (except CSI) [@-Z\\-_] @@ -141,18 +188,54 @@ def __init__(self, [ -/]* # Intermediate bytes [@-~] # Final byte ) -''', re.VERBOSE) +""", + 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): +def _get_original_docstring(parameterized: Type[param.Parameterized]) -> str: """Returns the original docstring of a Parameterized class""" - doc=ansi_escape.sub('', parameterized.__doc__) - doc2=doc[doc.find("\n")+1:] - doc3=doc2[:doc2.find("\nParameters of \'")] - return doc3 + 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): +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 @@ -164,30 +247,51 @@ def _get_args(parameterized): if not name.startswith("_"): if args: args += "\n" - doc = parameter.doc.lstrip('\n').lstrip() + 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) + 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 to_stub(parameterized): + +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) + 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} +{typed_parameters} -{_init} - """{original_doc}""" +{init} + """{original_doc} Args: {args} +""" ''' - \ No newline at end of file + +def module_to_stub(module: ModuleType) -> str: + stub = "" + for parameterized in _get_parameterized_classes(module): + stub += "\n\n" + parameterized_to_stub(parameterized) + return stub + + diff --git a/script.py b/script.py index 036245cde8..f9d1174330 100644 --- a/script.py +++ b/script.py @@ -1,7 +1,3 @@ import panel as pn -islider = pn.widgets.IntSlider() - - - - +pn.extension(sizing_mode="stretch_width", template="fast") \ No newline at end of file diff --git a/test_param_stubgen.py b/test_param_stubgen.py index 9543a7d50a..3424b72769 100644 --- a/test_param_stubgen.py +++ b/test_param_stubgen.py @@ -1,21 +1,31 @@ -from param_stubgen import ( - _get_parameterized_classes, - _to_bases, - _to_class_name, - to_stub, - _to_typed_attributes, - _to_typehint, - _default_to_string, - _to_init, - _sorted_parameter_names, -) -from panel.widgets import slider -import pytest +import datetime +import logging + +import numpy as np import param +import pytest + +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, module_to_stub, + parameterized_to_stub) +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.__name__, slider.__file__) + result = _get_parameterized_classes(slider) result_set = set(item.__name__ for item in result) assert result_set == { "_SliderBase", @@ -92,6 +102,10 @@ def test_default_to_string(parameter, expected): 'Optional[Union[int,str]]="test"', ), (slider.Widget.param.sizing_mode, "Union[NoneType,str]=None"), + (param.Number(), 'Number=0.0'), + (param.Date(), 'Optional[Union[datetime,date,np.datetime64]]=None'), + (param.Tuple(), "tuple=(0, 0)"), + (param.Range(), "Optional[Tuple[Number,Number]]=None"), ], ) def test_to_type_hint(parameter, typehint): @@ -110,15 +124,12 @@ def test_to_typed_attributes(): 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(): - class Parent(param.Parameterized): - a = param.Parameter() - c = param.Parameter() - - class Child(Parent): - b = param.Parameter() +def test_sort_parameters(): actual = _sorted_parameter_names(Child) assert actual==['b', 'a', 'c', 'name'] @@ -161,83 +172,37 @@ def __init__(self, assert len(actual) == len(expected) assert actual == expected +def test_to_stub_intslider_without_exceptions(): + assert parameterized_to_stub(slider.IntSlider) -def test_to_stub(): - parameterized = slider.IntSlider - stub = '''\ -class IntSlider(ContinuousSlider): - value: int=0 - start: int=0 - end: int=1 - step: int=1 - value_throttled: Optional[int]=None +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, - 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", + b: list=[], + a: str="", + c: int=0, + name: Optional[str]="Child", ): - """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. + """The Child class provides ... + + Args: + a: A string parameter + b: A list parameter + c: An int parameter 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 on mouse up - 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. -""" +""" ''' - assert to_stub(parameterized) == stub + assert parameterized_to_stub(Child) == expected + + +def test_module_to_stub_without_exceptions(): + module_to_stub(slider) + +print(module_to_stub(slider)) \ No newline at end of file From c15d81b421f9157766b4d70e43cb7d5d99810d7e Mon Sep 17 00:00:00 2001 From: Marc Skov Madsen Date: Wed, 26 Jan 2022 04:10:33 +0100 Subject: [PATCH 5/6] can create stubs for all Pararemeterized classes in Panel --- panel/widgets/slider.pyi | 7 +++++ param_stubgen.py | 62 ++++++++++++++++++++++++++++++++++------ script.py | 12 +++++++- test_param_stubgen.py | 49 ++++++++++++++++++++++++------- 4 files changed, 110 insertions(+), 20 deletions(-) diff --git a/panel/widgets/slider.pyi b/panel/widgets/slider.pyi index 2993c6b150..f712496020 100644 --- a/panel/widgets/slider.pyi +++ b/panel/widgets/slider.pyi @@ -365,6 +365,13 @@ 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 diff --git a/param_stubgen.py b/param_stubgen.py index 4690a917f8..7f2fd87d36 100644 --- a/param_stubgen.py +++ b/param_stubgen.py @@ -1,12 +1,18 @@ +import glob +import importlib import importlib.util -from types import ModuleType import inspect - -import param +import logging +import os +import pathlib import re +from types import ModuleType from typing import Type +import datetime -import logging +import param + +import panel as pn logger = logging.getLogger("param_stubgen") @@ -18,7 +24,12 @@ def _get_parameterized_classes(mod: ModuleType): spec = importlib.util.spec_from_file_location(module_name, module_path) foo = importlib.util.module_from_spec(spec) - spec.loader.exec_module(foo) + 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: @@ -46,15 +57,25 @@ def _to_bases(parameterized): param.Parameter: "Any", param.String: "str", param.Number: "Number", - param.Date: "Union[datetime,date,np.datetime64]", + 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: @@ -78,7 +99,7 @@ def _to_typehint(parameter: param.Parameter) -> str: ) else: tpe = class_.__name__ - elif isinstance(parameter, param.ObjectSelector): + elif isinstance(parameter, (param.ObjectSelector, param.Selector)): types = set(type(item).__name__ for item in parameter.objects) if len(types) == 0: tpe = "Any" @@ -97,7 +118,12 @@ def _to_typehint(parameter: param.Parameter) -> str: elif parameter.__class__ in PARAMETER_TO_TYPE: tpe = PARAMETER_TO_TYPE[parameter.__class__] else: - raise NotImplementedError(parameter) + 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}]" @@ -193,6 +219,7 @@ def __init__(self, ) import ast + # fname = "panel/widgets/slider.py" # with open(fname, 'r') as f: # tree = ast.parse(f.read()) @@ -263,7 +290,7 @@ def _get_args(parameterized: Type[param.Parameterized]) -> str: 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}" + args += f" {name}: {doc}" return args @@ -294,4 +321,21 @@ def module_to_stub(module: ModuleType) -> str: 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 index f9d1174330..f9194708c0 100644 --- a/script.py +++ b/script.py @@ -1,3 +1,13 @@ import panel as pn -pn.extension(sizing_mode="stretch_width", template="fast") \ No newline at end of file +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 index 3424b72769..5096bbafa7 100644 --- a/test_param_stubgen.py +++ b/test_param_stubgen.py @@ -1,16 +1,19 @@ 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, module_to_stub, - parameterized_to_stub) + _to_typed_attributes, _to_typehint, get_modules, + module_to_stub, parameterized_to_stub, to_module) FORMAT = '%(asctime)s %(message)s' logging.basicConfig(format=FORMAT) @@ -61,6 +64,7 @@ def test_to_bases(): (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): @@ -103,9 +107,23 @@ def test_default_to_string(parameter, expected): ), (slider.Widget.param.sizing_mode, "Union[NoneType,str]=None"), (param.Number(), 'Number=0.0'), - (param.Date(), 'Optional[Union[datetime,date,np.datetime64]]=None'), + (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): @@ -193,16 +211,27 @@ def __init__(self, """The Child class provides ... Args: - a: A string parameter - b: A list parameter - c: An int parameter - name: String identifier for this object. + 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_module_to_stub_without_exceptions(): - module_to_stub(slider) +def test_to_module(): + to_module(path="", parent="pathlib.Path(pn.__file__).parent") -print(module_to_stub(slider)) \ No newline at end of file +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) From 95aac351ddcb53d6d9249e2885533b372f113b05 Mon Sep 17 00:00:00 2001 From: Marc Skov Madsen Date: Fri, 28 Jan 2022 20:37:09 +0100 Subject: [PATCH 6/6] work in progress --- panel/__init__.py | 122 ++++++++++---------- panel/widgets/slider.py | 242 ++++++++++++++++++++++++++++------------ 2 files changed, 231 insertions(+), 133 deletions(-) diff --git a/panel/__init__.py b/panel/__init__.py index 14fed0fc2c..f0cfce106d 100644 --- a/panel/__init__.py +++ b/panel/__init__.py @@ -1,94 +1,96 @@ -"""# Panel is a high level app and dashboarding framework +""" +Panel is a high level app and dashboarding framework +==================================================== -Works with the tools you know and ❤️. Welcome to our community 👪 +Works with the tools you know and ❤️. -- Get Started: https://panel.holoviz.org/ -- Join the Community: https://discourse.holoviz.org/ -- Contribute: https://github.com/holoviz/panel +`Getting Started`_ \| `Discourse`_ \| `Github`_ \| `Twitter`_ \| +`LinkedIn`_ -Follow us on [Twitter](https://twitter.com/Panel_org) or [LinkedIn](https://www.linkedin.com/company/79754450). -¨ +Interactive models with ``.bind`` +--------------------------------- -## 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](https://user-images.githubusercontent.com/42288570/150686594-21b03e55-79ef-406b-9e61-1764c6b493c3.gif) + Interactive Model App -You can use Panels `.bind` to bind your models to widgets. +You can use Panels ``.bind`` to bind your models to widgets. -```python -import panel as pn +.. code:: python -color = "#C43F66" + import panel as pn -pn.extension(sizing_mode="stretch_width", template="fast") + 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") + def model(a, b, emoji): + result = "#" + (emoji * a) + " + " + (emoji * b) + " = " + (emoji * (a + b)) + return result -interactive_add = pn.bind(model, a=input1, b=input2, emoji="⭐") -pn.panel(interactive_add).servable(area="main", title="My interactive MODEL") + 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") -pn.state.template.param.update(site="Panel", accent_base_color=color, header_background=color) -``` + interactive_add = pn.bind(model, a=input1, b=input2, emoji="⭐") + pn.panel(interactive_add).servable(area="main", title="My interactive MODEL") -You can serve your app via + pn.state.template.param.update(site="Panel", accent_base_color=color, header_background=color) -```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 -``` +You can serve your app via -The file can be a `.py` script or `.ipynb` notebook. +.. code:: bash -Try changing the return value of the function. Panel will magically ✨ understand how to show the -objects you know and ❤️. + $ 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 -This includes -[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) and \ -[Plotly](https://panel.holoviz.org/reference/panes/Plotly.html#panes-gallery-plotly) figures. +The file can be a ``.py`` script or ``.ipynb`` notebook. -## Interactive dataframes with `.interactive` +Try changing the return value of the function. Panel will magically ✨ +understand how to show the objects you know and ❤️. -![Interactive DataFrame App](https://user-images.githubusercontent.com/42288570/150683991-9cece6a1-3751-42d2-8256-505f5deb12be.gif) +| This includes `Bokeh`_, +| `HoloViews`_, +| `Matplotlib`_ and +| `Plotly`_ figures. -You can use [hvplot .interactive](https://hvplot.holoviz.org/user_guide/Interactive.html) to make -your dataframes interactive. +Interactive dataframes with ``.interactive`` +-------------------------------------------- -```python -import panel as pn -import pandas as pd -import hvplot.pandas +.. figure:: https://user-images.githubusercontent.com/42288570/150683991-9cece6a1-3751-42d2-8256-505f5deb12be.gif + :alt: Interactive DataFrame App -color = "#0072B5" -df = pd.DataFrame(data={"x": [0, 1, 2, 3, 4], "y": [0, 2, 1, 3, 4]}) + Interactive DataFrame App -pn.extension(sizing_mode="stretch_width", template="fast") +You can use `hvplot .interactive`_ to make your dataframes interactive. -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") +\```python import panel as pn import pandas as pd import hvplot.pandas -interactive_df = df.interactive().head(n=count) -pn.panel(interactive_df.panel(height=200)).servable(area="main") +color = “#0072B5” df = pd.DataFrame(data={“x”: [0, 1, 2, 3, 4], “y”: [0, +2, 1, 3, 4]}) -interactive_plot = interactive_df.hvplot(x="x", y="y").opts(line_width=6, color=color) -pn.panel(interactive_plot.panel(height=200)).servable(area="main", title="My interactive DATAFRAME") +pn.extension(sizing_mode=“stretch_width”, template=“fast”) -pn.state.template.param.update(site="Panel", accent_base_color=color, header_background=color) -``` +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”) -The DataFrames can be any of Pandas, Dask, CuDF and Xarray dataframes. +interactive_df = df.interactive().head -For more about hvplot `.interactive` check out this [blog post](https://towardsdatascience.com/the-easiest-way-to-create-an-interactive-dashboard-in-python-77440f2511d1). +.. _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 diff --git a/panel/widgets/slider.py b/panel/widgets/slider.py index 2bbe5ea384..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,42 +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): - """The IntSlider widget allows selecting selecting an integer value within a set bounds +class IntSlider(_ContinuousSlider): + """The IntSlider widget allows selecting an integer value within a set of bounds using a slider. - See https://panel.holoviz.org/reference/widgets/IntSlider.html + Reference: https://panel.holoviz.org/reference/widgets/IntSlider.html """ value = param.Integer(default=0, doc=""" - The value of the widget. Updated when the slider is dragged""") + The selected integer value of the slider. Updated when the handle is dragged.""") start = param.Integer(default=0, doc=""" - The lower bound""") + The lower bound.""") end = param.Integer(default=1, doc=""" - The upper bound""") + The upper bound.""") step = param.Integer(default=1, doc=""" - The step size""") + The step size.""") value_throttled = param.Integer(default=None, constant=True, doc=""" - The value of the widget. Updated on mouse up""") + The value of the slider. Updated when the handle is released""") _rename = {'name': 'title'} @@ -171,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'} @@ -201,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} @@ -350,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] @@ -358,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 @@ -393,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, 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""") - step = param.Number(default=0.1) + 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} @@ -427,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) @@ -446,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} @@ -583,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 @@ -596,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.""") @@ -612,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