functional.py 11 KB

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