##// END OF EJS Templates
Get widgets from function annotations and default arguments....
Thomas Kluyver -
Show More

The requested changes are too big and content was truncated. Show full diff

This diff has been collapsed as it changes many lines, (819 lines changed) Show them Hide them
@@ -0,0 +1,819 b''
1 """Function signature objects for callables
2
3 Back port of Python 3.3's function signature tools from the inspect module,
4 modified to be compatible with Python 2.6, 2.7 and 3.2+.
5 """
6
7 #-----------------------------------------------------------------------------
8 # Python 3.3 stdlib inspect.py is public domain
9 #
10 # Backports Copyright (C) 2013 Aaron Iles
11 # Used under Apache License Version 2.0
12 #
13 # Further Changes are Copyright (C) 2013 The IPython Development Team
14 #
15 # Distributed under the terms of the BSD License. The full license is in
16 # the file COPYING, distributed as part of this software.
17 #-----------------------------------------------------------------------------
18
19 from __future__ import absolute_import, division, print_function
20 import itertools
21 import functools
22 import re
23 import types
24
25
26 # patch for single-file
27 # we don't support 2.6, so we can just import OrderedDict
28 from collections import OrderedDict
29
30 __version__ = '0.3'
31 # end patch
32
33 __all__ = ['BoundArguments', 'Parameter', 'Signature', 'signature']
34
35
36 _WrapperDescriptor = type(type.__call__)
37 _MethodWrapper = type(all.__call__)
38
39 _NonUserDefinedCallables = (_WrapperDescriptor,
40 _MethodWrapper,
41 types.BuiltinFunctionType)
42
43
44 def formatannotation(annotation, base_module=None):
45 if isinstance(annotation, type):
46 if annotation.__module__ in ('builtins', '__builtin__', base_module):
47 return annotation.__name__
48 return annotation.__module__+'.'+annotation.__name__
49 return repr(annotation)
50
51
52 def _get_user_defined_method(cls, method_name, *nested):
53 try:
54 if cls is type:
55 return
56 meth = getattr(cls, method_name)
57 for name in nested:
58 meth = getattr(meth, name, meth)
59 except AttributeError:
60 return
61 else:
62 if not isinstance(meth, _NonUserDefinedCallables):
63 # Once '__signature__' will be added to 'C'-level
64 # callables, this check won't be necessary
65 return meth
66
67
68 def signature(obj):
69 '''Get a signature object for the passed callable.'''
70
71 if not callable(obj):
72 raise TypeError('{0!r} is not a callable object'.format(obj))
73
74 if isinstance(obj, types.MethodType):
75 # In this case we skip the first parameter of the underlying
76 # function (usually `self` or `cls`).
77 sig = signature(obj.__func__)
78 return sig.replace(parameters=tuple(sig.parameters.values())[1:])
79
80 try:
81 sig = obj.__signature__
82 except AttributeError:
83 pass
84 else:
85 if sig is not None:
86 return sig
87
88 try:
89 # Was this function wrapped by a decorator?
90 wrapped = obj.__wrapped__
91 except AttributeError:
92 pass
93 else:
94 return signature(wrapped)
95
96 if isinstance(obj, types.FunctionType):
97 return Signature.from_function(obj)
98
99 if isinstance(obj, functools.partial):
100 sig = signature(obj.func)
101
102 new_params = OrderedDict(sig.parameters.items())
103
104 partial_args = obj.args or ()
105 partial_keywords = obj.keywords or {}
106 try:
107 ba = sig.bind_partial(*partial_args, **partial_keywords)
108 except TypeError as ex:
109 msg = 'partial object {0!r} has incorrect arguments'.format(obj)
110 raise ValueError(msg)
111
112 for arg_name, arg_value in ba.arguments.items():
113 param = new_params[arg_name]
114 if arg_name in partial_keywords:
115 # We set a new default value, because the following code
116 # is correct:
117 #
118 # >>> def foo(a): print(a)
119 # >>> print(partial(partial(foo, a=10), a=20)())
120 # 20
121 # >>> print(partial(partial(foo, a=10), a=20)(a=30))
122 # 30
123 #
124 # So, with 'partial' objects, passing a keyword argument is
125 # like setting a new default value for the corresponding
126 # parameter
127 #
128 # We also mark this parameter with '_partial_kwarg'
129 # flag. Later, in '_bind', the 'default' value of this
130 # parameter will be added to 'kwargs', to simulate
131 # the 'functools.partial' real call.
132 new_params[arg_name] = param.replace(default=arg_value,
133 _partial_kwarg=True)
134
135 elif (param.kind not in (_VAR_KEYWORD, _VAR_POSITIONAL) and
136 not param._partial_kwarg):
137 new_params.pop(arg_name)
138
139 return sig.replace(parameters=new_params.values())
140
141 sig = None
142 if isinstance(obj, type):
143 # obj is a class or a metaclass
144
145 # First, let's see if it has an overloaded __call__ defined
146 # in its metaclass
147 call = _get_user_defined_method(type(obj), '__call__')
148 if call is not None:
149 sig = signature(call)
150 else:
151 # Now we check if the 'obj' class has a '__new__' method
152 new = _get_user_defined_method(obj, '__new__')
153 if new is not None:
154 sig = signature(new)
155 else:
156 # Finally, we should have at least __init__ implemented
157 init = _get_user_defined_method(obj, '__init__')
158 if init is not None:
159 sig = signature(init)
160 elif not isinstance(obj, _NonUserDefinedCallables):
161 # An object with __call__
162 # We also check that the 'obj' is not an instance of
163 # _WrapperDescriptor or _MethodWrapper to avoid
164 # infinite recursion (and even potential segfault)
165 call = _get_user_defined_method(type(obj), '__call__', 'im_func')
166 if call is not None:
167 sig = signature(call)
168
169 if sig is not None:
170 return sig
171
172 if isinstance(obj, types.BuiltinFunctionType):
173 # Raise a nicer error message for builtins
174 msg = 'no signature found for builtin function {0!r}'.format(obj)
175 raise ValueError(msg)
176
177 raise ValueError('callable {0!r} is not supported by signature'.format(obj))
178
179
180 class _void(object):
181 '''A private marker - used in Parameter & Signature'''
182
183
184 class _empty(object):
185 pass
186
187
188 class _ParameterKind(int):
189 def __new__(self, *args, **kwargs):
190 obj = int.__new__(self, *args)
191 obj._name = kwargs['name']
192 return obj
193
194 def __str__(self):
195 return self._name
196
197 def __repr__(self):
198 return '<_ParameterKind: {0!r}>'.format(self._name)
199
200
201 _POSITIONAL_ONLY = _ParameterKind(0, name='POSITIONAL_ONLY')
202 _POSITIONAL_OR_KEYWORD = _ParameterKind(1, name='POSITIONAL_OR_KEYWORD')
203 _VAR_POSITIONAL = _ParameterKind(2, name='VAR_POSITIONAL')
204 _KEYWORD_ONLY = _ParameterKind(3, name='KEYWORD_ONLY')
205 _VAR_KEYWORD = _ParameterKind(4, name='VAR_KEYWORD')
206
207
208 class Parameter(object):
209 '''Represents a parameter in a function signature.
210
211 Has the following public attributes:
212
213 * name : str
214 The name of the parameter as a string.
215 * default : object
216 The default value for the parameter if specified. If the
217 parameter has no default value, this attribute is not set.
218 * annotation
219 The annotation for the parameter if specified. If the
220 parameter has no annotation, this attribute is not set.
221 * kind : str
222 Describes how argument values are bound to the parameter.
223 Possible values: `Parameter.POSITIONAL_ONLY`,
224 `Parameter.POSITIONAL_OR_KEYWORD`, `Parameter.VAR_POSITIONAL`,
225 `Parameter.KEYWORD_ONLY`, `Parameter.VAR_KEYWORD`.
226 '''
227
228 __slots__ = ('_name', '_kind', '_default', '_annotation', '_partial_kwarg')
229
230 POSITIONAL_ONLY = _POSITIONAL_ONLY
231 POSITIONAL_OR_KEYWORD = _POSITIONAL_OR_KEYWORD
232 VAR_POSITIONAL = _VAR_POSITIONAL
233 KEYWORD_ONLY = _KEYWORD_ONLY
234 VAR_KEYWORD = _VAR_KEYWORD
235
236 empty = _empty
237
238 def __init__(self, name, kind, default=_empty, annotation=_empty,
239 _partial_kwarg=False):
240
241 if kind not in (_POSITIONAL_ONLY, _POSITIONAL_OR_KEYWORD,
242 _VAR_POSITIONAL, _KEYWORD_ONLY, _VAR_KEYWORD):
243 raise ValueError("invalid value for 'Parameter.kind' attribute")
244 self._kind = kind
245
246 if default is not _empty:
247 if kind in (_VAR_POSITIONAL, _VAR_KEYWORD):
248 msg = '{0} parameters cannot have default values'.format(kind)
249 raise ValueError(msg)
250 self._default = default
251 self._annotation = annotation
252
253 if name is None:
254 if kind != _POSITIONAL_ONLY:
255 raise ValueError("None is not a valid name for a "
256 "non-positional-only parameter")
257 self._name = name
258 else:
259 name = str(name)
260 if kind != _POSITIONAL_ONLY and not re.match(r'[a-z_]\w*$', name, re.I):
261 msg = '{0!r} is not a valid parameter name'.format(name)
262 raise ValueError(msg)
263 self._name = name
264
265 self._partial_kwarg = _partial_kwarg
266
267 @property
268 def name(self):
269 return self._name
270
271 @property
272 def default(self):
273 return self._default
274
275 @property
276 def annotation(self):
277 return self._annotation
278
279 @property
280 def kind(self):
281 return self._kind
282
283 def replace(self, name=_void, kind=_void, annotation=_void,
284 default=_void, _partial_kwarg=_void):
285 '''Creates a customized copy of the Parameter.'''
286
287 if name is _void:
288 name = self._name
289
290 if kind is _void:
291 kind = self._kind
292
293 if annotation is _void:
294 annotation = self._annotation
295
296 if default is _void:
297 default = self._default
298
299 if _partial_kwarg is _void:
300 _partial_kwarg = self._partial_kwarg
301
302 return type(self)(name, kind, default=default, annotation=annotation,
303 _partial_kwarg=_partial_kwarg)
304
305 def __str__(self):
306 kind = self.kind
307
308 formatted = self._name
309 if kind == _POSITIONAL_ONLY:
310 if formatted is None:
311 formatted = ''
312 formatted = '<{0}>'.format(formatted)
313
314 # Add annotation and default value
315 if self._annotation is not _empty:
316 formatted = '{0}:{1}'.format(formatted,
317 formatannotation(self._annotation))
318
319 if self._default is not _empty:
320 formatted = '{0}={1}'.format(formatted, repr(self._default))
321
322 if kind == _VAR_POSITIONAL:
323 formatted = '*' + formatted
324 elif kind == _VAR_KEYWORD:
325 formatted = '**' + formatted
326
327 return formatted
328
329 def __repr__(self):
330 return '<{0} at {1:#x} {2!r}>'.format(self.__class__.__name__,
331 id(self), self.name)
332
333 def __hash__(self):
334 msg = "unhashable type: '{0}'".format(self.__class__.__name__)
335 raise TypeError(msg)
336
337 def __eq__(self, other):
338 return (issubclass(other.__class__, Parameter) and
339 self._name == other._name and
340 self._kind == other._kind and
341 self._default == other._default and
342 self._annotation == other._annotation)
343
344 def __ne__(self, other):
345 return not self.__eq__(other)
346
347
348 class BoundArguments(object):
349 '''Result of `Signature.bind` call. Holds the mapping of arguments
350 to the function's parameters.
351
352 Has the following public attributes:
353
354 * arguments : OrderedDict
355 An ordered mutable mapping of parameters' names to arguments' values.
356 Does not contain arguments' default values.
357 * signature : Signature
358 The Signature object that created this instance.
359 * args : tuple
360 Tuple of positional arguments values.
361 * kwargs : dict
362 Dict of keyword arguments values.
363 '''
364
365 def __init__(self, signature, arguments):
366 self.arguments = arguments
367 self._signature = signature
368
369 @property
370 def signature(self):
371 return self._signature
372
373 @property
374 def args(self):
375 args = []
376 for param_name, param in self._signature.parameters.items():
377 if (param.kind in (_VAR_KEYWORD, _KEYWORD_ONLY) or
378 param._partial_kwarg):
379 # Keyword arguments mapped by 'functools.partial'
380 # (Parameter._partial_kwarg is True) are mapped
381 # in 'BoundArguments.kwargs', along with VAR_KEYWORD &
382 # KEYWORD_ONLY
383 break
384
385 try:
386 arg = self.arguments[param_name]
387 except KeyError:
388 # We're done here. Other arguments
389 # will be mapped in 'BoundArguments.kwargs'
390 break
391 else:
392 if param.kind == _VAR_POSITIONAL:
393 # *args
394 args.extend(arg)
395 else:
396 # plain argument
397 args.append(arg)
398
399 return tuple(args)
400
401 @property
402 def kwargs(self):
403 kwargs = {}
404 kwargs_started = False
405 for param_name, param in self._signature.parameters.items():
406 if not kwargs_started:
407 if (param.kind in (_VAR_KEYWORD, _KEYWORD_ONLY) or
408 param._partial_kwarg):
409 kwargs_started = True
410 else:
411 if param_name not in self.arguments:
412 kwargs_started = True
413 continue
414
415 if not kwargs_started:
416 continue
417
418 try:
419 arg = self.arguments[param_name]
420 except KeyError:
421 pass
422 else:
423 if param.kind == _VAR_KEYWORD:
424 # **kwargs
425 kwargs.update(arg)
426 else:
427 # plain keyword argument
428 kwargs[param_name] = arg
429
430 return kwargs
431
432 def __hash__(self):
433 msg = "unhashable type: '{0}'".format(self.__class__.__name__)
434 raise TypeError(msg)
435
436 def __eq__(self, other):
437 return (issubclass(other.__class__, BoundArguments) and
438 self.signature == other.signature and
439 self.arguments == other.arguments)
440
441 def __ne__(self, other):
442 return not self.__eq__(other)
443
444
445 class Signature(object):
446 '''A Signature object represents the overall signature of a function.
447 It stores a Parameter object for each parameter accepted by the
448 function, as well as information specific to the function itself.
449
450 A Signature object has the following public attributes and methods:
451
452 * parameters : OrderedDict
453 An ordered mapping of parameters' names to the corresponding
454 Parameter objects (keyword-only arguments are in the same order
455 as listed in `code.co_varnames`).
456 * return_annotation : object
457 The annotation for the return type of the function if specified.
458 If the function has no annotation for its return type, this
459 attribute is not set.
460 * bind(*args, **kwargs) -> BoundArguments
461 Creates a mapping from positional and keyword arguments to
462 parameters.
463 * bind_partial(*args, **kwargs) -> BoundArguments
464 Creates a partial mapping from positional and keyword arguments
465 to parameters (simulating 'functools.partial' behavior.)
466 '''
467
468 __slots__ = ('_return_annotation', '_parameters')
469
470 _parameter_cls = Parameter
471 _bound_arguments_cls = BoundArguments
472
473 empty = _empty
474
475 def __init__(self, parameters=None, return_annotation=_empty,
476 __validate_parameters__=True):
477 '''Constructs Signature from the given list of Parameter
478 objects and 'return_annotation'. All arguments are optional.
479 '''
480
481 if parameters is None:
482 params = OrderedDict()
483 else:
484 if __validate_parameters__:
485 params = OrderedDict()
486 top_kind = _POSITIONAL_ONLY
487
488 for idx, param in enumerate(parameters):
489 kind = param.kind
490 if kind < top_kind:
491 msg = 'wrong parameter order: {0} before {1}'
492 msg = msg.format(top_kind, param.kind)
493 raise ValueError(msg)
494 else:
495 top_kind = kind
496
497 name = param.name
498 if name is None:
499 name = str(idx)
500 param = param.replace(name=name)
501
502 if name in params:
503 msg = 'duplicate parameter name: {0!r}'.format(name)
504 raise ValueError(msg)
505 params[name] = param
506 else:
507 params = OrderedDict(((param.name, param)
508 for param in parameters))
509
510 self._parameters = params
511 self._return_annotation = return_annotation
512
513 @classmethod
514 def from_function(cls, func):
515 '''Constructs Signature for the given python function'''
516
517 if not isinstance(func, types.FunctionType):
518 raise TypeError('{0!r} is not a Python function'.format(func))
519
520 Parameter = cls._parameter_cls
521
522 # Parameter information.
523 func_code = func.__code__
524 pos_count = func_code.co_argcount
525 arg_names = func_code.co_varnames
526 positional = tuple(arg_names[:pos_count])
527 keyword_only_count = getattr(func_code, 'co_kwonlyargcount', 0)
528 keyword_only = arg_names[pos_count:(pos_count + keyword_only_count)]
529 annotations = getattr(func, '__annotations__', {})
530 defaults = func.__defaults__
531 kwdefaults = getattr(func, '__kwdefaults__', None)
532
533 if defaults:
534 pos_default_count = len(defaults)
535 else:
536 pos_default_count = 0
537
538 parameters = []
539
540 # Non-keyword-only parameters w/o defaults.
541 non_default_count = pos_count - pos_default_count
542 for name in positional[:non_default_count]:
543 annotation = annotations.get(name, _empty)
544 parameters.append(Parameter(name, annotation=annotation,
545 kind=_POSITIONAL_OR_KEYWORD))
546
547 # ... w/ defaults.
548 for offset, name in enumerate(positional[non_default_count:]):
549 annotation = annotations.get(name, _empty)
550 parameters.append(Parameter(name, annotation=annotation,
551 kind=_POSITIONAL_OR_KEYWORD,
552 default=defaults[offset]))
553
554 # *args
555 if func_code.co_flags & 0x04:
556 name = arg_names[pos_count + keyword_only_count]
557 annotation = annotations.get(name, _empty)
558 parameters.append(Parameter(name, annotation=annotation,
559 kind=_VAR_POSITIONAL))
560
561 # Keyword-only parameters.
562 for name in keyword_only:
563 default = _empty
564 if kwdefaults is not None:
565 default = kwdefaults.get(name, _empty)
566
567 annotation = annotations.get(name, _empty)
568 parameters.append(Parameter(name, annotation=annotation,
569 kind=_KEYWORD_ONLY,
570 default=default))
571 # **kwargs
572 if func_code.co_flags & 0x08:
573 index = pos_count + keyword_only_count
574 if func_code.co_flags & 0x04:
575 index += 1
576
577 name = arg_names[index]
578 annotation = annotations.get(name, _empty)
579 parameters.append(Parameter(name, annotation=annotation,
580 kind=_VAR_KEYWORD))
581
582 return cls(parameters,
583 return_annotation=annotations.get('return', _empty),
584 __validate_parameters__=False)
585
586 @property
587 def parameters(self):
588 try:
589 return types.MappingProxyType(self._parameters)
590 except AttributeError:
591 return OrderedDict(self._parameters.items())
592
593 @property
594 def return_annotation(self):
595 return self._return_annotation
596
597 def replace(self, parameters=_void, return_annotation=_void):
598 '''Creates a customized copy of the Signature.
599 Pass 'parameters' and/or 'return_annotation' arguments
600 to override them in the new copy.
601 '''
602
603 if parameters is _void:
604 parameters = self.parameters.values()
605
606 if return_annotation is _void:
607 return_annotation = self._return_annotation
608
609 return type(self)(parameters,
610 return_annotation=return_annotation)
611
612 def __hash__(self):
613 msg = "unhashable type: '{0}'".format(self.__class__.__name__)
614 raise TypeError(msg)
615
616 def __eq__(self, other):
617 if (not issubclass(type(other), Signature) or
618 self.return_annotation != other.return_annotation or
619 len(self.parameters) != len(other.parameters)):
620 return False
621
622 other_positions = dict((param, idx)
623 for idx, param in enumerate(other.parameters.keys()))
624
625 for idx, (param_name, param) in enumerate(self.parameters.items()):
626 if param.kind == _KEYWORD_ONLY:
627 try:
628 other_param = other.parameters[param_name]
629 except KeyError:
630 return False
631 else:
632 if param != other_param:
633 return False
634 else:
635 try:
636 other_idx = other_positions[param_name]
637 except KeyError:
638 return False
639 else:
640 if (idx != other_idx or
641 param != other.parameters[param_name]):
642 return False
643
644 return True
645
646 def __ne__(self, other):
647 return not self.__eq__(other)
648
649 def _bind(self, args, kwargs, partial=False):
650 '''Private method. Don't use directly.'''
651
652 arguments = OrderedDict()
653
654 parameters = iter(self.parameters.values())
655 parameters_ex = ()
656 arg_vals = iter(args)
657
658 if partial:
659 # Support for binding arguments to 'functools.partial' objects.
660 # See 'functools.partial' case in 'signature()' implementation
661 # for details.
662 for param_name, param in self.parameters.items():
663 if (param._partial_kwarg and param_name not in kwargs):
664 # Simulating 'functools.partial' behavior
665 kwargs[param_name] = param.default
666
667 while True:
668 # Let's iterate through the positional arguments and corresponding
669 # parameters
670 try:
671 arg_val = next(arg_vals)
672 except StopIteration:
673 # No more positional arguments
674 try:
675 param = next(parameters)
676 except StopIteration:
677 # No more parameters. That's it. Just need to check that
678 # we have no `kwargs` after this while loop
679 break
680 else:
681 if param.kind == _VAR_POSITIONAL:
682 # That's OK, just empty *args. Let's start parsing
683 # kwargs
684 break
685 elif param.name in kwargs:
686 if param.kind == _POSITIONAL_ONLY:
687 msg = '{arg!r} parameter is positional only, ' \
688 'but was passed as a keyword'
689 msg = msg.format(arg=param.name)
690 raise TypeError(msg)
691 parameters_ex = (param,)
692 break
693 elif (param.kind == _VAR_KEYWORD or
694 param.default is not _empty):
695 # That's fine too - we have a default value for this
696 # parameter. So, lets start parsing `kwargs`, starting
697 # with the current parameter
698 parameters_ex = (param,)
699 break
700 else:
701 if partial:
702 parameters_ex = (param,)
703 break
704 else:
705 msg = '{arg!r} parameter lacking default value'
706 msg = msg.format(arg=param.name)
707 raise TypeError(msg)
708 else:
709 # We have a positional argument to process
710 try:
711 param = next(parameters)
712 except StopIteration:
713 raise TypeError('too many positional arguments')
714 else:
715 if param.kind in (_VAR_KEYWORD, _KEYWORD_ONLY):
716 # Looks like we have no parameter for this positional
717 # argument
718 raise TypeError('too many positional arguments')
719
720 if param.kind == _VAR_POSITIONAL:
721 # We have an '*args'-like argument, let's fill it with
722 # all positional arguments we have left and move on to
723 # the next phase
724 values = [arg_val]
725 values.extend(arg_vals)
726 arguments[param.name] = tuple(values)
727 break
728
729 if param.name in kwargs:
730 raise TypeError('multiple values for argument '
731 '{arg!r}'.format(arg=param.name))
732
733 arguments[param.name] = arg_val
734
735 # Now, we iterate through the remaining parameters to process
736 # keyword arguments
737 kwargs_param = None
738 for param in itertools.chain(parameters_ex, parameters):
739 if param.kind == _POSITIONAL_ONLY:
740 # This should never happen in case of a properly built
741 # Signature object (but let's have this check here
742 # to ensure correct behaviour just in case)
743 raise TypeError('{arg!r} parameter is positional only, '
744 'but was passed as a keyword'. \
745 format(arg=param.name))
746
747 if param.kind == _VAR_KEYWORD:
748 # Memorize that we have a '**kwargs'-like parameter
749 kwargs_param = param
750 continue
751
752 param_name = param.name
753 try:
754 arg_val = kwargs.pop(param_name)
755 except KeyError:
756 # We have no value for this parameter. It's fine though,
757 # if it has a default value, or it is an '*args'-like
758 # parameter, left alone by the processing of positional
759 # arguments.
760 if (not partial and param.kind != _VAR_POSITIONAL and
761 param.default is _empty):
762 raise TypeError('{arg!r} parameter lacking default value'. \
763 format(arg=param_name))
764
765 else:
766 arguments[param_name] = arg_val
767
768 if kwargs:
769 if kwargs_param is not None:
770 # Process our '**kwargs'-like parameter
771 arguments[kwargs_param.name] = kwargs
772 else:
773 raise TypeError('too many keyword arguments')
774
775 return self._bound_arguments_cls(self, arguments)
776
777 def bind(self, *args, **kwargs):
778 '''Get a BoundArguments object, that maps the passed `args`
779 and `kwargs` to the function's signature. Raises `TypeError`
780 if the passed arguments can not be bound.
781 '''
782 return self._bind(args, kwargs)
783
784 def bind_partial(self, *args, **kwargs):
785 '''Get a BoundArguments object, that partially maps the
786 passed `args` and `kwargs` to the function's signature.
787 Raises `TypeError` if the passed arguments can not be bound.
788 '''
789 return self._bind(args, kwargs, partial=True)
790
791 def __str__(self):
792 result = []
793 render_kw_only_separator = True
794 for idx, param in enumerate(self.parameters.values()):
795 formatted = str(param)
796
797 kind = param.kind
798 if kind == _VAR_POSITIONAL:
799 # OK, we have an '*args'-like parameter, so we won't need
800 # a '*' to separate keyword-only arguments
801 render_kw_only_separator = False
802 elif kind == _KEYWORD_ONLY and render_kw_only_separator:
803 # We have a keyword-only parameter to render and we haven't
804 # rendered an '*args'-like parameter before, so add a '*'
805 # separator to the parameters list ("foo(arg1, *, arg2)" case)
806 result.append('*')
807 # This condition should be only triggered once, so
808 # reset the flag
809 render_kw_only_separator = False
810
811 result.append(formatted)
812
813 rendered = '({0})'.format(', '.join(result))
814
815 if self.return_annotation is not _empty:
816 anno = formatannotation(self.return_annotation)
817 rendered += ' -> {0}'.format(anno)
818
819 return rendered
@@ -1,139 +1,190 b''
1 """Interact with functions using widgets.
1 """Interact with functions using widgets.
2 """
2 """
3
3
4 #-----------------------------------------------------------------------------
4 #-----------------------------------------------------------------------------
5 # Copyright (c) 2013, the IPython Development Team.
5 # Copyright (c) 2013, the IPython Development Team.
6 #
6 #
7 # Distributed under the terms of the Modified BSD License.
7 # Distributed under the terms of the Modified BSD License.
8 #
8 #
9 # The full license is in the file COPYING.txt, distributed with this software.
9 # The full license is in the file COPYING.txt, distributed with this software.
10 #-----------------------------------------------------------------------------
10 #-----------------------------------------------------------------------------
11
11
12 #-----------------------------------------------------------------------------
12 #-----------------------------------------------------------------------------
13 # Imports
13 # Imports
14 #-----------------------------------------------------------------------------
14 #-----------------------------------------------------------------------------
15
15
16 try: # Python >= 3.3
17 from inspect import signature, Parameter
18 except ImportError:
19 from IPython.utils.signatures import signature, Parameter
20
16 from IPython.html.widgets import (Widget, TextWidget,
21 from IPython.html.widgets import (Widget, TextWidget,
17 FloatSliderWidget, IntSliderWidget, CheckboxWidget, DropdownWidget,
22 FloatSliderWidget, IntSliderWidget, CheckboxWidget, DropdownWidget,
18 ContainerWidget)
23 ContainerWidget)
19 from IPython.display import display, clear_output
24 from IPython.display import display, clear_output
20 from IPython.utils.py3compat import string_types, unicode_type
25 from IPython.utils.py3compat import string_types, unicode_type
21
26
22 #-----------------------------------------------------------------------------
27 #-----------------------------------------------------------------------------
23 # Classes and Functions
28 # Classes and Functions
24 #-----------------------------------------------------------------------------
29 #-----------------------------------------------------------------------------
25
30
26
31
27 def _matches(o, pattern):
32 def _matches(o, pattern):
28 if not len(o) == len(pattern):
33 if not len(o) == len(pattern):
29 return False
34 return False
30 comps = zip(o,pattern)
35 comps = zip(o,pattern)
31 return all(isinstance(obj,kind) for obj,kind in comps)
36 return all(isinstance(obj,kind) for obj,kind in comps)
32
37
33
38
34 def _get_min_max_value(min, max, value):
39 def _get_min_max_value(min, max, value):
35 """Return min, max, value given input values with possible None."""
40 """Return min, max, value given input values with possible None."""
36 if value is None:
41 if value is None:
37 if not max > min:
42 if not max > min:
38 raise ValueError('max must be greater than min: (min={0}, max={1})'.format(min, max))
43 raise ValueError('max must be greater than min: (min={0}, max={1})'.format(min, max))
39 value = min + abs(min-max)/2
44 value = min + abs(min-max)/2
40 value = type(min)(value)
45 value = type(min)(value)
41 elif min is None and max is None:
46 elif min is None and max is None:
42 if value == 0.0:
47 if value == 0.0:
43 min, max, value = 0.0, 1.0, 0.5
48 min, max, value = 0.0, 1.0, 0.5
44 elif value == 0:
49 elif value == 0:
45 min, max, value = 0, 1, 0
50 min, max, value = 0, 1, 0
46 elif isinstance(value, float):
51 elif isinstance(value, float):
47 min, max = -value, 3.0*value
52 min, max = -value, 3.0*value
48 elif isinstance(value, int):
53 elif isinstance(value, int):
49 min, max = -value, 3*value
54 min, max = -value, 3*value
50 else:
55 else:
51 raise TypeError('expected a number, got: %r' % number)
56 raise TypeError('expected a number, got: %r' % value)
52 else:
57 else:
53 raise ValueError('unable to infer range, value from: ({0}, {1}, {2})'.format(min, max, value))
58 raise ValueError('unable to infer range, value from: ({0}, {1}, {2})'.format(min, max, value))
54 return min, max, value
59 return min, max, value
55
60
56
61 def _widget_abbrev_single_value(o):
57 def _widget_abbrev(o):
62 """Make widgets from single values, which can be used written as parameter defaults."""
58 if isinstance(o, string_types):
63 if isinstance(o, string_types):
59 return TextWidget(value=unicode_type(o))
64 return TextWidget(value=unicode_type(o))
60 elif isinstance(o, dict):
65 elif isinstance(o, dict):
61 labels = [unicode_type(k) for k in o]
66 labels = [unicode_type(k) for k in o]
62 values = o.values()
67 values = o.values()
63 w = DropdownWidget(value=values[0], values=values, labels=labels)
68 w = DropdownWidget(value=values[0], values=values, labels=labels)
64 return w
69 return w
65 # Special case float and int == 0.0
70 # Special case float and int == 0.0
66 # get_range(value):
71 # get_range(value):
67 elif isinstance(o, bool):
72 elif isinstance(o, bool):
68 return CheckboxWidget(value=o)
73 return CheckboxWidget(value=o)
69 elif isinstance(o, float):
74 elif isinstance(o, float):
70 min, max, value = _get_min_max_value(None, None, o)
75 min, max, value = _get_min_max_value(None, None, o)
71 return FloatSliderWidget(value=o, min=min, max=max)
76 return FloatSliderWidget(value=o, min=min, max=max)
72 elif isinstance(o, int):
77 elif isinstance(o, int):
73 min, max, value = _get_min_max_value(None, None, o)
78 min, max, value = _get_min_max_value(None, None, o)
74 return IntSliderWidget(value=o, min=min, max=max)
79 return IntSliderWidget(value=o, min=min, max=max)
80
81 def _widget_abbrev(o):
82 """Make widgets from abbreviations: single values, lists or tuples."""
75 if isinstance(o, (list, tuple)):
83 if isinstance(o, (list, tuple)):
76 if _matches(o, (int, int)):
84 if _matches(o, (int, int)):
77 min, max, value = _get_min_max_value(o[0], o[1], None)
85 min, max, value = _get_min_max_value(o[0], o[1], None)
78 return IntSliderWidget(value=value, min=min, max=max)
86 return IntSliderWidget(value=value, min=min, max=max)
79 elif _matches(o, (int, int, int)):
87 elif _matches(o, (int, int, int)):
80 min, max, value = _get_min_max_value(o[0], o[1], None)
88 min, max, value = _get_min_max_value(o[0], o[1], None)
81 return IntSliderWidget(value=value, min=min, max=max, step=o[2])
89 return IntSliderWidget(value=value, min=min, max=max, step=o[2])
82 elif _matches(o, (float, float)):
90 elif _matches(o, (float, float)):
83 min, max, value = _get_min_max_value(o[0], o[1], None)
91 min, max, value = _get_min_max_value(o[0], o[1], None)
84 return FloatSliderWidget(value=value, min=min, max=max)
92 return FloatSliderWidget(value=value, min=min, max=max)
85 elif _matches(o, (float, float, float)):
93 elif _matches(o, (float, float, float)):
86 min, max, value = _get_min_max_value(o[0], o[1], None)
94 min, max, value = _get_min_max_value(o[0], o[1], None)
87 return FloatSliderWidget(value=value, min=min, max=max, step=o[2])
95 return FloatSliderWidget(value=value, min=min, max=max, step=o[2])
88 elif _matches(o, (float, float, int)):
96 elif _matches(o, (float, float, int)):
89 min, max, value = _get_min_max_value(o[0], o[1], None)
97 min, max, value = _get_min_max_value(o[0], o[1], None)
90 return FloatSliderWidget(value=value, min=min, max=max, step=float(o[2]))
98 return FloatSliderWidget(value=value, min=min, max=max, step=float(o[2]))
91 elif all(isinstance(x, string_types) for x in o):
99 elif all(isinstance(x, string_types) for x in o):
92 return DropdownWidget(value=unicode_type(o[0]),
100 return DropdownWidget(value=unicode_type(o[0]),
93 values=[unicode_type(k) for k in o])
101 values=[unicode_type(k) for k in o])
94
102
103 else:
104 return _widget_abbrev_single_value(o)
105
106 def _widget_or_abbrev(value):
107 if isinstance(value, Widget):
108 return value
109
110 widget = _widget_abbrev(value)
111 if widget is None:
112 raise ValueError("%r cannot be transformed to a Widget" % value)
113 return widget
114
115 def _widget_for_param(param, kwargs):
116 """Get a widget for a parameter.
117
118 We look for, in this order:
119 - keyword arguments passed to interact[ive]() that match the parameter name.
120 - function annotations
121 - default values
122
123 Returns an instance of Widget, or None if nothing suitable is found.
124
125 Raises ValueError if the kwargs or annotation value cannot be made into
126 a widget.
127 """
128 if param.name in kwargs:
129 return _widget_or_abbrev(kwargs.pop(param.name))
130
131 if param.annotation is not Parameter.empty:
132 return _widget_or_abbrev(param.annotation)
133
134 if param.default is not Parameter.empty:
135 # Returns None if it's not suitable
136 return _widget_abbrev_single_value(param.default)
137
138 return None
95
139
96 def interactive(f, **kwargs):
140 def interactive(f, **kwargs):
97 """Interact with a function using widgets."""
141 """Build a group of widgets for setting the inputs to a function."""
98
142
99 co = kwargs.pop('clear_output', True)
143 co = kwargs.pop('clear_output', True)
100 # First convert all args to Widget instances
144 # First convert all args to Widget instances
101 widgets = []
145 widgets = []
102 container = ContainerWidget()
146 container = ContainerWidget()
103 container.result = None
147 container.result = None
104 container.kwargs = dict()
148 container.kwargs = dict()
105 for key, value in kwargs.items():
149
106 if isinstance(value, Widget):
150 # Extract parameters from the function signature
107 widget = value
151 for param in signature(f).parameters.values():
108 else:
152 param_widget = _widget_for_param(param, kwargs)
109 widget = _widget_abbrev(value)
153 if param_widget is not None:
110 if widget is None:
154 param_widget.description = param.name
111 raise ValueError("Object cannot be transformed to a Widget")
155 widgets.append(param_widget)
112 widget.description = key
156
113 widgets.append((key,widget))
157 # Extra parameters from keyword args - we assume f takes **kwargs
114 widgets.sort(key=lambda e: e[1].__class__.__name__)
158 for name, value in sorted(kwargs.items(), key = lambda x: x[0]):
115 container.children = [e[1] for e in widgets]
159 widget = _widget_or_abbrev(value)
160 widget.description = name
161 widgets.append(widget)
162
163 # This has to be done as an assignment, not using container.children.append,
164 # so that traitlets notices the update.
165 container.children = widgets
116
166
117 # Build the callback
167 # Build the callback
118 def call_f(name, old, new):
168 def call_f(name, old, new):
119 actual_kwargs = {}
169 actual_kwargs = {}
120 for key, widget in widgets:
170 for widget in widgets:
121 value = widget.value
171 value = widget.value
122 container.kwargs[key] = value
172 container.kwargs[widget.description] = value
123 actual_kwargs[key] = value
173 actual_kwargs[widget.description] = value
124 if co:
174 if co:
125 clear_output(wait=True)
175 clear_output(wait=True)
126 container.result = f(**actual_kwargs)
176 container.result = f(**actual_kwargs)
127
177
128 # Wire up the widgets
178 # Wire up the widgets
129 for key, widget in widgets:
179 for widget in widgets:
130 widget.on_trait_change(call_f, 'value')
180 widget.on_trait_change(call_f, 'value')
131
181
132 container.on_displayed(lambda _: call_f(None, None, None))
182 container.on_displayed(lambda _: call_f(None, None, None))
133
183
134 return container
184 return container
135
185
136 def interact(f, **kwargs):
186 def interact(f, **kwargs):
187 """Interact with a function using widgets."""
137 w = interactive(f, **kwargs)
188 w = interactive(f, **kwargs)
138 f.widget = w
189 f.widget = w
139 display(w)
190 display(w)
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
General Comments 0
You need to be logged in to leave comments. Login now