Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ExamplePicker #7411

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 110 additions & 0 deletions examples/reference/widgets/example.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
```python
import panel as pn

pn.extension()
```

The `Example` widget enables you to present a single example to the user to pick. When clicked
one or more `targets` will be updated or called.

#### Parameters

For details on other options for customizing the component see the [layout](../../how_to/layout/index.md) and [styling](../../how_to/styling/index.md) how-to guides.

##### Core

* **`value`** (str | iterable[Any]): The example value. Either a single value or an iterable of values to assign to a single or an iterable of `targets` when updated or triggered.
* **`name`** (str): An Optional name. Will be used as the Button label if not "". Default is "".
* **`thumbnail`** (str | Path): An Optional thumbnail image. A local file will be embedded as a *data url*. Default is "".
* **`targets`** (object | list[object] | None): An Optional object or list of objects. The individual object can be a class with a `.value` or `.object` attribute - for example a widget or pane. Can also be a callable taking a single argument. Will be updated or called when the `value` is updated or triggered. Default is None.

##### Display

* **`layout`** (ListPanel or None): An optional list like layout. If None the `Example` will be layout as a Button. If `Row` as a row with a Button and the value items. Default is None.
* **`button_kwargs`** (dict): An optional dictionary of `Button` keyword arguments like `button_type` or `height`.

## Basic Usage

You can create an `Example` to pick examples for a single target:

```python
widget = pn.widgets.TextInput(name="Selection")
example = pn.widgets.Example("Hello", targets=widget)
pn.Column(example, widget).servable()
```

You can give the `Example` a specific `name`:

```python
widget = pn.widgets.TextInput(name="Selection")
example = pn.widgets.Example("World", name="Hello", targets=widget)
pn.Column(example, widget).servable()
```

Or a `thumbnail`:

```python
widget = pn.widgets.TextInput(name="Selection")
example = pn.widgets.Example("Hello World", thumbnail="https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcSLZf31OpU0zqzpDS-IwNBp7lF1eejh9YJHHA&s", targets=widget)
pn.Column(example, widget).servable()
```

You can also provide provide your own custom `button_kwargs`:

```python
widget = pn.widgets.TextInput(name="Selection")
example = pn.widgets.Example("Hello", targets=widget, button_kwargs=dict(button_type="primary", button_style="outline", height=50, width=50))
pn.Column(example, widget).servable()
```

## Multiple Targets

You can create an `Example` to pick examples for multiple targets:

```python
text_input = pn.widgets.TextInput(name="Text Input")
audio_output = pn.pane.Audio(name="Audio Output")
def update_name(name, text_input=text_input):
text_input.name=name

example = pn.widgets.Example(("Beat Boxing", "https://assets.holoviz.org/panel/samples/beatboxing.mp3", "text input"), targets=[text_input, audio_output, update_name], name="beatboxing.mp3")
pn.Column(example, pn.Row(text_input, audio_output)).servable()
```

You can change the `layout` to `Row` to see the `Example` values:

```python
text_input = pn.widgets.TextInput(name="Text Input")
audio_output = pn.pane.Audio(name="Audio Output")
def update_name(name, text_input=text_input):
text_input.name=name

example = pn.widgets.Example(("Beat Boxing", "https://assets.holoviz.org/panel/samples/beatboxing.mp3", "text input"), targets=[text_input, audio_output, update_name], name="beatboxing.mp3", layout=pn.Row)
pn.Column(example, pn.Row(text_input, audio_output)).servable()
```

## Multiple Examples

You can create many examples:

```python
text_input = pn.widgets.TextInput(name="Text Input")
audio_output = pn.pane.Audio(name="Audio Output")
def update_name(name, text_input=text_input):
text_input.name=name

examples = [pn.widgets.Example((f"Beat Boxing {index}", "https://assets.holoviz.org/panel/samples/beatboxing.mp3", "text input"), name=f"Beat Boxing {index}", targets=[text_input, audio_output, update_name], layout=None) for index in range(0,100)]
pn.Column(pn.Column(pn.FlexBox(*examples), height=250, scroll=True), pn.Row(text_input, audio_output),).servable()
```

