You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

108 lines
3.7 KiB

4 years ago
  1. # -*- coding: utf-8 -*-
  2. """
  3. Created on Mon Jan 13 18:17:15 2014
  4. @author: takluyver
  5. """
  6. import sys
  7. PY3 = (sys.version_info[0] >= 3)
  8. try:
  9. from inspect import signature, Parameter # Python >= 3.3
  10. except ImportError:
  11. from ._signatures import signature, Parameter
  12. if PY3:
  13. from functools import wraps
  14. else:
  15. from functools import wraps as _wraps
  16. def wraps(f):
  17. def dec(func):
  18. _wraps(f)(func)
  19. func.__wrapped__ = f
  20. return func
  21. return dec
  22. def callback_prototype(prototype):
  23. """Decorator to process a callback prototype.
  24. A callback prototype is a function whose signature includes all the values
  25. that will be passed by the callback API in question.
  26. The original function will be returned, with a ``prototype.adapt`` attribute
  27. which can be used to prepare third party callbacks.
  28. """
  29. protosig = signature(prototype)
  30. positional, keyword = [], []
  31. for name, param in protosig.parameters.items():
  32. if param.kind in (Parameter.VAR_POSITIONAL, Parameter.VAR_KEYWORD):
  33. raise TypeError("*args/**kwargs not supported in prototypes")
  34. if (param.default is not Parameter.empty) \
  35. or (param.kind == Parameter.KEYWORD_ONLY):
  36. keyword.append(name)
  37. else:
  38. positional.append(name)
  39. kwargs = dict.fromkeys(keyword)
  40. def adapt(callback):
  41. """Introspect and prepare a third party callback."""
  42. sig = signature(callback)
  43. try:
  44. # XXX: callback can have extra optional parameters - OK?
  45. sig.bind(*positional, **kwargs)
  46. return callback
  47. except TypeError:
  48. pass
  49. # Match up arguments
  50. unmatched_pos = positional[:]
  51. unmatched_kw = kwargs.copy()
  52. unrecognised = []
  53. # TODO: unrecognised parameters with default values - OK?
  54. for name, param in sig.parameters.items():
  55. # print(name, param.kind) #DBG
  56. if param.kind == Parameter.POSITIONAL_ONLY:
  57. if len(unmatched_pos) > 0:
  58. unmatched_pos.pop(0)
  59. else:
  60. unrecognised.append(name)
  61. elif param.kind == Parameter.POSITIONAL_OR_KEYWORD:
  62. if (param.default is not Parameter.empty) and (name in unmatched_kw):
  63. unmatched_kw.pop(name)
  64. elif len(unmatched_pos) > 0:
  65. unmatched_pos.pop(0)
  66. else:
  67. unrecognised.append(name)
  68. elif param.kind == Parameter.VAR_POSITIONAL:
  69. unmatched_pos = []
  70. elif param.kind == Parameter.KEYWORD_ONLY:
  71. if name in unmatched_kw:
  72. unmatched_kw.pop(name)
  73. else:
  74. unrecognised.append(name)
  75. else: # VAR_KEYWORD
  76. unmatched_kw = {}
  77. # print(unmatched_pos, unmatched_kw, unrecognised) #DBG
  78. if unrecognised:
  79. raise TypeError("Function {!r} had unmatched arguments: {}".format(callback, unrecognised))
  80. n_positional = len(positional) - len(unmatched_pos)
  81. @wraps(callback)
  82. def adapted(*args, **kwargs):
  83. """Wrapper for third party callbacks that discards excess arguments"""
  84. # print(args, kwargs)
  85. args = args[:n_positional]
  86. for name in unmatched_kw:
  87. # XXX: Could name not be in kwargs?
  88. kwargs.pop(name)
  89. # print(args, kwargs, unmatched_pos, cut_positional, unmatched_kw)
  90. return callback(*args, **kwargs)
  91. return adapted
  92. prototype.adapt = adapt
  93. return prototype