Skip to content

Commit 0aa1efb

Browse files
Allow interact to use basic type hint annotations (#3908)
* 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.
1 parent 188abff commit 0aa1efb

File tree

3 files changed

+174
-6
lines changed

3 files changed

+174
-6
lines changed

docs/source/examples/Using Interact.ipynb

+111-4
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@
1818
"cell_type": "code",
1919
"execution_count": null,
2020
"metadata": {
21-
"tags": ["remove-cell"]
21+
"tags": [
22+
"remove-cell"
23+
]
2224
},
2325
"outputs": [],
2426
"source": [
@@ -353,6 +355,111 @@
353355
"interact(f, x=widgets.Combobox(options=[\"Chicago\", \"New York\", \"Washington\"], value=\"Chicago\"));"
354356
]
355357
},
358+
{
359+
"cell_type": "markdown",
360+
"metadata": {},
361+
"source": [
362+
"## Type Annotations"
363+
]
364+
},
365+
{
366+
"cell_type": "markdown",
367+
"metadata": {},
368+
"source": [
369+
"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`"
370+
]
371+
},
372+
{
373+
"cell_type": "code",
374+
"execution_count": null,
375+
"metadata": {},
376+
"outputs": [],
377+
"source": [
378+
"def f(x: float):\n",
379+
" return x"
380+
]
381+
},
382+
{
383+
"cell_type": "markdown",
384+
"metadata": {},
385+
"source": [
386+
"then `interact` will create a UI with a `FloatText` component without needing to be passed any values or abbreviations."
387+
]
388+
},
389+
{
390+
"cell_type": "code",
391+
"execution_count": null,
392+
"metadata": {},
393+
"outputs": [],
394+
"source": [
395+
"interact(f);"
396+
]
397+
},
398+
{
399+
"cell_type": "markdown",
400+
"metadata": {},
401+
"source": [
402+
"The following table gives an overview of different annotation types, and how they map to interactive controls:\n",
403+
"\n",
404+
"<table class=\"table table-condensed table-bordered\">\n",
405+
" <tr><td><strong>Type Annotation</strong></td><td><strong>Widget</strong></td></tr> \n",
406+
" <tr><td>`bool`</td><td>Checkbox</td></tr> \n",
407+
" <tr><td>`str`</td><td>Text</td></tr>\n",
408+
" <tr><td>`int`</td><td>IntText</td></tr>\n",
409+
" <tr><td>`float`</td><td>FloatText</td></tr>\n",
410+
" <tr><td>`Enum` subclasses</td><td>Dropdown</td></tr>\n",
411+
"</table>\n",
412+
"\n",
413+
"Other type annotations are ignored."
414+
]
415+
},
416+
{
417+
"cell_type": "markdown",
418+
"metadata": {},
419+
"source": [
420+
"If values or abbreviations are passed to the `interact` function, those will override any type annotations when determining what widgets to create."
421+
]
422+
},
423+
{
424+
"cell_type": "markdown",
425+
"metadata": {},
426+
"source": [
427+
"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."
428+
]
429+
},
430+
{
431+
"cell_type": "code",
432+
"execution_count": null,
433+
"metadata": {},
434+
"outputs": [],
435+
"source": [
436+
"from enum import Enum\n",
437+
"\n",
438+
"class Color(Enum):\n",
439+
" red = 0\n",
440+
" green = 1\n",
441+
" blue = 2\n",
442+
"\n",
443+
"def h(color: Color):\n",
444+
" return color"
445+
]
446+
},
447+
{
448+
"cell_type": "markdown",
449+
"metadata": {},
450+
"source": [
451+
"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`."
452+
]
453+
},
454+
{
455+
"cell_type": "code",
456+
"execution_count": null,
457+
"metadata": {},
458+
"outputs": [],
459+
"source": [
460+
"interact(h);"
461+
]
462+
},
356463
{
357464
"cell_type": "markdown",
358465
"metadata": {},
@@ -715,7 +822,7 @@
715822
},
716823
{
717824
"cell_type": "code",
718-
"execution_count": 1,
825+
"execution_count": null,
719826
"metadata": {},
720827
"outputs": [],
721828
"source": [
@@ -762,9 +869,9 @@
762869
"name": "python",
763870
"nbconvert_exporter": "python",
764871
"pygments_lexer": "ipython3",
765-
"version": "3.10.5"
872+
"version": "3.12.2"
766873
}
767874
},
768875
"nbformat": 4,
769-
"nbformat_minor": 2
876+
"nbformat_minor": 4
770877
}

