utils.py 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229
  1. # -*- coding: utf-8 -*-
  2. '''stuf utilities.'''
  3. from uuid import uuid4
  4. from threading import Lock
  5. from pickletools import genops
  6. from itertools import count, repeat
  7. from functools import update_wrapper, partial
  8. from .base import importer, first, norm
  9. from .six import (
  10. PY3, items, isstring, func_code, b, next, intern, rcompile, pickle, u)
  11. # first slug pattern
  12. one = partial(rcompile(r'[^\w\s-]').sub, '')
  13. # second slug pattern
  14. two = partial(rcompile(r'[-\s]+').sub, '-')
  15. # counter
  16. count = partial(next, count())
  17. # light weight range
  18. lrange = partial(repeat, None)
  19. # unique identifier selection
  20. unique_id = lambda: b(uuid4().hex.upper())
  21. # return one or all values
  22. oneorall = lambda value: value[0] if len(value) == 1 else value
  23. def diff(current, past):
  24. '''Difference between `past` and `current` ``dicts``.'''
  25. intersect = set(current).intersection(set(past))
  26. changed = set(o for o in intersect if past[o] != current[o])
  27. return dict((k, v) for k, v in items(current) if k in changed)
  28. def lazyimport(path, attribute=None, i=importer, s=isstring):
  29. '''
  30. Deferred module loader.
  31. :argument path: something to load
  32. :keyword str attribute: attribute on loaded module to return
  33. '''
  34. return importer(path, attribute) if s(path) else path
  35. # import loader
  36. lazyload = partial(
  37. lambda y, z, x: y(x) if z(x) and '.' in x else x,
  38. lazyimport,
  39. isstring,
  40. )
  41. def lru(maxsize=100):
  42. '''
  43. Least-recently-used cache decorator.
  44. If *maxsize* is set to None, the LRU features are disabled and the cache
  45. can grow without bound.
  46. Arguments to the cached function must be hashable.
  47. Access the underlying function with f.__wrapped__.
  48. See: http://en.wikipedia.org/wiki/Cache_algorithms#Least_Recently_Used
  49. By Raymond Hettinger
  50. '''
  51. def decorator(call):
  52. cache = dict()
  53. items_ = items
  54. repr_ = repr
  55. intern_ = intern
  56. # bound method to lookup key or return None
  57. cache_get = cache.get
  58. # localize the global len() function
  59. len_ = len
  60. # because linkedlist updates aren't threadsafe
  61. lock = Lock()
  62. # root of the circular doubly linked list
  63. root = []
  64. # make updateable non-locally
  65. nonlocal_root = [root]
  66. # initialize by pointing to self
  67. root[:] = [root, root, None, None]
  68. # names for the link fields
  69. PREV, NEXT, KEY, RESULT = 0, 1, 2, 3
  70. if maxsize is None:
  71. def wrapper(*args, **kw):
  72. # simple caching without ordering or size limit
  73. key = repr_(args, items_(kw)) if kw else repr_(args)
  74. # root used here as a unique not-found sentinel
  75. result = cache_get(key, root)
  76. if result is not root:
  77. return result
  78. result = call(*args, **kw)
  79. cache[intern_(key)] = result
  80. return result
  81. else:
  82. def wrapper(*args, **kw):
  83. # size limited caching that tracks accesses by recency
  84. key = repr_(args, items_(kw)) if kw else repr_(args)
  85. with lock:
  86. link = cache_get(key)
  87. if link is not None:
  88. # record recent use of the key by moving it to the
  89. # front of the list
  90. root, = nonlocal_root
  91. link_prev, link_next, key, result = link
  92. link_prev[NEXT] = link_next
  93. link_next[PREV] = link_prev
  94. last = root[PREV]
  95. last[NEXT] = root[PREV] = link
  96. link[PREV] = last
  97. link[NEXT] = root
  98. return result
  99. result = call(*args, **kw)
  100. with lock:
  101. root = first(nonlocal_root)
  102. if len_(cache) < maxsize:
  103. # put result in a new link at the front of the list
  104. last = root[PREV]
  105. link = [last, root, key, result]
  106. cache[intern_(key)] = last[NEXT] = root[PREV] = link
  107. else:
  108. # use root to store the new key and result
  109. root[KEY] = key
  110. root[RESULT] = result
  111. cache[intern_(key)] = root
  112. # empty the oldest link and make it the new root
  113. root = nonlocal_root[0] = root[NEXT]
  114. del cache[root[KEY]]
  115. root[KEY] = None
  116. root[RESULT] = None
  117. return result
  118. def clear():
  119. # clear the cache and cache statistics
  120. with lock:
  121. cache.clear()
  122. root = first(nonlocal_root)
  123. root[:] = [root, root, None, None]
  124. wrapper.__wrapped__ = call
  125. wrapper.clear = clear
  126. try:
  127. return update_wrapper(wrapper, call)
  128. except AttributeError:
  129. return wrapper
  130. return decorator
  131. def memoize(f, i=intern, z=items, r=repr, uw=update_wrapper):
  132. '''Memoize function.'''
  133. f.cache = {}.setdefault
  134. if func_code(f).co_argcount == 1:
  135. memoize_ = lambda arg: f.cache(i(r(arg)), f(arg))
  136. else:
  137. def memoize_(*args, **kw): # @IgnorePep8
  138. return f.setdefault(
  139. i(r(args, z(kw)) if kw else r(args)), f(*args, **kw)
  140. )
  141. return uw(f, memoize_)
  142. def optimize(
  143. obj,
  144. S=StopIteration,
  145. b_=b,
  146. d=pickle.dumps,
  147. g=genops,
  148. n=next,
  149. p=pickle.HIGHEST_PROTOCOL,
  150. s=set,
  151. ):
  152. '''
  153. Optimize a pickle string by removing unused PUT opcodes.
  154. Raymond Hettinger Python cookbook recipe # 545418
  155. '''
  156. # set of args used by a GET opcode
  157. this = d(obj, p)
  158. gets = s()
  159. # (arg, startpos, stoppos) for the PUT opcodes set to pos if previous
  160. # opcode was a PUT
  161. def iterthing(gadd=gets.add, this=this, g=g, n=n): # @IgnorePep8
  162. prevpos, prevarg = None, None
  163. try:
  164. nextr = g(this)
  165. while 1:
  166. opcode, arg, pos = n(nextr)
  167. if prevpos is not None:
  168. yield prevarg, prevpos, pos
  169. prevpos = None
  170. if 'PUT' in opcode.name:
  171. prevarg, prevpos = arg, pos
  172. elif 'GET' in opcode.name:
  173. gadd(arg)
  174. except S:
  175. pass
  176. # copy the pickle string except for PUTS without a corresponding GET
  177. def iterthingy(iterthing=iterthing(), this=this, n=n): # @IgnorePep8
  178. i = 0
  179. try:
  180. while 1:
  181. arg, start, stop = n(iterthing)
  182. yield this[i:stop if (arg in gets) else start]
  183. i = stop
  184. except S:
  185. pass
  186. yield this[i:]
  187. return b_('').join(iterthingy())
  188. moptimize = memoize(optimize)
  189. if PY3:
  190. ld = loads = memoize(lambda x: pickle.loads(x, encoding='latin-1'))
  191. def sluggify(value, n=norm, o=one, t=two):
  192. '''
  193. Normalize `value`, convert to lowercase, remove non-alpha characters,
  194. and convert spaces to hyphens.
  195. '''
  196. return t(o(n(value)).strip().lower())
  197. else:
  198. ld = loads = memoize(lambda x: pickle.loads(x))
  199. def sluggify(value, n=norm, o=one, t=two):
  200. '''
  201. Normalize `value`, convert to lowercase, remove non-alpha characters,
  202. and convert spaces to hyphens.
  203. '''
  204. return t(u(o(n(u(value)).encode('ascii', 'ignore')).strip().lower()))