functional.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354
  1. # -*- coding: utf-8 -*-
  2. """Functional-style utilties."""
  3. from __future__ import absolute_import, print_function, unicode_literals
  4. import inspect
  5. import sys
  6. from functools import partial
  7. from itertools import chain, islice
  8. from kombu.utils.functional import (
  9. LRUCache, dictfilter, lazy, maybe_evaluate, memoize,
  10. is_list, maybe_list,
  11. )
  12. from vine import promise
  13. from celery.five import UserList, getfullargspec, range
  14. __all__ = [
  15. 'LRUCache', 'is_list', 'maybe_list', 'memoize', 'mlazy', 'noop',
  16. 'first', 'firstmethod', 'chunks', 'padlist', 'mattrgetter', 'uniq',
  17. 'regen', 'dictfilter', 'lazy', 'maybe_evaluate', 'head_from_fun',
  18. 'maybe', 'fun_accepts_kwargs',
  19. ]
  20. IS_PY3 = sys.version_info[0] == 3
  21. FUNHEAD_TEMPLATE = """
  22. def {fun_name}({fun_args}):
  23. return {fun_value}
  24. """
  25. class DummyContext(object):
  26. def __enter__(self):
  27. return self
  28. def __exit__(self, *exc_info):
  29. pass
  30. class mlazy(lazy):
  31. """Memoized lazy evaluation.
  32. The function is only evaluated once, every subsequent access
  33. will return the same value.
  34. """
  35. #: Set to :const:`True` after the object has been evaluated.
  36. evaluated = False
  37. _value = None
  38. def evaluate(self):
  39. if not self.evaluated:
  40. self._value = super(mlazy, self).evaluate()
  41. self.evaluated = True
  42. return self._value
  43. def noop(*args, **kwargs):
  44. """No operation.
  45. Takes any arguments/keyword arguments and does nothing.
  46. """
  47. pass
  48. def pass1(arg, *args, **kwargs):
  49. """Return the first positional argument."""
  50. return arg
  51. def evaluate_promises(it):
  52. for value in it:
  53. if isinstance(value, promise):
  54. value = value()
  55. yield value
  56. def first(predicate, it):
  57. """Return the first element in ``it`` that ``predicate`` accepts.
  58. If ``predicate`` is None it will return the first item that's not
  59. :const:`None`.
  60. """
  61. return next(
  62. (v for v in evaluate_promises(it) if (
  63. predicate(v) if predicate is not None else v is not None)),
  64. None,
  65. )
  66. def firstmethod(method, on_call=None):
  67. """Multiple dispatch.
  68. Return a function that with a list of instances,
  69. finds the first instance that gives a value for the given method.
  70. The list can also contain lazy instances
  71. (:class:`~kombu.utils.functional.lazy`.)
  72. """
  73. def _matcher(it, *args, **kwargs):
  74. for obj in it:
  75. try:
  76. meth = getattr(maybe_evaluate(obj), method)
  77. reply = (on_call(meth, *args, **kwargs) if on_call
  78. else meth(*args, **kwargs))
  79. except AttributeError:
  80. pass
  81. else:
  82. if reply is not None:
  83. return reply
  84. return _matcher
  85. def chunks(it, n):
  86. """Split an iterator into chunks with `n` elements each.
  87. Warning:
  88. ``it`` must be an actual iterator, if you pass this a
  89. concrete sequence will get you repeating elements.
  90. So ``chunks(iter(range(1000)), 10)`` is fine, but
  91. ``chunks(range(1000), 10)`` is not.
  92. Example:
  93. # n == 2
  94. >>> x = chunks(iter([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]), 2)
  95. >>> list(x)
  96. [[0, 1], [2, 3], [4, 5], [6, 7], [8, 9], [10]]
  97. # n == 3
  98. >>> x = chunks(iter([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]), 3)
  99. >>> list(x)
  100. [[0, 1, 2], [3, 4, 5], [6, 7, 8], [9, 10]]
  101. """
  102. for item in it:
  103. yield [item] + list(islice(it, n - 1))
  104. def padlist(container, size, default=None):
  105. """Pad list with default elements.
  106. Example:
  107. >>> first, last, city = padlist(['George', 'Costanza', 'NYC'], 3)
  108. ('George', 'Costanza', 'NYC')
  109. >>> first, last, city = padlist(['George', 'Costanza'], 3)
  110. ('George', 'Costanza', None)
  111. >>> first, last, city, planet = padlist(
  112. ... ['George', 'Costanza', 'NYC'], 4, default='Earth',
  113. ... )
  114. ('George', 'Costanza', 'NYC', 'Earth')
  115. """
  116. return list(container)[:size] + [default] * (size - len(container))
  117. def mattrgetter(*attrs):
  118. """Get attributes, ignoring attribute errors.
  119. Like :func:`operator.itemgetter` but return :const:`None` on missing
  120. attributes instead of raising :exc:`AttributeError`.
  121. """
  122. return lambda obj: {attr: getattr(obj, attr, None) for attr in attrs}
  123. def uniq(it):
  124. """Return all unique elements in ``it``, preserving order."""
  125. seen = set()
  126. return (seen.add(obj) or obj for obj in it if obj not in seen)
  127. def regen(it):
  128. """Convert iterator to an object that can be consumed multiple times.
  129. ``Regen`` takes any iterable, and if the object is an
  130. generator it will cache the evaluated list on first access,
  131. so that the generator can be "consumed" multiple times.
  132. """
  133. if isinstance(it, (list, tuple)):
  134. return it
  135. return _regen(it)
  136. class _regen(UserList, list):
  137. # must be subclass of list so that json can encode.
  138. def __init__(self, it):
  139. # pylint: disable=super-init-not-called
  140. # UserList creates a new list and sets .data, so we don't
  141. # want to call init here.
  142. self.__it = it
  143. self.__index = 0
  144. self.__consumed = []
  145. def __reduce__(self):
  146. return list, (self.data,)
  147. def __length_hint__(self):
  148. return self.__it.__length_hint__()
  149. def __iter__(self):
  150. return chain(self.__consumed, self.__it)
  151. def __getitem__(self, index):
  152. if index < 0:
  153. return self.data[index]
  154. try:
  155. return self.__consumed[index]
  156. except IndexError:
  157. try:
  158. for _ in range(self.__index, index + 1):
  159. self.__consumed.append(next(self.__it))
  160. except StopIteration:
  161. raise IndexError(index)
  162. else:
  163. return self.__consumed[index]
  164. @property
  165. def data(self):
  166. try:
  167. self.__consumed.extend(list(self.__it))
  168. except StopIteration:
  169. pass
  170. return self.__consumed
  171. def _argsfromspec(spec, replace_defaults=True):
  172. if spec.defaults:
  173. split = len(spec.defaults)
  174. defaults = (list(range(len(spec.defaults))) if replace_defaults
  175. else spec.defaults)
  176. positional = spec.args[:-split]
  177. optional = list(zip(spec.args[-split:], defaults))
  178. else:
  179. positional, optional = spec.args, []
  180. varargs = spec.varargs
  181. varkw = spec.varkw
  182. if spec.kwonlydefaults:
  183. split = len(spec.kwonlydefaults)
  184. kwonlyargs = spec.kwonlyargs[:-split]
  185. if replace_defaults:
  186. kwonlyargs_optional = [
  187. (kw, i) for i, kw in enumerate(spec.kwonlyargs[-split:])]
  188. else:
  189. kwonlyargs_optional = list(spec.kwonlydefaults.items())
  190. else:
  191. kwonlyargs, kwonlyargs_optional = spec.kwonlyargs, []
  192. return ', '.join(filter(None, [
  193. ', '.join(positional),
  194. ', '.join('{0}={1}'.format(k, v) for k, v in optional),
  195. '*{0}'.format(varargs) if varargs else None,
  196. '**{0}'.format(varkw) if varkw else None,
  197. '*' if (kwonlyargs or kwonlyargs_optional) and not varargs else None,
  198. ', '.join(kwonlyargs) if kwonlyargs else None,
  199. ', '.join('{0}="{1}"'.format(k, v) for k, v in kwonlyargs_optional),
  200. ]))
  201. def head_from_fun(fun, bound=False, debug=False):
  202. """Generate signature function from actual function."""
  203. # we could use inspect.Signature here, but that implementation
  204. # is very slow since it implements the argument checking
  205. # in pure-Python. Instead we use exec to create a new function
  206. # with an empty body, meaning it has the same performance as
  207. # as just calling a function.
  208. if not inspect.isfunction(fun) and hasattr(fun, '__call__'):
  209. name, fun = fun.__class__.__name__, fun.__call__
  210. else:
  211. name = fun.__name__
  212. definition = FUNHEAD_TEMPLATE.format(
  213. fun_name=name,
  214. fun_args=_argsfromspec(getfullargspec(fun)),
  215. fun_value=1,
  216. )
  217. if debug: # pragma: no cover
  218. print(definition, file=sys.stderr)
  219. namespace = {'__name__': fun.__module__}
  220. # pylint: disable=exec-used
  221. # Tasks are rarely, if ever, created at runtime - exec here is fine.
  222. exec(definition, namespace)
  223. result = namespace[name]
  224. result._source = definition
  225. if bound:
  226. return partial(result, object())
  227. return result
  228. def arity_greater(fun, n):
  229. argspec = getfullargspec(fun)
  230. return argspec.varargs or len(argspec.args) > n
  231. def fun_takes_argument(name, fun, position=None):
  232. spec = getfullargspec(fun)
  233. return (
  234. spec.varkw or spec.varargs or
  235. (len(spec.args) >= position if position else name in spec.args)
  236. )
  237. if hasattr(inspect, 'signature'):
  238. def fun_accepts_kwargs(fun):
  239. """Return true if function accepts arbitrary keyword arguments."""
  240. return any(
  241. p for p in inspect.signature(fun).parameters.values()
  242. if p.kind == p.VAR_KEYWORD
  243. )
  244. else:
  245. def fun_accepts_kwargs(fun): # noqa
  246. """Return true if function accepts arbitrary keyword arguments."""
  247. try:
  248. argspec = inspect.getargspec(fun)
  249. except TypeError:
  250. try:
  251. argspec = inspect.getargspec(fun.__call__)
  252. except (TypeError, AttributeError):
  253. return
  254. return not argspec or argspec[2] is not None
  255. def maybe(typ, val):
  256. """Call typ on value if val is defined."""
  257. return typ(val) if val is not None else val
  258. def seq_concat_item(seq, item):
  259. """Return copy of sequence seq with item added.
  260. Returns:
  261. Sequence: if seq is a tuple, the result will be a tuple,
  262. otherwise it depends on the implementation of ``__add__``.
  263. """
  264. return seq + (item,) if isinstance(seq, tuple) else seq + [item]
  265. def seq_concat_seq(a, b):
  266. """Concatenate two sequences: ``a + b``.
  267. Returns:
  268. Sequence: The return value will depend on the largest sequence
  269. - if b is larger and is a tuple, the return value will be a tuple.
  270. - if a is larger and is a list, the return value will be a list,
  271. """
  272. # find the type of the largest sequence
  273. prefer = type(max([a, b], key=len))
  274. # convert the smallest list to the type of the largest sequence.
  275. if not isinstance(a, prefer):
  276. a = prefer(a)
  277. if not isinstance(b, prefer):
  278. b = prefer(b)
  279. return a + b