interaction.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576
  1. # Copyright (c) Jupyter Development Team.
  2. # Distributed under the terms of the Modified BSD License.
  3. """Interact with functions using widgets."""
  4. from __future__ import print_function
  5. from __future__ import division
  6. try: # Python >= 3.3
  7. from inspect import signature, Parameter
  8. except ImportError:
  9. from IPython.utils.signatures import signature, Parameter
  10. from inspect import getcallargs
  11. try:
  12. from inspect import getfullargspec as check_argspec
  13. except ImportError:
  14. from inspect import getargspec as check_argspec # py2
  15. import sys
  16. from IPython.core.getipython import get_ipython
  17. from . import (ValueWidget, Text,
  18. FloatSlider, IntSlider, Checkbox, Dropdown,
  19. VBox, Button, DOMWidget, Output)
  20. from IPython.display import display, clear_output
  21. from ipython_genutils.py3compat import string_types, unicode_type
  22. from traitlets import HasTraits, Any, Unicode, observe
  23. from numbers import Real, Integral
  24. from warnings import warn
  25. try:
  26. from collections.abc import Iterable, Mapping
  27. except ImportError:
  28. from collections import Iterable, Mapping # py2
  29. empty = Parameter.empty
  30. def show_inline_matplotlib_plots():
  31. """Show matplotlib plots immediately if using the inline backend.
  32. With ipywidgets 6.0, matplotlib plots don't work well with interact when
  33. using the inline backend that comes with ipykernel. Basically, the inline
  34. backend only shows the plot after the entire cell executes, which does not
  35. play well with drawing plots inside of an interact function. See
  36. https://github.com/jupyter-widgets/ipywidgets/issues/1181/ and
  37. https://github.com/ipython/ipython/issues/10376 for more details. This
  38. function displays any matplotlib plots if the backend is the inline backend.
  39. """
  40. if 'matplotlib' not in sys.modules:
  41. # matplotlib hasn't been imported, nothing to do.
  42. return
  43. try:
  44. import matplotlib as mpl
  45. from ipykernel.pylab.backend_inline import flush_figures
  46. except ImportError:
  47. return
  48. if mpl.get_backend() == 'module://ipykernel.pylab.backend_inline':
  49. flush_figures()
  50. def interactive_output(f, controls):
  51. """Connect widget controls to a function.
  52. This function does not generate a user interface for the widgets (unlike `interact`).
  53. This enables customisation of the widget user interface layout.
  54. The user interface layout must be defined and displayed manually.
  55. """
  56. out = Output()
  57. def observer(change):
  58. kwargs = {k:v.value for k,v in controls.items()}
  59. show_inline_matplotlib_plots()
  60. with out:
  61. clear_output(wait=True)
  62. f(**kwargs)
  63. show_inline_matplotlib_plots()
  64. for k,w in controls.items():
  65. w.observe(observer, 'value')
  66. show_inline_matplotlib_plots()
  67. observer(None)
  68. return out
  69. def _matches(o, pattern):
  70. """Match a pattern of types in a sequence."""
  71. if not len(o) == len(pattern):
  72. return False
  73. comps = zip(o,pattern)
  74. return all(isinstance(obj,kind) for obj,kind in comps)
  75. def _get_min_max_value(min, max, value=None, step=None):
  76. """Return min, max, value given input values with possible None."""
  77. # Either min and max need to be given, or value needs to be given
  78. if value is None:
  79. if min is None or max is None:
  80. raise ValueError('unable to infer range, value from: ({0}, {1}, {2})'.format(min, max, value))
  81. diff = max - min
  82. value = min + (diff / 2)
  83. # Ensure that value has the same type as diff
  84. if not isinstance(value, type(diff)):
  85. value = min + (diff // 2)
  86. else: # value is not None
  87. if not isinstance(value, Real):
  88. raise TypeError('expected a real number, got: %r' % value)
  89. # Infer min/max from value
  90. if value == 0:
  91. # This gives (0, 1) of the correct type
  92. vrange = (value, value + 1)
  93. elif value > 0:
  94. vrange = (-value, 3*value)
  95. else:
  96. vrange = (3*value, -value)
  97. if min is None:
  98. min = vrange[0]
  99. if max is None:
  100. max = vrange[1]
  101. if step is not None:
  102. # ensure value is on a step
  103. tick = int((value - min) / step)
  104. value = min + tick * step
  105. if not min <= value <= max:
  106. raise ValueError('value must be between min and max (min={0}, value={1}, max={2})'.format(min, value, max))
  107. return min, max, value
  108. def _yield_abbreviations_for_parameter(param, kwargs):
  109. """Get an abbreviation for a function parameter."""
  110. name = param.name
  111. kind = param.kind
  112. ann = param.annotation
  113. default = param.default
  114. not_found = (name, empty, empty)
  115. if kind in (Parameter.POSITIONAL_OR_KEYWORD, Parameter.KEYWORD_ONLY):
  116. if name in kwargs:
  117. value = kwargs.pop(name)
  118. elif ann is not empty:
  119. warn("Using function annotations to implicitly specify interactive controls is deprecated. Use an explicit keyword argument for the parameter instead.", DeprecationWarning)
  120. value = ann
  121. elif default is not empty:
  122. value = default
  123. else:
  124. yield not_found
  125. yield (name, value, default)
  126. elif kind == Parameter.VAR_KEYWORD:
  127. # In this case name=kwargs and we yield the items in kwargs with their keys.
  128. for k, v in kwargs.copy().items():
  129. kwargs.pop(k)
  130. yield k, v, empty
  131. class interactive(VBox):
  132. """
  133. A VBox container containing a group of interactive widgets tied to a
  134. function.
  135. Parameters
  136. ----------
  137. __interact_f : function
  138. The function to which the interactive widgets are tied. The `**kwargs`
  139. should match the function signature.
  140. __options : dict
  141. A dict of options. Currently, the only supported keys are
  142. ``"manual"`` and ``"manual_name"``.
  143. **kwargs : various, optional
  144. An interactive widget is created for each keyword argument that is a
  145. valid widget abbreviation.
  146. Note that the first two parameters intentionally start with a double
  147. underscore to avoid being mixed up with keyword arguments passed by
  148. ``**kwargs``.
  149. """
  150. def __init__(self, __interact_f, __options={}, **kwargs):
  151. VBox.__init__(self, _dom_classes=['widget-interact'])
  152. self.result = None
  153. self.args = []
  154. self.kwargs = {}
  155. self.f = f = __interact_f
  156. self.clear_output = kwargs.pop('clear_output', True)
  157. self.manual = __options.get("manual", False)
  158. self.manual_name = __options.get("manual_name", "Run Interact")
  159. self.auto_display = __options.get("auto_display", False)
  160. new_kwargs = self.find_abbreviations(kwargs)
  161. # Before we proceed, let's make sure that the user has passed a set of args+kwargs
  162. # that will lead to a valid call of the function. This protects against unspecified
  163. # and doubly-specified arguments.
  164. try:
  165. check_argspec(f)
  166. except TypeError:
  167. # if we can't inspect, we can't validate
  168. pass
  169. else:
  170. getcallargs(f, **{n:v for n,v,_ in new_kwargs})
  171. # Now build the widgets from the abbreviations.
  172. self.kwargs_widgets = self.widgets_from_abbreviations(new_kwargs)
  173. # This has to be done as an assignment, not using self.children.append,
  174. # so that traitlets notices the update. We skip any objects (such as fixed) that
  175. # are not DOMWidgets.
  176. c = [w for w in self.kwargs_widgets if isinstance(w, DOMWidget)]
  177. # If we are only to run the function on demand, add a button to request this.
  178. if self.manual:
  179. self.manual_button = Button(description=self.manual_name)
  180. c.append(self.manual_button)
  181. self.out = Output()
  182. c.append(self.out)
  183. self.children = c
  184. # Wire up the widgets
  185. # If we are doing manual running, the callback is only triggered by the button
  186. # Otherwise, it is triggered for every trait change received
  187. # On-demand running also suppresses running the function with the initial parameters
  188. if self.manual:
  189. self.manual_button.on_click(self.update)
  190. # Also register input handlers on text areas, so the user can hit return to
  191. # invoke execution.
  192. for w in self.kwargs_widgets:
  193. if isinstance(w, Text):
  194. w.on_submit(self.update)
  195. else:
  196. for widget in self.kwargs_widgets:
  197. widget.observe(self.update, names='value')
  198. self.on_displayed(self.update)
  199. # Callback function
  200. def update(self, *args):
  201. """
  202. Call the interact function and update the output widget with
  203. the result of the function call.
  204. Parameters
  205. ----------
  206. *args : ignored
  207. Required for this method to be used as traitlets callback.
  208. """
  209. self.kwargs = {}
  210. if self.manual:
  211. self.manual_button.disabled = True
  212. try:
  213. show_inline_matplotlib_plots()
  214. with self.out:
  215. if self.clear_output:
  216. clear_output(wait=True)
  217. for widget in self.kwargs_widgets:
  218. value = widget.get_interact_value()
  219. self.kwargs[widget._kwarg] = value
  220. self.result = self.f(**self.kwargs)
  221. show_inline_matplotlib_plots()
  222. if self.auto_display and self.result is not None:
  223. display(self.result)
  224. except Exception as e:
  225. ip = get_ipython()
  226. if ip is None:
  227. self.log.warn("Exception in interact callback: %s", e, exc_info=True)
  228. else:
  229. ip.showtraceback()
  230. finally:
  231. if self.manual:
  232. self.manual_button.disabled = False
  233. # Find abbreviations
  234. def signature(self):
  235. return signature(self.f)
  236. def find_abbreviations(self, kwargs):
  237. """Find the abbreviations for the given function and kwargs.
  238. Return (name, abbrev, default) tuples.
  239. """
  240. new_kwargs = []
  241. try:
  242. sig = self.signature()
  243. except (ValueError, TypeError):
  244. # can't inspect, no info from function; only use kwargs
  245. return [ (key, value, value) for key, value in kwargs.items() ]
  246. for param in sig.parameters.values():
  247. for name, value, default in _yield_abbreviations_for_parameter(param, kwargs):
  248. if value is empty:
  249. raise ValueError('cannot find widget or abbreviation for argument: {!r}'.format(name))
  250. new_kwargs.append((name, value, default))
  251. return new_kwargs
  252. # Abbreviations to widgets
  253. def widgets_from_abbreviations(self, seq):
  254. """Given a sequence of (name, abbrev, default) tuples, return a sequence of Widgets."""
  255. result = []
  256. for name, abbrev, default in seq:
  257. widget = self.widget_from_abbrev(abbrev, default)
  258. if not (isinstance(widget, ValueWidget) or isinstance(widget, fixed)):
  259. if widget is None:
  260. raise ValueError("{!r} cannot be transformed to a widget".format(abbrev))
  261. else:
  262. raise TypeError("{!r} is not a ValueWidget".format(widget))
  263. if not widget.description:
  264. widget.description = name
  265. widget._kwarg = name
  266. result.append(widget)
  267. return result
  268. @classmethod
  269. def widget_from_abbrev(cls, abbrev, default=empty):
  270. """Build a ValueWidget instance given an abbreviation or Widget."""
  271. if isinstance(abbrev, ValueWidget) or isinstance(abbrev, fixed):
  272. return abbrev
  273. if isinstance(abbrev, tuple):
  274. widget = cls.widget_from_tuple(abbrev)
  275. if default is not empty:
  276. try:
  277. widget.value = default
  278. except Exception:
  279. # ignore failure to set default
  280. pass
  281. return widget
  282. # Try single value
  283. widget = cls.widget_from_single_value(abbrev)
  284. if widget is not None:
  285. return widget
  286. # Something iterable (list, dict, generator, ...). Note that str and
  287. # tuple should be handled before, that is why we check this case last.
  288. if isinstance(abbrev, Iterable):
  289. widget = cls.widget_from_iterable(abbrev)
  290. if default is not empty:
  291. try:
  292. widget.value = default
  293. except Exception:
  294. # ignore failure to set default
  295. pass
  296. return widget
  297. # No idea...
  298. return None
  299. @staticmethod
  300. def widget_from_single_value(o):
  301. """Make widgets from single values, which can be used as parameter defaults."""
  302. if isinstance(o, string_types):
  303. return Text(value=unicode_type(o))
  304. elif isinstance(o, bool):
  305. return Checkbox(value=o)
  306. elif isinstance(o, Integral):
  307. min, max, value = _get_min_max_value(None, None, o)
  308. return IntSlider(value=o, min=min, max=max)
  309. elif isinstance(o, Real):
  310. min, max, value = _get_min_max_value(None, None, o)
  311. return FloatSlider(value=o, min=min, max=max)
  312. else:
  313. return None
  314. @staticmethod
  315. def widget_from_tuple(o):
  316. """Make widgets from a tuple abbreviation."""
  317. if _matches(o, (Real, Real)):
  318. min, max, value = _get_min_max_value(o[0], o[1])
  319. if all(isinstance(_, Integral) for _ in o):
  320. cls = IntSlider
  321. else:
  322. cls = FloatSlider
  323. return cls(value=value, min=min, max=max)
  324. elif _matches(o, (Real, Real, Real)):
  325. step = o[2]
  326. if step <= 0:
  327. raise ValueError("step must be >= 0, not %r" % step)
  328. min, max, value = _get_min_max_value(o[0], o[1], step=step)
  329. if all(isinstance(_, Integral) for _ in o):
  330. cls = IntSlider
  331. else:
  332. cls = FloatSlider
  333. return cls(value=value, min=min, max=max, step=step)
  334. @staticmethod
  335. def widget_from_iterable(o):
  336. """Make widgets from an iterable. This should not be done for
  337. a string or tuple."""
  338. # Dropdown expects a dict or list, so we convert an arbitrary
  339. # iterable to either of those.
  340. if isinstance(o, (list, dict)):
  341. return Dropdown(options=o)
  342. elif isinstance(o, Mapping):
  343. return Dropdown(options=list(o.items()))
  344. else:
  345. return Dropdown(options=list(o))
  346. # Return a factory for interactive functions
  347. @classmethod
  348. def factory(cls):
  349. options = dict(manual=False, auto_display=True, manual_name="Run Interact")
  350. return _InteractFactory(cls, options)
  351. class _InteractFactory(object):
  352. """
  353. Factory for instances of :class:`interactive`.
  354. This class is needed to support options like::
  355. >>> @interact.options(manual=True)
  356. ... def greeting(text="World"):
  357. ... print("Hello {}".format(text))
  358. Parameters
  359. ----------
  360. cls : class
  361. The subclass of :class:`interactive` to construct.
  362. options : dict
  363. A dict of options used to construct the interactive
  364. function. By default, this is returned by
  365. ``cls.default_options()``.
  366. kwargs : dict
  367. A dict of **kwargs to use for widgets.
  368. """
  369. def __init__(self, cls, options, kwargs={}):
  370. self.cls = cls
  371. self.opts = options
  372. self.kwargs = kwargs
  373. def widget(self, f):
  374. """
  375. Return an interactive function widget for the given function.
  376. The widget is only constructed, not displayed nor attached to
  377. the function.
  378. Returns
  379. -------
  380. An instance of ``self.cls`` (typically :class:`interactive`).
  381. Parameters
  382. ----------
  383. f : function
  384. The function to which the interactive widgets are tied.
  385. """
  386. return self.cls(f, self.opts, **self.kwargs)
  387. def __call__(self, __interact_f=None, **kwargs):
  388. """
  389. Make the given function interactive by adding and displaying
  390. the corresponding :class:`interactive` widget.
  391. Expects the first argument to be a function. Parameters to this
  392. function are widget abbreviations passed in as keyword arguments
  393. (``**kwargs``). Can be used as a decorator (see examples).
  394. Returns
  395. -------
  396. f : __interact_f with interactive widget attached to it.
  397. Parameters
  398. ----------
  399. __interact_f : function
  400. The function to which the interactive widgets are tied. The `**kwargs`
  401. should match the function signature. Passed to :func:`interactive()`
  402. **kwargs : various, optional
  403. An interactive widget is created for each keyword argument that is a
  404. valid widget abbreviation. Passed to :func:`interactive()`
  405. Examples
  406. --------
  407. Render an interactive text field that shows the greeting with the passed in
  408. text::
  409. # 1. Using interact as a function
  410. def greeting(text="World"):
  411. print("Hello {}".format(text))
  412. interact(greeting, text="IPython Widgets")
  413. # 2. Using interact as a decorator
  414. @interact
  415. def greeting(text="World"):
  416. print("Hello {}".format(text))
  417. # 3. Using interact as a decorator with named parameters
  418. @interact(text="IPython Widgets")
  419. def greeting(text="World"):
  420. print("Hello {}".format(text))
  421. Render an interactive slider widget and prints square of number::
  422. # 1. Using interact as a function
  423. def square(num=1):
  424. print("{} squared is {}".format(num, num*num))
  425. interact(square, num=5)
  426. # 2. Using interact as a decorator
  427. @interact
  428. def square(num=2):
  429. print("{} squared is {}".format(num, num*num))
  430. # 3. Using interact as a decorator with named parameters
  431. @interact(num=5)
  432. def square(num=2):
  433. print("{} squared is {}".format(num, num*num))
  434. """
  435. # If kwargs are given, replace self by a new
  436. # _InteractFactory with the updated kwargs
  437. if kwargs:
  438. kw = dict(self.kwargs)
  439. kw.update(kwargs)
  440. self = type(self)(self.cls, self.opts, kw)
  441. f = __interact_f
  442. if f is None:
  443. # This branch handles the case 3
  444. # @interact(a=30, b=40)
  445. # def f(*args, **kwargs):
  446. # ...
  447. #
  448. # Simply return the new factory
  449. return self
  450. # positional arg support in: https://gist.github.com/8851331
  451. # Handle the cases 1 and 2
  452. # 1. interact(f, **kwargs)
  453. # 2. @interact
  454. # def f(*args, **kwargs):
  455. # ...
  456. w = self.widget(f)
  457. try:
  458. f.widget = w
  459. except AttributeError:
  460. # some things (instancemethods) can't have attributes attached,
  461. # so wrap in a lambda
  462. f = lambda *args, **kwargs: __interact_f(*args, **kwargs)
  463. f.widget = w
  464. show_inline_matplotlib_plots()
  465. display(w)
  466. return f
  467. def options(self, **kwds):
  468. """
  469. Change options for interactive functions.
  470. Returns
  471. -------
  472. A new :class:`_InteractFactory` which will apply the
  473. options when called.
  474. """
  475. opts = dict(self.opts)
  476. for k in kwds:
  477. try:
  478. # Ensure that the key exists because we want to change
  479. # existing options, not add new ones.
  480. _ = opts[k]
  481. except KeyError:
  482. raise ValueError("invalid option {!r}".format(k))
  483. opts[k] = kwds[k]
  484. return type(self)(self.cls, opts, self.kwargs)
  485. interact = interactive.factory()
  486. interact_manual = interact.options(manual=True, manual_name="Run Interact")
  487. class fixed(HasTraits):
  488. """A pseudo-widget whose value is fixed and never synced to the client."""
  489. value = Any(help="Any Python object")
  490. description = Unicode('', help="Any Python object")
  491. def __init__(self, value, **kwargs):
  492. super(fixed, self).__init__(value=value, **kwargs)
  493. def get_interact_value(self):
  494. """Return the value for this widget which should be passed to
  495. interactive functions. Custom widgets can change this method
  496. to process the raw value ``self.value``.
  497. """
  498. return self.value