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 | 1 | """Interact with functions using widgets. |
|
2 | 2 | """ |
|
3 | 3 | |
|
4 | 4 | #----------------------------------------------------------------------------- |
|
5 | 5 | # Copyright (c) 2013, the IPython Development Team. |
|
6 | 6 | # |
|
7 | 7 | # Distributed under the terms of the Modified BSD License. |
|
8 | 8 | # |
|
9 | 9 | # The full license is in the file COPYING.txt, distributed with this software. |
|
10 | 10 | #----------------------------------------------------------------------------- |
|
11 | 11 | |
|
12 | 12 | #----------------------------------------------------------------------------- |
|
13 | 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 | 21 | from IPython.html.widgets import (Widget, TextWidget, |
|
17 | 22 | FloatSliderWidget, IntSliderWidget, CheckboxWidget, DropdownWidget, |
|
18 | 23 | ContainerWidget) |
|
19 | 24 | from IPython.display import display, clear_output |
|
20 | 25 | from IPython.utils.py3compat import string_types, unicode_type |
|
21 | 26 | |
|
22 | 27 | #----------------------------------------------------------------------------- |
|
23 | 28 | # Classes and Functions |
|
24 | 29 | #----------------------------------------------------------------------------- |
|
25 | 30 | |
|
26 | 31 | |
|
27 | 32 | def _matches(o, pattern): |
|
28 | 33 | if not len(o) == len(pattern): |
|
29 | 34 | return False |
|
30 | 35 | comps = zip(o,pattern) |
|
31 | 36 | return all(isinstance(obj,kind) for obj,kind in comps) |
|
32 | 37 | |
|
33 | 38 | |
|
34 | 39 | def _get_min_max_value(min, max, value): |
|
35 | 40 | """Return min, max, value given input values with possible None.""" |
|
36 | 41 | if value is None: |
|
37 | 42 | if not max > min: |
|
38 | 43 | raise ValueError('max must be greater than min: (min={0}, max={1})'.format(min, max)) |
|
39 | 44 | value = min + abs(min-max)/2 |
|
40 | 45 | value = type(min)(value) |
|
41 | 46 | elif min is None and max is None: |
|
42 | 47 | if value == 0.0: |
|
43 | 48 | min, max, value = 0.0, 1.0, 0.5 |
|
44 | 49 | elif value == 0: |
|
45 | 50 | min, max, value = 0, 1, 0 |
|
46 | 51 | elif isinstance(value, float): |
|
47 | 52 | min, max = -value, 3.0*value |
|
48 | 53 | elif isinstance(value, int): |
|
49 | 54 | min, max = -value, 3*value |
|
50 | 55 | else: |
|
51 |
raise TypeError('expected a number, got: %r' % |
|
|
56 | raise TypeError('expected a number, got: %r' % value) | |
|
52 | 57 | else: |
|
53 | 58 | raise ValueError('unable to infer range, value from: ({0}, {1}, {2})'.format(min, max, value)) |
|
54 | 59 | return min, max, value |
|
55 | 60 | |
|
56 | ||
|
57 | def _widget_abbrev(o): | |
|
61 | def _widget_abbrev_single_value(o): | |
|
62 | """Make widgets from single values, which can be used written as parameter defaults.""" | |
|
58 | 63 | if isinstance(o, string_types): |
|
59 | 64 | return TextWidget(value=unicode_type(o)) |
|
60 | 65 | elif isinstance(o, dict): |
|
61 | 66 | labels = [unicode_type(k) for k in o] |
|
62 | 67 | values = o.values() |
|
63 | 68 | w = DropdownWidget(value=values[0], values=values, labels=labels) |
|
64 | 69 | return w |
|
65 | 70 | # Special case float and int == 0.0 |
|
66 | 71 | # get_range(value): |
|
67 | 72 | elif isinstance(o, bool): |
|
68 | 73 | return CheckboxWidget(value=o) |
|
69 | 74 | elif isinstance(o, float): |
|
70 | 75 | min, max, value = _get_min_max_value(None, None, o) |
|
71 | 76 | return FloatSliderWidget(value=o, min=min, max=max) |
|
72 | 77 | elif isinstance(o, int): |
|
73 | 78 | min, max, value = _get_min_max_value(None, None, o) |
|
74 | 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 | 83 | if isinstance(o, (list, tuple)): |
|
76 | 84 | if _matches(o, (int, int)): |
|
77 | 85 | min, max, value = _get_min_max_value(o[0], o[1], None) |
|
78 | 86 | return IntSliderWidget(value=value, min=min, max=max) |
|
79 | 87 | elif _matches(o, (int, int, int)): |
|
80 | 88 | min, max, value = _get_min_max_value(o[0], o[1], None) |
|
81 | 89 | return IntSliderWidget(value=value, min=min, max=max, step=o[2]) |
|
82 | 90 | elif _matches(o, (float, float)): |
|
83 | 91 | min, max, value = _get_min_max_value(o[0], o[1], None) |
|
84 | 92 | return FloatSliderWidget(value=value, min=min, max=max) |
|
85 | 93 | elif _matches(o, (float, float, float)): |
|
86 | 94 | min, max, value = _get_min_max_value(o[0], o[1], None) |
|
87 | 95 | return FloatSliderWidget(value=value, min=min, max=max, step=o[2]) |
|
88 | 96 | elif _matches(o, (float, float, int)): |
|
89 | 97 | min, max, value = _get_min_max_value(o[0], o[1], None) |
|
90 | 98 | return FloatSliderWidget(value=value, min=min, max=max, step=float(o[2])) |
|
91 | 99 | elif all(isinstance(x, string_types) for x in o): |
|
92 | 100 | return DropdownWidget(value=unicode_type(o[0]), |
|
93 | 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 | 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 | 143 | co = kwargs.pop('clear_output', True) |
|
100 | 144 | # First convert all args to Widget instances |
|
101 | 145 | widgets = [] |
|
102 | 146 | container = ContainerWidget() |
|
103 | 147 | container.result = None |
|
104 | 148 | container.kwargs = dict() |
|
105 | for key, value in kwargs.items(): | |
|
106 | if isinstance(value, Widget): | |
|
107 | widget = value | |
|
108 | else: | |
|
109 | widget = _widget_abbrev(value) | |
|
110 | if widget is None: | |
|
111 | raise ValueError("Object cannot be transformed to a Widget") | |
|
112 | widget.description = key | |
|
113 | widgets.append((key,widget)) | |
|
114 | widgets.sort(key=lambda e: e[1].__class__.__name__) | |
|
115 | container.children = [e[1] for e in widgets] | |
|
149 | ||
|
150 | # Extract parameters from the function signature | |
|
151 | for param in signature(f).parameters.values(): | |
|
152 | param_widget = _widget_for_param(param, kwargs) | |
|
153 | if param_widget is not None: | |
|
154 | param_widget.description = param.name | |
|
155 | widgets.append(param_widget) | |
|
156 | ||
|
157 | # Extra parameters from keyword args - we assume f takes **kwargs | |
|
158 | for name, value in sorted(kwargs.items(), key = lambda x: x[0]): | |
|
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 | 167 | # Build the callback |
|
118 | 168 | def call_f(name, old, new): |
|
119 | 169 | actual_kwargs = {} |
|
120 |
for |
|
|
170 | for widget in widgets: | |
|
121 | 171 | value = widget.value |
|
122 |
container.kwargs[ |
|
|
123 |
actual_kwargs[ |
|
|
172 | container.kwargs[widget.description] = value | |
|
173 | actual_kwargs[widget.description] = value | |
|
124 | 174 | if co: |
|
125 | 175 | clear_output(wait=True) |
|
126 | 176 | container.result = f(**actual_kwargs) |
|
127 | 177 | |
|
128 | 178 | # Wire up the widgets |
|
129 |
for |
|
|
179 | for widget in widgets: | |
|
130 | 180 | widget.on_trait_change(call_f, 'value') |
|
131 | 181 | |
|
132 | 182 | container.on_displayed(lambda _: call_f(None, None, None)) |
|
133 | 183 | |
|
134 | 184 | return container |
|
135 | 185 | |
|
136 | 186 | def interact(f, **kwargs): |
|
187 | """Interact with a function using widgets.""" | |
|
137 | 188 | w = interactive(f, **kwargs) |
|
138 | 189 | f.widget = w |
|
139 | 190 | display(w) |
|
1 | 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