For efficient columnar layout of `Example` rows use the `Feed`:

```python
text_input = pn.widgets.TextInput(name="Text Input")
audio_output = pn.pane.Audio(name="Audio Output")
def update_name(name, text_input=text_input):
text_input.name=name

examples = [pn.widgets.Example((f"Beat Boxing {index}", "https://assets.holoviz.org/panel/samples/beatboxing.mp3", "text input"), name=f"Beat Boxing {index}", targets=[text_input, audio_output, update_name], layout=pn.Row) for index in range(0,100)]
pn.Column(pn.Feed(*examples, height=250, scroll=True, load_buffer=200), pn.Row(text_input, audio_output),).servable()
```
71 changes: 71 additions & 0 deletions examples/reference/widgets/examples.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
```python
import panel as pn

pn.extension()
```

## Basic Usage

You can create an `Examples` widget to pick examples for a single target:

```python
widget = pn.widgets.TextInput(name="Selection")
examples = pn.widgets.Examples("Hello", "World", targets=widget)
pn.Column(examples, widget).servable()
```

The `Examples` widget is *list like*, i.e. you can work with it as a list:

```python
pn.Row(examples[1], examples[0]).servable()
```

To gain more control over the look of the examples you can provide `Example` values:

```python
from panel.widgets import Example

widget = pn.widgets.TextInput(name="Selection")
examples = pn.widgets.Examples(Example("Hello", "HELLO", button_kwargs={"button_style": "outline"}), Example("Hello World", "", "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcSLZf31OpU0zqzpDS-IwNBp7lF1eejh9YJHHA&s"), targets=widget)
pn.Column(examples, widget).servable()
```

## Multiple Targets

You can create an `Examples` widget to pick examples for multiple targets:

```python
text_input = pn.widgets.TextInput(name="Text Input")
audio_output = pn.pane.Audio(name="Audio Output")
def update_name(name, text_input=text_input):
text_input.name=name

examples = pn.widgets.Examples(("Stanford", "https://ccrma.stanford.edu/~jos/mp3/pno-cs.mp3", "TEXT INPUT"), ("Beat Boxing", "https://assets.holoviz.org/panel/samples/beatboxing.mp3", "text input"), targets=[text_input, audio_output, update_name], name="🎵 Audio Examples")
pn.FlexBox(examples, text_input, audio_output).servable()
```

Please note targets can be widgets, panes and functions.

If your want to see the values as well as the buttons you can provide a `layout`:

```python
text_input = pn.widgets.TextInput(name="Text Input")
audio_output = pn.pane.Audio(name="Audio Output")
def update_name(name, text_input=text_input):
text_input.name=name

examples = pn.widgets.Examples(("Stanford", "https://ccrma.stanford.edu/~jos/mp3/pno-cs.mp3", "TEXT INPUT"), ("Beat Boxing", "https://assets.holoviz.org/panel/samples/beatboxing.mp3", "text input"), targets=[text_input, audio_output, update_name], name="🎵 Audio Examples", layout=pn.Row)
pn.FlexBox(examples, text_input, audio_output).servable()
```

```python
text_input = pn.widgets.TextInput(name="Text Input")
audio_output = pn.pane.Audio(name="Audio Output")
def update_name(name, text_input=text_input):
text_input.name=name

samples = [pn.widgets.Example((f"Beat Boxing {index}", "https://assets.holoviz.org/panel/samples/beatboxing.mp3", "text input"), name=f"Beat Boxing {index}", targets=[text_input, audio_output, update_name], layout=None) for index in range(0,100)]
# Todo: support height and scroll parameters
examples = pn.widgets.Examples(*samples, targets=[text_input, audio_output, update_name], name="🎵 Audio Examples", layout=pn.Row)
pn.FlexBox(examples, text_input, audio_output).servable()
```
Binary file added panel/tests/assets/example-image.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
152 changes: 152 additions & 0 deletions panel/tests/widgets/test_example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
"""Test the `Example`

The `Example` can be used like a Button to provide the example `value` to a target or a list of targets.
"""
from pathlib import Path