python/ipywidgets/ipywidgets/widgets/interaction.py

+27-2
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,16 @@
44
"""Interact with functions using widgets."""
55

66
from collections.abc import Iterable, Mapping
7+
from enum import EnumMeta as EnumType
78
from inspect import signature, Parameter
89
from inspect import getcallargs
910
from inspect import getfullargspec as check_argspec
1011
import sys
1112

1213
from IPython import get_ipython
1314
from . import (Widget, ValueWidget, Text,
14-
FloatSlider, IntSlider, Checkbox, Dropdown,
15-
VBox, Button, DOMWidget, Output)
15+
FloatSlider, FloatText, IntSlider, IntText, Checkbox,
16+
Dropdown, VBox, Button, DOMWidget, Output)
1617
from IPython.display import display, clear_output
1718
from traitlets import HasTraits, Any, Unicode, observe
1819
from numbers import Real, Integral
@@ -125,6 +126,8 @@ def _yield_abbreviations_for_parameter(param, kwargs):
125126
value = kwargs.pop(name)
126127
elif default is not empty:
127128
value = default
129+
elif param.annotation:
130+
value = param.annotation
128131
else:
129132
yield not_found
130133
yield (name, value, default)
@@ -304,6 +307,12 @@ def widget_from_abbrev(cls, abbrev, default=empty):
304307
# ignore failure to set default
305308
pass
306309
return widget
310+
311+
# Try type annotation
312+
if isinstance(abbrev, type):
313+
widget = cls.widget_from_annotation(abbrev)
314+
if widget is not None:
315+
return widget
307316

308317
# Try single value
309318
widget = cls.widget_from_single_value(abbrev)
@@ -341,6 +350,22 @@ def widget_from_single_value(o):
341350
else:
342351
return None
343352

353+
@staticmethod
354+
def widget_from_annotation(t):
355+
"""Make widgets from type annotation and optional default value."""
356+
if t is str:
357+
return Text()
358+
elif t is bool:
359+
return Checkbox()
360+
elif t in {int, Integral}:
361+
return IntText()
362+
elif t in {float, Real}:
363+
return FloatText()
364+
elif isinstance(t, EnumType):
365+
return Dropdown(options={option.name: option for option in t})
366+
else:
367+
return None
368+
344369
@staticmethod
345370
def widget_from_tuple(o):
346371
"""Make widgets from a tuple abbreviation."""

python/ipywidgets/ipywidgets/widgets/tests/test_interaction.py

+36
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from unittest.mock import patch
77

88
import os
9+
from enum import Enum
910
from collections import OrderedDict
1011
import pytest
1112

@@ -22,6 +23,17 @@
2223
def f(**kwargs):
2324
pass
2425

26+
27+
class Color(Enum):
28+
red = 0
29+
green = 1
30+
blue = 2
31+
32+
33+
def g(a: str, b: bool, c: int, d: float, e: Color) -> None:
34+
pass
35+
36+
2537
displayed = []
2638
@pytest.fixture()
2739
def clear_display():
@@ -622,3 +634,27 @@ def test_state_schema():
622634
with open(os.path.join(os.path.dirname(os.path.realpath(__file__)), '../../', 'state.schema.json')) as f:
623635
schema = json.load(f)
624636
jsonschema.validate(state, schema)
637+
638+
def test_type_hints():
639+
c = interactive(g)
640+
641+
assert len(c.children) == 6
642+
643+
check_widget_children(
644+
c,
645+
a={'cls': widgets.Text},
646+
b={'cls': widgets.Checkbox},
647+
c={'cls': widgets.IntText},
648+
d={'cls': widgets.FloatText},
649+
e={
650+
'cls': widgets.Dropdown,
651+
'options': {
652+
'red': Color.red,
653+
'green': Color.green,
654+
'blue': Color.blue,
655+
},
656+
'_options_labels': ("red", "green", "blue"),
657+
'_options_values': (Color.red, Color.green, Color.blue),
658+
},
659+
)
660+

0 commit comments

Comments
 (0)