utils.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384
  1. import asyncio
  2. import functools
  3. import inspect
  4. import os
  5. import queue
  6. import random
  7. import socket
  8. import string
  9. import time
  10. from collections import OrderedDict
  11. from contextlib import closing
  12. from os.path import abspath, dirname, join, normpath
  13. project_dir = dirname(abspath(__file__))
  14. STATIC_PATH = join(project_dir, 'html')
  15. def pyinstaller_datas(cli_args=False):
  16. """Return data files included in the PyWebIO to be added to pyinstaller bundle."""
  17. datas = [
  18. (STATIC_PATH, 'pywebio/html'),
  19. (normpath(STATIC_PATH + '/../platform/tpl'), 'pywebio/platform/tpl')
  20. ]
  21. if cli_args:
  22. args = ''
  23. for item in datas:
  24. args += ' --add-data %s%s%s' % (item[0], os.pathsep, item[1])
  25. return args
  26. return datas
  27. class Setter:
  28. """
  29. 可以在对象属性上保存数据。
  30. 访问数据对象不存在的属性时会返回None而不是抛出异常。
  31. """
  32. def __getattribute__(self, name):
  33. try:
  34. return super().__getattribute__(name)
  35. except AttributeError:
  36. return None
  37. class ObjectDictProxy:
  38. """
  39. 通过属性访问的字典。实例不维护底层字典,而是每次在访问时使用回调函数获取
  40. 在对象属性上保存的数据会被保存到底层字典中
  41. 访问数据对象不存在的属性时会返回None而不是抛出异常。
  42. 不能保存下划线开始的属性
  43. 用 ``obj._dict`` 获取对象的字典表示
  44. Example::
  45. d = {}
  46. data = LazyObjectDict(lambda: d)
  47. data.name = "Wang"
  48. data.age = 22
  49. assert data.foo is None
  50. data[10] = "10"
  51. for key in data:
  52. print(key)
  53. assert 'bar' not in data
  54. assert 'name' in data
  55. assert data._dict is d
  56. print(data._dict)
  57. """
  58. def __init__(self, dict_getter):
  59. # 使用 self.__dict__ 避免触发 __setattr__
  60. self.__dict__['_dict_getter'] = dict_getter
  61. @property
  62. def _dict(self):
  63. return self._dict_getter()
  64. def __len__(self):
  65. return len(self._dict)
  66. def __getitem__(self, key):
  67. if key in self._dict:
  68. return self._dict[key]
  69. raise KeyError(key)
  70. def __setitem__(self, key, item):
  71. self._dict[key] = item
  72. def __delitem__(self, key):
  73. del self._dict[key]
  74. def __iter__(self):
  75. return iter(self._dict)
  76. def __contains__(self, key):
  77. return key in self._dict
  78. def __repr__(self):
  79. return repr(self._dict)
  80. def __setattr__(self, key, value):
  81. """
  82. 无论属性是否存在都会被调用
  83. 使用 self.__dict__[name] = value 避免递归
  84. """
  85. assert not key.startswith('_'), "Cannot set attributes starting with underscore"
  86. self._dict.__setitem__(key, value)
  87. def __getattr__(self, item):
  88. """访问一个不存在的属性时触发"""
  89. assert not item.startswith('_'), 'object has no attribute %s' % item
  90. return self._dict.get(item, None)
  91. def __delattr__(self, item):
  92. try:
  93. del self._dict[item]
  94. except KeyError:
  95. pass
  96. class ReadOnlyObjectDict(ObjectDictProxy):
  97. def __delitem__(self, key):
  98. raise NotImplementedError
  99. def __delattr__(self, item):
  100. raise NotImplementedError
  101. def __setitem__(self, key, item):
  102. raise NotImplementedError
  103. def __setattr__(self, key, value):
  104. raise NotImplementedError
  105. def catch_exp_call(func, logger):
  106. """运行函数,将捕获异常记录到日志
  107. :param func: 函数
  108. :param logger: 日志
  109. :return: ``func`` 返回值
  110. """
  111. try:
  112. return func()
  113. except Exception:
  114. logger.exception("Error when invoke `%s`" % func)
  115. def iscoroutinefunction(object):
  116. while isinstance(object, functools.partial):
  117. object = object.func
  118. return asyncio.iscoroutinefunction(object)
  119. def isgeneratorfunction(object):
  120. while isinstance(object, functools.partial):
  121. object = object.func
  122. return inspect.isgeneratorfunction(object)
  123. def get_function_name(func, default=None):
  124. while isinstance(func, functools.partial):
  125. func = func.func
  126. return getattr(func, '__name__', default)
  127. def get_function_doc(func):
  128. """获取函数的doc注释
  129. 如果函数被functools.partial包装,则返回内部原始函数的文档,可以通过设置新函数的 func.__doc__ 属性来更新doc注释
  130. """
  131. partial_doc = inspect.getdoc(functools.partial)
  132. if isinstance(func, functools.partial) and inspect.getdoc(func) == partial_doc:
  133. while isinstance(func, functools.partial):
  134. func = func.func
  135. return inspect.getdoc(func) or ''
  136. def get_function_attr(func, attrs):
  137. """Get the attribute values of the given function, even if the function is decorated by `functools.partial` """
  138. values = {attr: getattr(func, attr) for attr in attrs if hasattr(func, attr)}
  139. while isinstance(func, functools.partial):
  140. func = func.func
  141. values.update({
  142. attr: getattr(func, attr)
  143. for attr in attrs
  144. if hasattr(func, attr) and attr not in values
  145. })
  146. return values
  147. class LimitedSizeQueue(queue.Queue):
  148. """
  149. 有限大小的队列
  150. `get()` 返回全部数据
  151. 队列满时,再 `put()` 会阻塞
  152. """
  153. def get(self):
  154. """获取队列全部数据"""
  155. try:
  156. return super().get(block=False)
  157. except queue.Empty:
  158. return []
  159. def wait_empty(self, timeout=None):
  160. """等待队列内的数据被取走"""
  161. with self.not_full:
  162. if self._qsize() == 0:
  163. return
  164. if timeout is None:
  165. self.not_full.wait()
  166. elif timeout < 0:
  167. raise ValueError("'timeout' must be a non-negative number")
  168. else:
  169. self.not_full.wait(timeout)
  170. def _init(self, maxsize):
  171. self.queue = []
  172. def _qsize(self):
  173. return len(self.queue)
  174. # Put a new item in the queue
  175. def _put(self, item):
  176. self.queue.append(item)
  177. # Get an item from the queue
  178. def _get(self):
  179. all_data = self.queue
  180. self.queue = []
  181. return all_data
  182. async def wait_host_port(host, port, duration=10, delay=2):
  183. """Repeatedly try if a port on a host is open until duration seconds passed
  184. from: https://gist.github.com/betrcode/0248f0fda894013382d7#gistcomment-3161499
  185. :param str host: host ip address or hostname
  186. :param int port: port number
  187. :param int/float duration: Optional. Total duration in seconds to wait, by default 10
  188. :param int/float delay: Optional. Delay in seconds between each try, by default 2
  189. :return: awaitable bool
  190. """
  191. tmax = time.time() + duration
  192. while time.time() < tmax:
  193. try:
  194. _, writer = await asyncio.wait_for(asyncio.open_connection(host, port), timeout=5)
  195. writer.close()
  196. # asyncio.StreamWriter.wait_closed is introduced in py 3.7
  197. # See https://docs.python.org/3/library/asyncio-stream.html#asyncio.StreamWriter.wait_closed
  198. if hasattr(writer, 'wait_closed'):
  199. await writer.wait_closed()
  200. return True
  201. except Exception:
  202. if delay:
  203. await asyncio.sleep(delay)
  204. return False
  205. def get_free_port():
  206. """
  207. pick a free port number
  208. :return int: port number
  209. """
  210. with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as s:
  211. s.bind(('', 0))
  212. s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
  213. return s.getsockname()[1]
  214. def random_str(length=16):
  215. """生成字母和数组组成的随机字符串
  216. :param int length: 字符串长度
  217. """
  218. candidates = string.ascii_letters + string.digits
  219. return ''.join(random.SystemRandom().choice(candidates) for _ in range(length))
  220. def run_as_function(gen):
  221. res = None
  222. while 1:
  223. try:
  224. res = gen.send(res)
  225. except StopIteration as e:
  226. if len(e.args) == 1:
  227. return e.args[0]
  228. return
  229. async def to_coroutine(gen):
  230. res = None
  231. while 1:
  232. try:
  233. c = gen.send(res)
  234. res = await c
  235. except StopIteration as e:
  236. if len(e.args) == 1:
  237. return e.args[0]
  238. return
  239. class LRUDict(OrderedDict):
  240. """
  241. Store items in the order the keys were last recent updated.
  242. The last recent updated item was in end.
  243. The last furthest updated item was in front.
  244. """
  245. def __setitem__(self, key, value):
  246. OrderedDict.__setitem__(self, key, value)
  247. self.move_to_end(key)
  248. _html_value_chars = set(string.ascii_letters + string.digits + '_-')
  249. def check_webio_js():
  250. js_files = [os.path.join(STATIC_PATH, 'js', i) for i in ('pywebio.js', 'pywebio.min.js')]
  251. if any(os.path.isfile(f) for f in js_files):
  252. return
  253. error_msg = """
  254. Error: Missing pywebio.js library for frontend page.
  255. This may be because you cloned or downloaded the project directly from the Git repository.
  256. You Can:
  257. * Manually build the pywebio.js file. See `webiojs/README.md` for more info.
  258. OR
  259. * Use the following command to install the latest development version of PyWebIO:
  260. pip3 install -U https://code.aliyun.com/wang0618/pywebio/repository/archive.zip
  261. """.strip()
  262. raise RuntimeError(error_msg)
  263. def parse_file_size(size):
  264. """Transform file size to byte
  265. :param str/int/float size: 1, '30', '20M', '32k', '16G', '15mb'
  266. :return int: in byte
  267. """
  268. if isinstance(size, (int, float)):
  269. return int(size)
  270. assert isinstance(size, str), '`size` must be int/float/str, got %s' % type(size)
  271. size = size.lower().replace('b', '')
  272. for idx, i in enumerate(['k', 'm', 'g', 't', 'p'], 1):
  273. if i in size:
  274. s = size.replace(i, '')
  275. base = 2 ** (idx * 10)
  276. return int(float(s) * base)
  277. return int(size)
  278. def strip_space(text, n):
  279. """strip n spaces of every line in text"""
  280. lines = (
  281. i[n:] if (i[:n] == ' ' * n) else i
  282. for i in text.splitlines()
  283. )
  284. return '\n'.join(lines)
  285. def check_dom_name_value(value, name='`name`'):
  286. """check the class name / id name of DOM element"""
  287. allowed_chars = set(string.ascii_letters + string.digits + '_-')
  288. if not all(i in allowed_chars for i in value):
  289. raise ValueError(name + " can only contain letters, digits, "
  290. "hyphens ('-') and underscore ('_')")