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

Allow interact to use basic type hint annotations #3908

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 111 additions & 4 deletions docs/source/examples/Using Interact.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@
"cell_type": "code",
"execution_count": null,
"metadata": {
"tags": ["remove-cell"]
"tags": [
"remove-cell"
]
},
"outputs": [],
"source": [
Expand Down Expand Up @@ -353,6 +355,111 @@
"interact(f, x=widgets.Combobox(options=[\"Chicago\", \"New York\", \"Washington\"], value=\"Chicago\"));"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Type Annotations"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"If the function that you are using with interact uses type annotations, `interact` may be able to use those to determine what UI components to use in the auto-generated UI. For example, given a function with an argument annotated with type `float`"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def f(x: float):\n",
" return x"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"then `interact` will create a UI with a `FloatText` component without needing to be passed any values or abbreviations."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"interact(f);"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The following table gives an overview of different annotation types, and how they map to interactive controls:\n",
"\n",
"<table class=\"table table-condensed table-bordered\">\n",
" <tr><td><strong>Type Annotation</strong></td><td><strong>Widget</strong></td></tr> \n",
" <tr><td>`bool`</td><td>Checkbox</td></tr> \n",
" <tr><td>`str`</td><td>Text</td></tr>\n",
" <tr><td>`int`</td><td>IntText</td></tr>\n",
" <tr><td>`float`</td><td>FloatText</td></tr>\n",
" <tr><td>`Enum` subclasses</td><td>Dropdown</td></tr>\n",
"</table>\n",
"\n",
"Other type annotations are ignored."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"If values or abbreviations are passed to the `interact` function, those will override any type annotations when determining what widgets to create."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Parameters which are annotationed with an `Enum` subclass will have a dropdown created whose labels are the names of the enumeration and which pass the corresponding values to the function parameter."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from enum import Enum\n",
"\n",
"class Color(Enum):\n",
" red = 0\n",
" green = 1\n",
" blue = 2\n",
"\n",
"def h(color: Color):\n",
" return color"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"When `interact` is used with the function `h`, the Dropdown widget it creates will have options `\"red\"`, `\"green\"` and `\"blue\"` and the values passed to the function will be, correspondingly, `Color.red`, `Color.green` and `Color.blue`."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"interact(h);"
]
},
{
"cell_type": "markdown",
"metadata": {},
Expand Down Expand Up @@ -715,7 +822,7 @@
},
{
"cell_type": "code",
"execution_count": 1,
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
Expand Down Expand Up @@ -762,9 +869,9 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.10.5"
"version": "3.12.2"
}
},
"nbformat": 4,
"nbformat_minor": 2
"nbformat_minor": 4
}
29 changes: 27 additions & 2 deletions python/ipywidgets/ipywidgets/widgets/interaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,16 @@
"""Interact with functions using widgets."""

from collections.abc import Iterable, Mapping
from enum import EnumMeta as EnumType
from inspect import signature, Parameter
from inspect import getcallargs
from inspect import getfullargspec as check_argspec
import sys

from IPython import get_ipython
from . import (Widget, ValueWidget, Text,
FloatSlider, IntSlider, Checkbox, Dropdown,
VBox, Button, DOMWidget, Output)
FloatSlider, FloatText, IntSlider, IntText, Checkbox,
Dropdown, VBox, Button, DOMWidget, Output)
from IPython.display import display, clear_output
from traitlets import HasTraits, Any, Unicode, observe
from numbers import Real, Integral
Expand Down Expand Up @@ -125,6 +126,8 @@ def _yield_abbreviations_for_parameter(param, kwargs):
value = kwargs.pop(name)
elif default is not empty:
value = default
elif param.annotation:
value = param.annotation
else:
yield not_found
yield (name, value, default)
Expand Down Expand Up @@ -304,6 +307,12 @@ def widget_from_abbrev(cls, abbrev, default=empty):
# ignore failure to set default
pass
return widget

# Try type annotation
if isinstance(abbrev, type):
widget = cls.widget_from_annotation(abbrev)
if widget is not None:
return widget

# Try single value
widget = cls.widget_from_single_value(abbrev)
Expand Down Expand Up @@ -341,6 +350,22 @@ def widget_from_single_value(o):
else:
return None

@staticmethod
def widget_from_annotation(t):
"""Make widgets from type annotation and optional default value."""
if t is str:
return Text()
elif t is bool:
return Checkbox()
elif t in {int, Integral}:
return IntText()
elif t in {float, Real}:
return FloatText()
elif isinstance(t, EnumType):
return Dropdown(options={option.name: option for option in t})
else:
return None

@staticmethod
def widget_from_tuple(o):
"""Make widgets from a tuple abbreviation."""
Expand Down
36 changes: 36 additions & 0 deletions python/ipywidgets/ipywidgets/widgets/tests/test_interaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from unittest.mock import patch

import os
from enum import Enum
from collections import OrderedDict
import pytest

Expand All @@ -22,6 +23,17 @@
def f(**kwargs):
pass


class Color(Enum):
red = 0
green = 1
blue = 2


def g(a: str, b: bool, c: int, d: float, e: Color) -> None:
pass


displayed = []
@pytest.fixture()
def clear_display():
Expand Down Expand Up @@ -622,3 +634,27 @@ def test_state_schema():
with open(os.path.join(os.path.dirname(os.path.realpath(__file__)), '../../', 'state.schema.json')) as f:
schema = json.load(f)
jsonschema.validate(state, schema)

def test_type_hints():
c = interactive(g)

assert len(c.children) == 6

check_widget_children(
c,
a={'cls': widgets.Text},
b={'cls': widgets.Checkbox},
c={'cls': widgets.IntText},
d={'cls': widgets.FloatText},
e={
'cls': widgets.Dropdown,
'options': {
'red': Color.red,
'green': Color.green,
'blue': Color.blue,
},
'_options_labels': ("red", "green", "blue"),
'_options_values': (Color.red, Color.green, Color.blue),
},
)

Loading