Skip to content

Commit

Permalink
Allow interact to use basic type hint annotations (#3908)
Browse files Browse the repository at this point in the history
* Add basic implementation of type-hint-based `interact`

* Add tests for type annotated interact.

* Update interact documentation to discuss type annotations.

* Use EnumMeta instead of EnumType for backwards compatibility.
  • Loading branch information
corranwebster authored Aug 21, 2024
1 parent 188abff commit 0aa1efb
Show file tree
Hide file tree
Showing 3 changed files with 174 additions and 6 deletions.
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),
},
)

0 comments on commit 0aa1efb

Please sign in to comment.