test_interaction.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732
  1. # Copyright (c) Jupyter Development Team.
  2. # Distributed under the terms of the Modified BSD License.
  3. """Test interact and interactive."""
  4. from __future__ import print_function
  5. try:
  6. from unittest.mock import patch
  7. except ImportError:
  8. from mock import patch
  9. import os
  10. from collections import OrderedDict
  11. import pytest
  12. import ipywidgets as widgets
  13. from traitlets import TraitError
  14. from ipywidgets import (interact, interact_manual, interactive,
  15. interaction, Output)
  16. from ipython_genutils.py3compat import annotate
  17. #-----------------------------------------------------------------------------
  18. # Utility stuff
  19. #-----------------------------------------------------------------------------
  20. from .utils import setup, teardown
  21. def f(**kwargs):
  22. pass
  23. displayed = []
  24. @pytest.fixture()
  25. def clear_display():
  26. global displayed
  27. displayed = []
  28. def record_display(*args):
  29. displayed.extend(args)
  30. #-----------------------------------------------------------------------------
  31. # Actual tests
  32. #-----------------------------------------------------------------------------
  33. def check_widget(w, **d):
  34. """Check a single widget against a dict"""
  35. for attr, expected in d.items():
  36. if attr == 'cls':
  37. assert w.__class__ is expected
  38. else:
  39. value = getattr(w, attr)
  40. assert value == expected, "%s.%s = %r != %r" % (w.__class__.__name__, attr, value, expected)
  41. # For numeric values, the types should match too
  42. if isinstance(value, (int, float)):
  43. tv = type(value)
  44. te = type(expected)
  45. assert tv is te, "type(%s.%s) = %r != %r" % (w.__class__.__name__, attr, tv, te)
  46. def check_widgets(container, **to_check):
  47. """Check that widgets are created as expected"""
  48. # build a widget dictionary, so it matches
  49. widgets = {}
  50. for w in container.children:
  51. if not isinstance(w, Output):
  52. widgets[w.description] = w
  53. for key, d in to_check.items():
  54. assert key in widgets
  55. check_widget(widgets[key], **d)
  56. def test_single_value_string():
  57. a = u'hello'
  58. c = interactive(f, a=a)
  59. w = c.children[0]
  60. check_widget(w,
  61. cls=widgets.Text,
  62. description='a',
  63. value=a,
  64. )
  65. def test_single_value_bool():
  66. for a in (True, False):
  67. c = interactive(f, a=a)
  68. w = c.children[0]
  69. check_widget(w,
  70. cls=widgets.Checkbox,
  71. description='a',
  72. value=a,
  73. )
  74. def test_single_value_float():
  75. for a in (2.25, 1.0, -3.5, 0.0):
  76. if not a:
  77. expected_min = 0.0
  78. expected_max = 1.0
  79. elif a > 0:
  80. expected_min = -a
  81. expected_max = 3*a
  82. else:
  83. expected_min = 3*a
  84. expected_max = -a
  85. c = interactive(f, a=a)
  86. w = c.children[0]
  87. check_widget(w,
  88. cls=widgets.FloatSlider,
  89. description='a',
  90. value=a,
  91. min=expected_min,
  92. max=expected_max,
  93. step=0.1,
  94. readout=True,
  95. )
  96. def test_single_value_int():
  97. for a in (1, 5, -3, 0):
  98. if not a:
  99. expected_min = 0
  100. expected_max = 1
  101. elif a > 0:
  102. expected_min = -a
  103. expected_max = 3*a
  104. else:
  105. expected_min = 3*a
  106. expected_max = -a
  107. c = interactive(f, a=a)
  108. assert len(c.children) == 2
  109. w = c.children[0]
  110. check_widget(w,
  111. cls=widgets.IntSlider,
  112. description='a',
  113. value=a,
  114. min=expected_min,
  115. max=expected_max,
  116. step=1,
  117. readout=True,
  118. )
  119. def test_list_str():
  120. values = ['hello', 'there', 'guy']
  121. first = values[0]
  122. c = interactive(f, lis=values)
  123. assert len(c.children) == 2
  124. d = dict(
  125. cls=widgets.Dropdown,
  126. value=first,
  127. options=tuple(values),
  128. _options_labels=tuple(values),
  129. _options_values=tuple(values),
  130. )
  131. check_widgets(c, lis=d)
  132. def test_list_int():
  133. values = [3, 1, 2]
  134. first = values[0]
  135. c = interactive(f, lis=values)
  136. assert len(c.children) == 2
  137. d = dict(
  138. cls=widgets.Dropdown,
  139. value=first,
  140. options=tuple(values),
  141. _options_labels=tuple(str(v) for v in values),
  142. _options_values=tuple(values),
  143. )
  144. check_widgets(c, lis=d)
  145. def test_list_tuple():
  146. values = [(3, 300), (1, 100), (2, 200)]
  147. first = values[0][1]
  148. c = interactive(f, lis=values)
  149. assert len(c.children) == 2
  150. d = dict(
  151. cls=widgets.Dropdown,
  152. value=first,
  153. options=tuple(values),
  154. _options_labels=("3", "1", "2"),
  155. _options_values=(300, 100, 200),
  156. )
  157. check_widgets(c, lis=d)
  158. def test_list_tuple_invalid():
  159. for bad in [
  160. (),
  161. ]:
  162. with pytest.raises(ValueError):
  163. print(bad) # because there is no custom message in assert_raises
  164. c = interactive(f, tup=bad)
  165. def test_dict():
  166. for d in [
  167. dict(a=5),
  168. dict(a=5, b='b', c=dict),
  169. ]:
  170. c = interactive(f, d=d)
  171. w = c.children[0]
  172. check = dict(
  173. cls=widgets.Dropdown,
  174. description='d',
  175. value=next(iter(d.values())),
  176. options=d,
  177. _options_labels=tuple(d.keys()),
  178. _options_values=tuple(d.values()),
  179. )
  180. check_widget(w, **check)
  181. def test_ordereddict():
  182. from collections import OrderedDict
  183. items = [(3, 300), (1, 100), (2, 200)]
  184. first = items[0][1]
  185. values = OrderedDict(items)
  186. c = interactive(f, lis=values)
  187. assert len(c.children) == 2
  188. d = dict(
  189. cls=widgets.Dropdown,
  190. value=first,
  191. options=values,
  192. _options_labels=("3", "1", "2"),
  193. _options_values=(300, 100, 200),
  194. )
  195. check_widgets(c, lis=d)
  196. def test_iterable():
  197. def yield_values():
  198. yield 3
  199. yield 1
  200. yield 2
  201. first = next(yield_values())
  202. c = interactive(f, lis=yield_values())
  203. assert len(c.children) == 2
  204. d = dict(
  205. cls=widgets.Dropdown,
  206. value=first,
  207. options=(3, 1, 2),
  208. _options_labels=("3", "1", "2"),
  209. _options_values=(3, 1, 2),
  210. )
  211. check_widgets(c, lis=d)
  212. def test_iterable_tuple():
  213. values = [(3, 300), (1, 100), (2, 200)]
  214. first = values[0][1]
  215. c = interactive(f, lis=iter(values))
  216. assert len(c.children) == 2
  217. d = dict(
  218. cls=widgets.Dropdown,
  219. value=first,
  220. options=tuple(values),
  221. _options_labels=("3", "1", "2"),
  222. _options_values=(300, 100, 200),
  223. )
  224. check_widgets(c, lis=d)
  225. def test_mapping():
  226. from collections import Mapping, OrderedDict
  227. class TestMapping(Mapping):
  228. def __init__(self, values):
  229. self.values = values
  230. def __getitem__(self):
  231. raise NotImplementedError
  232. def __len__(self):
  233. raise NotImplementedError
  234. def __iter__(self):
  235. raise NotImplementedError
  236. def items(self):
  237. return self.values
  238. items = [(3, 300), (1, 100), (2, 200)]
  239. first = items[0][1]
  240. values = TestMapping(items)
  241. c = interactive(f, lis=values)
  242. assert len(c.children) == 2
  243. d = dict(
  244. cls=widgets.Dropdown,
  245. value=first,
  246. options=tuple(items),
  247. _options_labels=("3", "1", "2"),
  248. _options_values=(300, 100, 200),
  249. )
  250. check_widgets(c, lis=d)
  251. def test_defaults():
  252. @annotate(n=10)
  253. def f(n, f=4.5, g=1):
  254. pass
  255. c = interactive(f)
  256. check_widgets(c,
  257. n=dict(
  258. cls=widgets.IntSlider,
  259. value=10,
  260. ),
  261. f=dict(
  262. cls=widgets.FloatSlider,
  263. value=4.5,
  264. ),
  265. g=dict(
  266. cls=widgets.IntSlider,
  267. value=1,
  268. ),
  269. )
  270. def test_default_values():
  271. @annotate(n=10, f=(0, 10.), g=5, h=OrderedDict([('a',1), ('b',2)]), j=['hi', 'there'])
  272. def f(n, f=4.5, g=1, h=2, j='there'):
  273. pass
  274. c = interactive(f)
  275. check_widgets(c,
  276. n=dict(
  277. cls=widgets.IntSlider,
  278. value=10,
  279. ),
  280. f=dict(
  281. cls=widgets.FloatSlider,
  282. value=4.5,
  283. ),
  284. g=dict(
  285. cls=widgets.IntSlider,
  286. value=5,
  287. ),
  288. h=dict(
  289. cls=widgets.Dropdown,
  290. options=OrderedDict([('a',1), ('b',2)]),
  291. value=2
  292. ),
  293. j=dict(
  294. cls=widgets.Dropdown,
  295. options=('hi', 'there'),
  296. value='there'
  297. ),
  298. )
  299. def test_default_out_of_bounds():
  300. @annotate(f=(0, 10.), h={'a': 1}, j=['hi', 'there'])
  301. def f(f='hi', h=5, j='other'):
  302. pass
  303. c = interactive(f)
  304. check_widgets(c,
  305. f=dict(
  306. cls=widgets.FloatSlider,
  307. value=5.,
  308. ),
  309. h=dict(
  310. cls=widgets.Dropdown,
  311. options={'a': 1},
  312. value=1,
  313. ),
  314. j=dict(
  315. cls=widgets.Dropdown,
  316. options=('hi', 'there'),
  317. value='hi',
  318. ),
  319. )
  320. def test_annotations():
  321. @annotate(n=10, f=widgets.FloatText())
  322. def f(n, f):
  323. pass
  324. c = interactive(f)
  325. check_widgets(c,
  326. n=dict(
  327. cls=widgets.IntSlider,
  328. value=10,
  329. ),
  330. f=dict(
  331. cls=widgets.FloatText,
  332. ),
  333. )
  334. def test_priority():
  335. @annotate(annotate='annotate', kwarg='annotate')
  336. def f(kwarg='default', annotate='default', default='default'):
  337. pass
  338. c = interactive(f, kwarg='kwarg')
  339. check_widgets(c,
  340. kwarg=dict(
  341. cls=widgets.Text,
  342. value='kwarg',
  343. ),
  344. annotate=dict(
  345. cls=widgets.Text,
  346. value='annotate',
  347. ),
  348. )
  349. def test_decorator_kwarg(clear_display):
  350. with patch.object(interaction, 'display', record_display):
  351. @interact(a=5)
  352. def foo(a):
  353. pass
  354. assert len(displayed) == 1
  355. w = displayed[0].children[0]
  356. check_widget(w,
  357. cls=widgets.IntSlider,
  358. value=5,
  359. )
  360. def test_interact_instancemethod(clear_display):
  361. class Foo(object):
  362. def show(self, x):
  363. print(x)
  364. f = Foo()
  365. with patch.object(interaction, 'display', record_display):
  366. g = interact(f.show, x=(1,10))
  367. assert len(displayed) == 1
  368. w = displayed[0].children[0]
  369. check_widget(w,
  370. cls=widgets.IntSlider,
  371. value=5,
  372. )
  373. def test_decorator_no_call(clear_display):
  374. with patch.object(interaction, 'display', record_display):
  375. @interact
  376. def foo(a='default'):
  377. pass
  378. assert len(displayed) == 1
  379. w = displayed[0].children[0]
  380. check_widget(w,
  381. cls=widgets.Text,
  382. value='default',
  383. )
  384. def test_call_interact(clear_display):
  385. def foo(a='default'):
  386. pass
  387. with patch.object(interaction, 'display', record_display):
  388. ifoo = interact(foo)
  389. assert len(displayed) == 1
  390. w = displayed[0].children[0]
  391. check_widget(w,
  392. cls=widgets.Text,
  393. value='default',
  394. )
  395. def test_call_interact_on_trait_changed_none_return(clear_display):
  396. def foo(a='default'):
  397. pass
  398. with patch.object(interaction, 'display', record_display):
  399. ifoo = interact(foo)
  400. assert len(displayed) == 1
  401. w = displayed[0].children[0]
  402. check_widget(w,
  403. cls=widgets.Text,
  404. value='default',
  405. )
  406. with patch.object(interaction, 'display', record_display):
  407. w.value = 'called'
  408. assert len(displayed) == 1
  409. def test_call_interact_kwargs(clear_display):
  410. def foo(a='default'):
  411. pass
  412. with patch.object(interaction, 'display', record_display):
  413. ifoo = interact(foo, a=10)
  414. assert len(displayed) == 1
  415. w = displayed[0].children[0]
  416. check_widget(w,
  417. cls=widgets.IntSlider,
  418. value=10,
  419. )
  420. def test_call_decorated_on_trait_change(clear_display):
  421. """test calling @interact decorated functions"""
  422. d = {}
  423. with patch.object(interaction, 'display', record_display):
  424. @interact
  425. def foo(a='default'):
  426. d['a'] = a
  427. return a
  428. assert len(displayed) == 1
  429. w = displayed[0].children[0]
  430. check_widget(w,
  431. cls=widgets.Text,
  432. value='default',
  433. )
  434. # test calling the function directly
  435. a = foo('hello')
  436. assert a == 'hello'
  437. assert d['a'] == 'hello'
  438. # test that setting trait values calls the function
  439. with patch.object(interaction, 'display', record_display):
  440. w.value = 'called'
  441. assert d['a'] == 'called'
  442. assert len(displayed) == 2
  443. assert w.value == displayed[-1]
  444. def test_call_decorated_kwargs_on_trait_change(clear_display):
  445. """test calling @interact(foo=bar) decorated functions"""
  446. d = {}
  447. with patch.object(interaction, 'display', record_display):
  448. @interact(a='kwarg')
  449. def foo(a='default'):
  450. d['a'] = a
  451. return a
  452. assert len(displayed) == 1
  453. w = displayed[0].children[0]
  454. check_widget(w,
  455. cls=widgets.Text,
  456. value='kwarg',
  457. )
  458. # test calling the function directly
  459. a = foo('hello')
  460. assert a == 'hello'
  461. assert d['a'] == 'hello'
  462. # test that setting trait values calls the function
  463. with patch.object(interaction, 'display', record_display):
  464. w.value = 'called'
  465. assert d['a'] == 'called'
  466. assert len(displayed) == 2
  467. assert w.value == displayed[-1]
  468. def test_fixed():
  469. c = interactive(f, a=widgets.fixed(5), b='text')
  470. assert len(c.children) == 2
  471. w = c.children[0]
  472. check_widget(w,
  473. cls=widgets.Text,
  474. value='text',
  475. description='b',
  476. )
  477. def test_default_description():
  478. c = interactive(f, b='text')
  479. w = c.children[0]
  480. check_widget(w,
  481. cls=widgets.Text,
  482. value='text',
  483. description='b',
  484. )
  485. def test_custom_description():
  486. d = {}
  487. def record_kwargs(**kwargs):
  488. d.clear()
  489. d.update(kwargs)
  490. c = interactive(record_kwargs, b=widgets.Text(value='text', description='foo'))
  491. w = c.children[0]
  492. check_widget(w,
  493. cls=widgets.Text,
  494. value='text',
  495. description='foo',
  496. )
  497. w.value = 'different text'
  498. assert d == {'b': 'different text'}
  499. def test_interact_manual_button():
  500. c = interact.options(manual=True).widget(f)
  501. w = c.children[0]
  502. check_widget(w, cls=widgets.Button)
  503. def test_interact_manual_nocall():
  504. callcount = 0
  505. def calltest(testarg):
  506. callcount += 1
  507. c = interact.options(manual=True)(calltest, testarg=5).widget
  508. c.children[0].value = 10
  509. assert callcount == 0
  510. def test_interact_call():
  511. w = interact.widget(f)
  512. w.update()
  513. w = interact_manual.widget(f)
  514. w.update()
  515. def test_interact_options():
  516. def f(x):
  517. return x
  518. w = interact.options(manual=False).options(manual=True)(f, x=21).widget
  519. assert w.manual == True
  520. w = interact_manual.options(manual=False).options()(x=21).widget(f)
  521. assert w.manual == False
  522. w = interact(x=21)().options(manual=True)(f).widget
  523. assert w.manual == True
  524. def test_interact_options_bad():
  525. with pytest.raises(ValueError):
  526. interact.options(bad="foo")
  527. def test_int_range_logic():
  528. irsw = widgets.IntRangeSlider
  529. w = irsw(value=(2, 4), min=0, max=6)
  530. check_widget(w, cls=irsw, value=(2, 4), min=0, max=6)
  531. w.upper = 3
  532. w.max = 3
  533. check_widget(w, cls=irsw, value=(2, 3), min=0, max=3)
  534. w.min = 0
  535. w.max = 6
  536. w.lower = 2
  537. w.upper = 4
  538. check_widget(w, cls=irsw, value=(2, 4), min=0, max=6)
  539. w.value = (0, 1) #lower non-overlapping range
  540. check_widget(w, cls=irsw, value=(0, 1), min=0, max=6)
  541. w.value = (5, 6) #upper non-overlapping range
  542. check_widget(w, cls=irsw, value=(5, 6), min=0, max=6)
  543. w.lower = 2
  544. check_widget(w, cls=irsw, value=(2, 6), min=0, max=6)
  545. with pytest.raises(TraitError):
  546. w.min = 7
  547. with pytest.raises(TraitError):
  548. w.max = -1
  549. w = irsw(min=2, max=3, value=(2, 3))
  550. check_widget(w, min=2, max=3, value=(2, 3))
  551. w = irsw(min=100, max=200, value=(125, 175))
  552. check_widget(w, value=(125, 175))
  553. with pytest.raises(TraitError):
  554. irsw(min=2, max=1)
  555. def test_float_range_logic():
  556. frsw = widgets.FloatRangeSlider
  557. w = frsw(value=(.2, .4), min=0., max=.6)
  558. check_widget(w, cls=frsw, value=(.2, .4), min=0., max=.6)
  559. w.min = 0.
  560. w.max = .6
  561. w.lower = .2
  562. w.upper = .4
  563. check_widget(w, cls=frsw, value=(.2, .4), min=0., max=.6)
  564. w.value = (0., .1) #lower non-overlapping range
  565. check_widget(w, cls=frsw, value=(0., .1), min=0., max=.6)
  566. w.value = (.5, .6) #upper non-overlapping range
  567. check_widget(w, cls=frsw, value=(.5, .6), min=0., max=.6)
  568. w.lower = .2
  569. check_widget(w, cls=frsw, value=(.2, .6), min=0., max=.6)
  570. with pytest.raises(TraitError):
  571. w.min = .7
  572. with pytest.raises(TraitError):
  573. w.max = -.1
  574. w = frsw(min=2, max=3, value=(2.2, 2.5))
  575. check_widget(w, min=2., max=3.)
  576. with pytest.raises(TraitError):
  577. frsw(min=.2, max=.1)
  578. def test_multiple_selection():
  579. smw = widgets.SelectMultiple
  580. # degenerate multiple select
  581. w = smw()
  582. check_widget(w, value=tuple())
  583. # don't accept random other value when no options
  584. with pytest.raises(TraitError):
  585. w.value = (2,)
  586. check_widget(w, value=tuple())
  587. # basic multiple select
  588. w = smw(options=[(1, 1)], value=[1])
  589. check_widget(w, cls=smw, value=(1,), options=((1, 1),))
  590. # don't accept random other value
  591. with pytest.raises(TraitError):
  592. w.value = w.value + (2,)
  593. check_widget(w, value=(1,))
  594. # change options, which resets value
  595. w.options = w.options + ((2, 2),)
  596. check_widget(w, options=((1, 1), (2,2)), value=())
  597. # change value
  598. w.value = (1,2)
  599. check_widget(w, value=(1, 2))
  600. # dict style
  601. w.options = {1: 1}
  602. check_widget(w, options={1:1})
  603. # updating
  604. with pytest.raises(TraitError):
  605. w.value = (2,)
  606. check_widget(w, options={1:1})
  607. def test_interact_noinspect():
  608. a = u'hello'
  609. c = interactive(print, a=a)
  610. w = c.children[0]
  611. check_widget(w,
  612. cls=widgets.Text,
  613. description='a',
  614. value=a,
  615. )
  616. def test_get_interact_value():
  617. from ipywidgets.widgets import ValueWidget
  618. from traitlets import Unicode
  619. class TheAnswer(ValueWidget):
  620. _model_name = Unicode('TheAnswer')
  621. description = Unicode()
  622. def get_interact_value(self):
  623. return 42
  624. w = TheAnswer()
  625. c = interactive(lambda v: v, v=w)
  626. c.update()
  627. assert c.result == 42
  628. def test_state_schema():
  629. from ipywidgets.widgets import IntSlider, Widget
  630. import json
  631. import jsonschema
  632. s = IntSlider()
  633. state = Widget.get_manager_state(drop_defaults=True)
  634. with open(os.path.join(os.path.dirname(os.path.realpath(__file__)), '../../', 'state.schema.json')) as f:
  635. schema = json.load(f)
  636. jsonschema.validate(state, schema)