import pandas as pd
import pytest

import panel as pn

from panel.widgets import Example

EXAMPLE_IMAGE = (Path(__file__).parent.parent/"assets"/"example-image.png").absolute()

# value, name, thumbnail

def test_create():
example = Example(value="example")
assert example.value=="example"
layout = example._get_layout()
assert isinstance(layout, pn.widgets.Button)
assert layout.name==example.name

def test_create_row_layout():
example = Example(value=1, layout=pn.Row)
layout = example._get_layout()
assert isinstance(layout, pn.Row)
assert isinstance(layout[0], pn.widgets.Button)
assert layout[0].name=="1"


IMAGE = ""

class ClassWithNameAttribute():
def __init__(self, name):
self.name = name

object_with_name_attribute = ClassWithNameAttribute("my-name")
empty_dataframe = pd.DataFrame()
empty_series = pd.Series()

TEST_EXAMPLES = [
("https://ccrma.stanford.edu/~jos/mp3/pno-cs.mp3", Example("https://ccrma.stanford.edu/~jos/mp3/pno-cs.mp3", "pno-cs.mp3", "")),
("https://example.com/image.png", Example("https://example.com/image.png", "image.png", "https://example.com/image.png")),
("image.png", Example("image.png", "image.png", "image.png")),
("some-text", Example("some-text", "some-text", "")),
({"a": "b"}, Example({"a": "b"}, "Example", "")),
(["a", "b"], Example(["a", "b"], "Example", "")),
(1, Example(1, "1", "")),
(1.2, Example(1.2, "1.2", "")),
(empty_dataframe, Example(empty_dataframe, "Example", "")),
(empty_series, Example(empty_series, "Example", "")),
(None, Example(None, "None", "")),
(object_with_name_attribute, Example(object_with_name_attribute, "my-name", "")),
]

def test_instantiation_with_no_value():
with pytest.raises(TypeError):
Example()

@pytest.mark.parametrize(["value", "expected"], TEST_EXAMPLES)
def test_instantiation_from_value(value, expected):
example = Example(value)
assert example==expected
assert example.to_dict()
assert repr(example)
example.layout=None
assert example.__panel__()
assert isinstance(example._get_layout(), pn.widgets.Button)
example.layout=pn.Row
assert example.__panel__()
assert isinstance(example._get_layout(), pn.Row)


def test_can_set_single_target():
widget = pn.widgets.TextInput()
example = Example("example1", "example2", targets=widget)
example.param.trigger("click")
assert widget.value == example.value

def test_can_set_multiple_targets():
widget1 = pn.widgets.TextInput()
widget2 = pn.widgets.TextInput()
targets = [widget1, widget2]
example = Example(("a", "b"), targets=targets)
example.param.trigger("click")
assert widget1.value == example.value[0]
assert widget2.value == example.value[1]


@pytest.mark.parametrize("name, expected_name, start_index, expected_index", [
("", "Example 1", 1, 2), # Empty name should be updated
("example", "Example 1", 1, 2), # Name "example" should be updated (case insensitive)
("Example", "Example 1", 1, 2), # Name "Example" should be updated
("test", "test", 1, 1), # Name "test" should not be updated
])
def test_clean_name(name, expected_name, start_index, expected_index):
example = Example(value="", name=name)

result_index = example._clean_name(start_index)

# Check if the index is correctly updated
assert result_index == expected_index
# Check if the name is updated as expected
assert example.name == expected_name

def test_can_layout_as_button():
example = Example("example", layout=None)
assert isinstance(example._get_layout(), pn.widgets.Button)

