__init__.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685
  1. from __future__ import annotations
  2. import collections.abc
  3. import functools
  4. import inspect
  5. import itertools
  6. import operator
  7. import time
  8. import types
  9. import warnings
  10. from typing import Callable, TypeVar
  11. import more_itertools
  12. def compose(*funcs):
  13. """
  14. Compose any number of unary functions into a single unary function.
  15. Comparable to
  16. `function composition <https://en.wikipedia.org/wiki/Function_composition>`_
  17. in mathematics:
  18. ``h = g ∘ f`` implies ``h(x) = g(f(x))``.
  19. In Python, ``h = compose(g, f)``.
  20. >>> import textwrap
  21. >>> expected = str.strip(textwrap.dedent(compose.__doc__))
  22. >>> strip_and_dedent = compose(str.strip, textwrap.dedent)
  23. >>> strip_and_dedent(compose.__doc__) == expected
  24. True
  25. Compose also allows the innermost function to take arbitrary arguments.
  26. >>> round_three = lambda x: round(x, ndigits=3)
  27. >>> f = compose(round_three, int.__truediv__)
  28. >>> [f(3*x, x+1) for x in range(1,10)]
  29. [1.5, 2.0, 2.25, 2.4, 2.5, 2.571, 2.625, 2.667, 2.7]
  30. """
  31. def compose_two(f1, f2):
  32. return lambda *args, **kwargs: f1(f2(*args, **kwargs))
  33. return functools.reduce(compose_two, funcs)
  34. def once(func):
  35. """
  36. Decorate func so it's only ever called the first time.
  37. This decorator can ensure that an expensive or non-idempotent function
  38. will not be expensive on subsequent calls and is idempotent.
  39. >>> add_three = once(lambda a: a+3)
  40. >>> add_three(3)
  41. 6
  42. >>> add_three(9)
  43. 6
  44. >>> add_three('12')
  45. 6
  46. To reset the stored value, simply clear the property ``saved_result``.
  47. >>> del add_three.saved_result
  48. >>> add_three(9)
  49. 12
  50. >>> add_three(8)
  51. 12
  52. Or invoke 'reset()' on it.
  53. >>> add_three.reset()
  54. >>> add_three(-3)
  55. 0
  56. >>> add_three(0)
  57. 0
  58. """
  59. @functools.wraps(func)
  60. def wrapper(*args, **kwargs):
  61. if not hasattr(wrapper, 'saved_result'):
  62. wrapper.saved_result = func(*args, **kwargs)
  63. return wrapper.saved_result
  64. wrapper.reset = lambda: vars(wrapper).__delitem__('saved_result')
  65. return wrapper
  66. def method_cache(method, cache_wrapper=functools.lru_cache()):
  67. """
  68. Wrap lru_cache to support storing the cache data in the object instances.
  69. Abstracts the common paradigm where the method explicitly saves an
  70. underscore-prefixed protected property on first call and returns that
  71. subsequently.
  72. >>> class MyClass:
  73. ... calls = 0
  74. ...
  75. ... @method_cache
  76. ... def method(self, value):
  77. ... self.calls += 1
  78. ... return value
  79. >>> a = MyClass()
  80. >>> a.method(3)
  81. 3
  82. >>> for x in range(75):
  83. ... res = a.method(x)
  84. >>> a.calls
  85. 75
  86. Note that the apparent behavior will be exactly like that of lru_cache
  87. except that the cache is stored on each instance, so values in one
  88. instance will not flush values from another, and when an instance is
  89. deleted, so are the cached values for that instance.
  90. >>> b = MyClass()
  91. >>> for x in range(35):
  92. ... res = b.method(x)
  93. >>> b.calls
  94. 35
  95. >>> a.method(0)
  96. 0
  97. >>> a.calls
  98. 75
  99. Note that if method had been decorated with ``functools.lru_cache()``,
  100. a.calls would have been 76 (due to the cached value of 0 having been
  101. flushed by the 'b' instance).
  102. Clear the cache with ``.cache_clear()``
  103. >>> a.method.cache_clear()
  104. Same for a method that hasn't yet been called.
  105. >>> c = MyClass()
  106. >>> c.method.cache_clear()
  107. Another cache wrapper may be supplied:
  108. >>> cache = functools.lru_cache(maxsize=2)
  109. >>> MyClass.method2 = method_cache(lambda self: 3, cache_wrapper=cache)
  110. >>> a = MyClass()
  111. >>> a.method2()
  112. 3
  113. Caution - do not subsequently wrap the method with another decorator, such
  114. as ``@property``, which changes the semantics of the function.
  115. See also
  116. http://code.activestate.com/recipes/577452-a-memoize-decorator-for-instance-methods/
  117. for another implementation and additional justification.
  118. """
  119. def wrapper(self, *args, **kwargs):
  120. # it's the first call, replace the method with a cached, bound method
  121. bound_method = types.MethodType(method, self)
  122. cached_method = cache_wrapper(bound_method)
  123. setattr(self, method.__name__, cached_method)
  124. return cached_method(*args, **kwargs)
  125. # Support cache clear even before cache has been created.
  126. wrapper.cache_clear = lambda: None
  127. return _special_method_cache(method, cache_wrapper) or wrapper
  128. def _special_method_cache(method, cache_wrapper):
  129. """
  130. Because Python treats special methods differently, it's not
  131. possible to use instance attributes to implement the cached
  132. methods.
  133. Instead, install the wrapper method under a different name
  134. and return a simple proxy to that wrapper.
  135. https://github.com/jaraco/jaraco.functools/issues/5
  136. """
  137. name = method.__name__
  138. special_names = '__getattr__', '__getitem__'
  139. if name not in special_names:
  140. return None
  141. wrapper_name = '__cached' + name
  142. def proxy(self, /, *args, **kwargs):
  143. if wrapper_name not in vars(self):
  144. bound = types.MethodType(method, self)
  145. cache = cache_wrapper(bound)
  146. setattr(self, wrapper_name, cache)
  147. else:
  148. cache = getattr(self, wrapper_name)
  149. return cache(*args, **kwargs)
  150. return proxy
  151. def apply(transform):
  152. """
  153. Decorate a function with a transform function that is
  154. invoked on results returned from the decorated function.
  155. >>> @apply(reversed)
  156. ... def get_numbers(start):
  157. ... "doc for get_numbers"
  158. ... return range(start, start+3)
  159. >>> list(get_numbers(4))
  160. [6, 5, 4]
  161. >>> get_numbers.__doc__
  162. 'doc for get_numbers'
  163. """
  164. def wrap(func):
  165. return functools.wraps(func)(compose(transform, func))
  166. return wrap
  167. def result_invoke(action):
  168. r"""
  169. Decorate a function with an action function that is
  170. invoked on the results returned from the decorated
  171. function (for its side effect), then return the original
  172. result.
  173. >>> @result_invoke(print)
  174. ... def add_two(a, b):
  175. ... return a + b
  176. >>> x = add_two(2, 3)
  177. 5
  178. >>> x
  179. 5
  180. """
  181. def wrap(func):
  182. @functools.wraps(func)
  183. def wrapper(*args, **kwargs):
  184. result = func(*args, **kwargs)
  185. action(result)
  186. return result
  187. return wrapper
  188. return wrap
  189. def invoke(f, /, *args, **kwargs):
  190. """
  191. Call a function for its side effect after initialization.
  192. The benefit of using the decorator instead of simply invoking a function
  193. after defining it is that it makes explicit the author's intent for the
  194. function to be called immediately. Whereas if one simply calls the
  195. function immediately, it's less obvious if that was intentional or
  196. incidental. It also avoids repeating the name - the two actions, defining
  197. the function and calling it immediately are modeled separately, but linked
  198. by the decorator construct.
  199. The benefit of having a function construct (opposed to just invoking some
  200. behavior inline) is to serve as a scope in which the behavior occurs. It
  201. avoids polluting the global namespace with local variables, provides an
  202. anchor on which to attach documentation (docstring), keeps the behavior
  203. logically separated (instead of conceptually separated or not separated at
  204. all), and provides potential to re-use the behavior for testing or other
  205. purposes.
  206. This function is named as a pithy way to communicate, "call this function
  207. primarily for its side effect", or "while defining this function, also
  208. take it aside and call it". It exists because there's no Python construct
  209. for "define and call" (nor should there be, as decorators serve this need
  210. just fine). The behavior happens immediately and synchronously.
  211. >>> @invoke
  212. ... def func(): print("called")
  213. called
  214. >>> func()
  215. called
  216. Use functools.partial to pass parameters to the initial call
  217. >>> @functools.partial(invoke, name='bingo')
  218. ... def func(name): print('called with', name)
  219. called with bingo
  220. """
  221. f(*args, **kwargs)
  222. return f
  223. class Throttler:
  224. """Rate-limit a function (or other callable)."""
  225. def __init__(self, func, max_rate=float('Inf')):
  226. if isinstance(func, Throttler):
  227. func = func.func
  228. self.func = func
  229. self.max_rate = max_rate
  230. self.reset()
  231. def reset(self):
  232. self.last_called = 0
  233. def __call__(self, *args, **kwargs):
  234. self._wait()
  235. return self.func(*args, **kwargs)
  236. def _wait(self):
  237. """Ensure at least 1/max_rate seconds from last call."""
  238. elapsed = time.time() - self.last_called
  239. must_wait = 1 / self.max_rate - elapsed
  240. time.sleep(max(0, must_wait))
  241. self.last_called = time.time()
  242. def __get__(self, obj, owner=None):
  243. return first_invoke(self._wait, functools.partial(self.func, obj))
  244. def first_invoke(func1, func2):
  245. """
  246. Return a function that when invoked will invoke func1 without
  247. any parameters (for its side effect) and then invoke func2
  248. with whatever parameters were passed, returning its result.
  249. """
  250. def wrapper(*args, **kwargs):
  251. func1()
  252. return func2(*args, **kwargs)
  253. return wrapper
  254. method_caller = first_invoke(
  255. lambda: warnings.warn(
  256. '`jaraco.functools.method_caller` is deprecated, '
  257. 'use `operator.methodcaller` instead',
  258. DeprecationWarning,
  259. stacklevel=3,
  260. ),
  261. operator.methodcaller,
  262. )
  263. def retry_call(func, cleanup=lambda: None, retries=0, trap=()):
  264. """
  265. Given a callable func, trap the indicated exceptions
  266. for up to 'retries' times, invoking cleanup on the
  267. exception. On the final attempt, allow any exceptions
  268. to propagate.
  269. """
  270. attempts = itertools.count() if retries == float('inf') else range(retries)
  271. for _ in attempts:
  272. try:
  273. return func()
  274. except trap:
  275. cleanup()
  276. return func()
  277. def retry(*r_args, **r_kwargs):
  278. """
  279. Decorator wrapper for retry_call. Accepts arguments to retry_call
  280. except func and then returns a decorator for the decorated function.
  281. Ex:
  282. >>> @retry(retries=3)
  283. ... def my_func(a, b):
  284. ... "this is my funk"
  285. ... print(a, b)
  286. >>> my_func.__doc__
  287. 'this is my funk'
  288. """
  289. def decorate(func):
  290. @functools.wraps(func)
  291. def wrapper(*f_args, **f_kwargs):
  292. bound = functools.partial(func, *f_args, **f_kwargs)
  293. return retry_call(bound, *r_args, **r_kwargs)
  294. return wrapper
  295. return decorate
  296. def print_yielded(func):
  297. """
  298. Convert a generator into a function that prints all yielded elements.
  299. >>> @print_yielded
  300. ... def x():
  301. ... yield 3; yield None
  302. >>> x()
  303. 3
  304. None
  305. """
  306. print_all = functools.partial(map, print)
  307. print_results = compose(more_itertools.consume, print_all, func)
  308. return functools.wraps(func)(print_results)
  309. def pass_none(func):
  310. """
  311. Wrap func so it's not called if its first param is None.
  312. >>> print_text = pass_none(print)
  313. >>> print_text('text')
  314. text
  315. >>> print_text(None)
  316. """
  317. @functools.wraps(func)
  318. def wrapper(param, /, *args, **kwargs):
  319. if param is not None:
  320. return func(param, *args, **kwargs)
  321. return None
  322. return wrapper
  323. def assign_params(func, namespace):
  324. """
  325. Assign parameters from namespace where func solicits.
  326. >>> def func(x, y=3):
  327. ... print(x, y)
  328. >>> assigned = assign_params(func, dict(x=2, z=4))
  329. >>> assigned()
  330. 2 3
  331. The usual errors are raised if a function doesn't receive
  332. its required parameters:
  333. >>> assigned = assign_params(func, dict(y=3, z=4))
  334. >>> assigned()
  335. Traceback (most recent call last):
  336. TypeError: func() ...argument...
  337. It even works on methods:
  338. >>> class Handler:
  339. ... def meth(self, arg):
  340. ... print(arg)
  341. >>> assign_params(Handler().meth, dict(arg='crystal', foo='clear'))()
  342. crystal
  343. """
  344. sig = inspect.signature(func)
  345. params = sig.parameters.keys()
  346. call_ns = {k: namespace[k] for k in params if k in namespace}
  347. return functools.partial(func, **call_ns)
  348. def save_method_args(method):
  349. """
  350. Wrap a method such that when it is called, the args and kwargs are
  351. saved on the method.
  352. >>> class MyClass:
  353. ... @save_method_args
  354. ... def method(self, a, b):
  355. ... print(a, b)
  356. >>> my_ob = MyClass()
  357. >>> my_ob.method(1, 2)
  358. 1 2
  359. >>> my_ob._saved_method.args
  360. (1, 2)
  361. >>> my_ob._saved_method.kwargs
  362. {}
  363. >>> my_ob.method(a=3, b='foo')
  364. 3 foo
  365. >>> my_ob._saved_method.args
  366. ()
  367. >>> my_ob._saved_method.kwargs == dict(a=3, b='foo')
  368. True
  369. The arguments are stored on the instance, allowing for
  370. different instance to save different args.
  371. >>> your_ob = MyClass()
  372. >>> your_ob.method({str('x'): 3}, b=[4])
  373. {'x': 3} [4]
  374. >>> your_ob._saved_method.args
  375. ({'x': 3},)
  376. >>> my_ob._saved_method.args
  377. ()
  378. """
  379. args_and_kwargs = collections.namedtuple('args_and_kwargs', 'args kwargs')
  380. @functools.wraps(method)
  381. def wrapper(self, /, *args, **kwargs):
  382. attr_name = '_saved_' + method.__name__
  383. attr = args_and_kwargs(args, kwargs)
  384. setattr(self, attr_name, attr)
  385. return method(self, *args, **kwargs)
  386. return wrapper
  387. def except_(*exceptions, replace=None, use=None):
  388. """
  389. Replace the indicated exceptions, if raised, with the indicated
  390. literal replacement or evaluated expression (if present).
  391. >>> safe_int = except_(ValueError)(int)
  392. >>> safe_int('five')
  393. >>> safe_int('5')
  394. 5
  395. Specify a literal replacement with ``replace``.
  396. >>> safe_int_r = except_(ValueError, replace=0)(int)
  397. >>> safe_int_r('five')
  398. 0
  399. Provide an expression to ``use`` to pass through particular parameters.
  400. >>> safe_int_pt = except_(ValueError, use='args[0]')(int)
  401. >>> safe_int_pt('five')
  402. 'five'
  403. """
  404. def decorate(func):
  405. @functools.wraps(func)
  406. def wrapper(*args, **kwargs):
  407. try:
  408. return func(*args, **kwargs)
  409. except exceptions:
  410. try:
  411. return eval(use)
  412. except TypeError:
  413. return replace
  414. return wrapper
  415. return decorate
  416. def identity(x):
  417. """
  418. Return the argument.
  419. >>> o = object()
  420. >>> identity(o) is o
  421. True
  422. """
  423. return x
  424. def bypass_when(check, *, _op=identity):
  425. """
  426. Decorate a function to return its parameter when ``check``.
  427. >>> bypassed = [] # False
  428. >>> @bypass_when(bypassed)
  429. ... def double(x):
  430. ... return x * 2
  431. >>> double(2)
  432. 4
  433. >>> bypassed[:] = [object()] # True
  434. >>> double(2)
  435. 2
  436. """
  437. def decorate(func):
  438. @functools.wraps(func)
  439. def wrapper(param, /):
  440. return param if _op(check) else func(param)
  441. return wrapper
  442. return decorate
  443. def bypass_unless(check):
  444. """
  445. Decorate a function to return its parameter unless ``check``.
  446. >>> enabled = [object()] # True
  447. >>> @bypass_unless(enabled)
  448. ... def double(x):
  449. ... return x * 2
  450. >>> double(2)
  451. 4
  452. >>> del enabled[:] # False
  453. >>> double(2)
  454. 2
  455. """
  456. return bypass_when(check, _op=operator.not_)
  457. @functools.singledispatch
  458. def _splat_inner(args, func):
  459. """Splat args to func."""
  460. return func(*args)
  461. @_splat_inner.register
  462. def _(args: collections.abc.Mapping, func):
  463. """Splat kargs to func as kwargs."""
  464. return func(**args)
  465. def splat(func):
  466. """
  467. Wrap func to expect its parameters to be passed positionally in a tuple.
  468. Has a similar effect to that of ``itertools.starmap`` over
  469. simple ``map``.
  470. >>> pairs = [(-1, 1), (0, 2)]
  471. >>> more_itertools.consume(itertools.starmap(print, pairs))
  472. -1 1
  473. 0 2
  474. >>> more_itertools.consume(map(splat(print), pairs))
  475. -1 1
  476. 0 2
  477. The approach generalizes to other iterators that don't have a "star"
  478. equivalent, such as a "starfilter".
  479. >>> list(filter(splat(operator.add), pairs))
  480. [(0, 2)]
  481. Splat also accepts a mapping argument.
  482. >>> def is_nice(msg, code):
  483. ... return "smile" in msg or code == 0
  484. >>> msgs = [
  485. ... dict(msg='smile!', code=20),
  486. ... dict(msg='error :(', code=1),
  487. ... dict(msg='unknown', code=0),
  488. ... ]
  489. >>> for msg in filter(splat(is_nice), msgs):
  490. ... print(msg)
  491. {'msg': 'smile!', 'code': 20}
  492. {'msg': 'unknown', 'code': 0}
  493. """
  494. return functools.wraps(func)(functools.partial(_splat_inner, func=func))
  495. _T = TypeVar('_T')
  496. def chainable(method: Callable[[_T, ...], None]) -> Callable[[_T, ...], _T]:
  497. """
  498. Wrap an instance method to always return self.
  499. >>> class Dingus:
  500. ... @chainable
  501. ... def set_attr(self, name, val):
  502. ... setattr(self, name, val)
  503. >>> d = Dingus().set_attr('a', 'eh!')
  504. >>> d.a
  505. 'eh!'
  506. >>> d2 = Dingus().set_attr('a', 'eh!').set_attr('b', 'bee!')
  507. >>> d2.a + d2.b
  508. 'eh!bee!'
  509. Enforces that the return value is null.
  510. >>> class BorkedDingus:
  511. ... @chainable
  512. ... def set_attr(self, name, val):
  513. ... setattr(self, name, val)
  514. ... return len(name)
  515. >>> BorkedDingus().set_attr('a', 'eh!')
  516. Traceback (most recent call last):
  517. ...
  518. AssertionError
  519. """
  520. @functools.wraps(method)
  521. def wrapper(self, *args, **kwargs):
  522. assert method(self, *args, **kwargs) is None
  523. return self
  524. return wrapper