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