def test_can_layout_as_row():
example = Example("example", layout=pn.Row)
assert isinstance(example._get_layout(), pn.Row)

def test_button_kwargs():
example = Example("example", button_kwargs=dict(width=101), layout=None)
assert example._button.width==101

@pytest.mark.parametrize("value", [str(EXAMPLE_IMAGE), EXAMPLE_IMAGE])
def test_local_image_as_value(value):
example = Example(value)
assert example.value==value
assert example.name=="example-image.png"
assert example.thumbnail.startswith("data:image/png;base64,")

@pytest.mark.parametrize("thumbnail", [str(EXAMPLE_IMAGE), EXAMPLE_IMAGE])
def test_local_image_as_thumbnail(thumbnail):
example = Example("some-value", thumbnail=thumbnail)
assert example.value=="some-value"
assert example.name=="some-value"
assert example.thumbnail.startswith("data:image/png;base64,")

@pytest.mark.parametrize("value", ["http://panel.holoviz.org/_static/logo_horizontal_light_theme.png", "https://panel.holoviz.org/_static/logo_horizontal_light_theme.png"])
def test_url_image_as_value(value):
example = Example(value)
assert example.value==value
assert example.name=="logo_horizontal_light_theme.png"
assert example.thumbnail==value

@pytest.mark.parametrize("thumbnail", ["http://panel.holoviz.org/_static/logo_horizontal_light_theme.png", "https://panel.holoviz.org/_static/logo_horizontal_light_theme.png"])
def test_url_image_as_thumbnail(thumbnail):
example = Example("some-value", thumbnail=thumbnail)
assert example.value=="some-value"
assert example.name=="some-value"
assert example.thumbnail==thumbnail

def test_special_image():
example = Example("Hello World", thumbnail="https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcSLZf31OpU0zqzpDS-IwNBp7lF1eejh9YJHHA&s")
assert example.name=="Hello World"
assert example.value=="Hello World"
assert example.thumbnail=="https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcSLZf31OpU0zqzpDS-IwNBp7lF1eejh9YJHHA&s"
62 changes: 62 additions & 0 deletions panel/tests/widgets/test_examples.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
"""Test the Examples widget

The Examples widget can be used to pick Examples from a list of Examples and provide the example as
input to one or more callables.
"""
import pytest

import panel as pn

from panel.widgets import Examples
from panel.widgets.examples import Example

# value, name, thumbnail

def test_examples_init():
examples = []
examples = Examples(*examples)
assert list(examples)==[]
assert examples.__panel__()

def test_examples_init_from_list_of_examples():
samples = [Example("example1"), Example("example2")]
examples = Examples(*samples)
assert list(examples)==samples
assert examples.__panel__()

def test_examples_init_from_non_examples():
samples = ["example1", "example2"]
examples = Examples(*samples)
assert list(examples)==[Example(sample) for sample in samples]
assert examples.__panel__()

def test_can_watch():
examples = Examples("example1", "example2")
examples[0].param.trigger("click")
assert examples.value == examples[0].value
examples[1].param.trigger("click")
assert examples.value == examples[1].value

def test_can_select():
widget = pn.widgets.TextInput()
examples = Examples("example1", "example2", targets=widget)
examples[0].param.trigger("click")
assert examples.value == examples[0].value
examples[1].param.trigger("click")
assert examples.value == examples[1].value

@pytest.mark.parametrize("name, expected_name, start_index, expected_index", [
("", "Example 1", 1, 2), # Empty name should be updated
("example", "Example 1", 1, 2), # Name "example" should be updated (case insensitive)
("Example", "Example 1", 1, 2), # Name "Example" should be updated
("test", "test", 1, 1), # Name "test" should not be updated
])
def test_clean_name(name, expected_name, start_index, expected_index):
example = Example(value="", name=name)

result_index = example._clean_name(start_index)

# Check if the index is correctly updated
assert result_index == expected_index
# Check if the name is updated as expected
assert example.name == expected_name
Loading
Loading