|
|
- # -*- coding: utf-8 -*-
- """
- Created on Mon Jan 13 18:17:15 2014
-
- @author: takluyver
- """
- import sys
- PY3 = (sys.version_info[0] >= 3)
-
- try:
- from inspect import signature, Parameter # Python >= 3.3
- except ImportError:
- from ._signatures import signature, Parameter
-
- if PY3:
- from functools import wraps
- else:
- from functools import wraps as _wraps
- def wraps(f):
- def dec(func):
- _wraps(f)(func)
- func.__wrapped__ = f
- return func
-
- return dec
-
- def callback_prototype(prototype):
- """Decorator to process a callback prototype.
-
- A callback prototype is a function whose signature includes all the values
- that will be passed by the callback API in question.
-
- The original function will be returned, with a ``prototype.adapt`` attribute
- which can be used to prepare third party callbacks.
- """
- protosig = signature(prototype)
- positional, keyword = [], []
- for name, param in protosig.parameters.items():
- if param.kind in (Parameter.VAR_POSITIONAL, Parameter.VAR_KEYWORD):
- raise TypeError("*args/**kwargs not supported in prototypes")
-
- if (param.default is not Parameter.empty) \
- or (param.kind == Parameter.KEYWORD_ONLY):
- keyword.append(name)
- else:
- positional.append(name)
-
- kwargs = dict.fromkeys(keyword)
- def adapt(callback):
- """Introspect and prepare a third party callback."""
- sig = signature(callback)
- try:
- # XXX: callback can have extra optional parameters - OK?
- sig.bind(*positional, **kwargs)
- return callback
- except TypeError:
- pass
-
- # Match up arguments
- unmatched_pos = positional[:]
- unmatched_kw = kwargs.copy()
- unrecognised = []
- # TODO: unrecognised parameters with default values - OK?
- for name, param in sig.parameters.items():
- # print(name, param.kind) #DBG
- if param.kind == Parameter.POSITIONAL_ONLY:
- if len(unmatched_pos) > 0:
- unmatched_pos.pop(0)
- else:
- unrecognised.append(name)
- elif param.kind == Parameter.POSITIONAL_OR_KEYWORD:
- if (param.default is not Parameter.empty) and (name in unmatched_kw):
- unmatched_kw.pop(name)
- elif len(unmatched_pos) > 0:
- unmatched_pos.pop(0)
- else:
- unrecognised.append(name)
- elif param.kind == Parameter.VAR_POSITIONAL:
- unmatched_pos = []
- elif param.kind == Parameter.KEYWORD_ONLY:
- if name in unmatched_kw:
- unmatched_kw.pop(name)
- else:
- unrecognised.append(name)
- else: # VAR_KEYWORD
- unmatched_kw = {}
-
- # print(unmatched_pos, unmatched_kw, unrecognised) #DBG
-
- if unrecognised:
- raise TypeError("Function {!r} had unmatched arguments: {}".format(callback, unrecognised))
-
- n_positional = len(positional) - len(unmatched_pos)
-
- @wraps(callback)
- def adapted(*args, **kwargs):
- """Wrapper for third party callbacks that discards excess arguments"""
- # print(args, kwargs)
- args = args[:n_positional]
- for name in unmatched_kw:
- # XXX: Could name not be in kwargs?
- kwargs.pop(name)
- # print(args, kwargs, unmatched_pos, cut_positional, unmatched_kw)
- return callback(*args, **kwargs)
-
- return adapted
-
- prototype.adapt = adapt
- return prototype
|