__init__.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573
  1. r"""
  2. .. autofunction:: download
  3. .. autofunction:: run_js
  4. .. autofunction:: eval_js
  5. .. autofunction:: register_thread
  6. .. autofunction:: defer_call
  7. .. data:: local
  8. The session-local object for current session.
  9. ``local`` is a dictionary object that can be accessed through attributes,
  10. it aim to be used to save some session-local state of your application.
  11. Attributes of ``local`` are not shared between sessions, each session sees only the attributes it itself placed in there.
  12. :Usage Scenes:
  13. When you need to share some session-independent data with multiple functions,
  14. it is more convenient to use session-local objects to save state than to use function parameters.
  15. Here is a example of a session independent counter implementation::
  16. from pywebio.session import local
  17. def add():
  18. local.cnt = (local.cnt or 0) + 1
  19. def show():
  20. put_text(local.cnt or 0)
  21. def main():
  22. put_buttons(['Add counter', 'Show counter'], [add, show])
  23. The way to pass state through function parameters is::
  24. from functools import partial
  25. def add(cnt):
  26. cnt[0] += 1
  27. def show(cnt):
  28. put_text(cnt[0])
  29. def main():
  30. cnt = [0] # Trick: to pass by reference
  31. put_buttons(['Add counter', 'Show counter'], [partial(add, cnt), partial(show, cnt)])
  32. Of course, you can also use function closures to achieved the same::
  33. def main():
  34. cnt = 0
  35. def add():
  36. nonlocal cnt
  37. cnt += 1
  38. def show():
  39. put_text(cnt)
  40. put_buttons(['Add counter', 'Show counter'], [add, show])
  41. :Operations supported by local object:
  42. ``local`` is a dictionary object that can be accessed through attributes. When accessing a property that does not
  43. exist in the data object, it returns ``None`` instead of throwing an exception. The method of dictionary is not
  44. supported in ``local``. It supports the ``in`` operator to determine whether the key exists. You can use
  45. ``local._dict`` to get the underlying dictionary data.
  46. ::
  47. local.name = "Wang"
  48. local.age = 22
  49. assert local.foo is None
  50. local[10] = "10"
  51. for key in local:
  52. print(key)
  53. assert 'bar' not in local
  54. assert 'name' in local
  55. print(local._dict)
  56. .. versionadded:: 1.1
  57. .. autofunction:: set_env
  58. .. autofunction:: go_app
  59. .. data:: info
  60. The session information data object, whose attributes are:
  61. * ``user_agent`` : The Object of the user browser information, whose attributes are
  62. * ``is_mobile`` (bool): whether user agent is identified as a mobile phone
  63. (iPhone, Android phones, Blackberry, Windows Phone devices etc)
  64. * ``is_tablet`` (bool): whether user agent is identified as a tablet device (iPad, Kindle Fire, Nexus 7 etc)
  65. * ``is_pc`` (bool): whether user agent is identified to be running a traditional "desktop" OS (Windows, OS X, Linux)
  66. * ``is_touch_capable`` (bool): whether user agent has touch capabilities
  67. * ``browser.family`` (str): Browser family. such as 'Mobile Safari'
  68. * ``browser.version`` (tuple): Browser version. such as (5, 1)
  69. * ``browser.version_string`` (str): Browser version string. such as '5.1'
  70. * ``os.family`` (str): User OS family. such as 'iOS'
  71. * ``os.version`` (tuple): User OS version. such as (5, 1)
  72. * ``os.version_string`` (str): User OS version string. such as '5.1'
  73. * ``device.family`` (str): User agent's device family. such as 'iPhone'
  74. * ``device.brand`` (str): Device brand. such as 'Apple'
  75. * ``device.model`` (str): Device model. such as 'iPhone'
  76. * ``user_language`` (str): Language used by the user's operating system. (e.g., ``'zh-CN'``)
  77. * ``server_host`` (str): PyWebIO server host, including domain and port, the port can be omitted when 80.
  78. * ``origin`` (str): Indicate where the user from. Including protocol, host, and port parts.
  79. Such as ``'http://localhost:8080'``. It may be empty, but it is guaranteed to have a value when the user's page
  80. address is not under the server host. (that is, the host, port part are inconsistent with ``server_host``).
  81. * ``user_ip`` (str): User's ip address.
  82. * ``backend`` (str): The current PyWebIO backend server implementation.
  83. The possible values are ``'tornado'``, ``'flask'``, ``'django'`` , ``'aiohttp'`` , ``'starlette'``.
  84. * ``protocol`` (str): The communication protocol between PyWebIO server and browser.
  85. The possible values are ``'websocket'``, ``'http'``
  86. * ``request`` (object): The request object when creating the current session.
  87. Depending on the backend server, the type of ``request`` can be:
  88. * When using Tornado, ``request`` is instance of
  89. `tornado.httputil.HTTPServerRequest <https://www.tornadoweb.org/en/stable/httputil.html#tornado.httputil.HTTPServerRequest>`_
  90. * When using Flask, ``request`` is instance of `flask.Request <https://flask.palletsprojects.com/en/1.1.x/api/#incoming-request-data>`_
  91. * When using Django, ``request`` is instance of `django.http.HttpRequest <https://docs.djangoproject.com/en/3.0/ref/request-response/#django.http.HttpRequest>`_
  92. * When using aiohttp, ``request`` is instance of `aiohttp.web.BaseRequest <https://docs.aiohttp.org/en/stable/web_reference.html#aiohttp.web.BaseRequest>`_
  93. * When using FastAPI/Starlette, ``request`` is instance of `starlette.websockets.WebSocket <https://www.starlette.io/websockets/>`_
  94. The ``user_agent`` attribute of the session information object is parsed by the user-agents library.
  95. See https://github.com/selwin/python-user-agents#usage
  96. .. versionchanged:: 1.2
  97. Added the ``protocol`` attribute.
  98. Example:
  99. .. exportable-codeblock::
  100. :name: get_info
  101. :summary: `session.info` usage
  102. import json
  103. from pywebio.session import info as session_info
  104. put_code(json.dumps({
  105. k: str(getattr(session_info, k))
  106. for k in ['user_agent', 'user_language', 'server_host',
  107. 'origin', 'user_ip', 'backend', 'protocol', 'request']
  108. }, indent=4), 'json')
  109. .. autoclass:: pywebio.session.coroutinebased.TaskHandler
  110. :members:
  111. .. autofunction:: hold
  112. .. autofunction:: run_async
  113. .. autofunction:: run_asyncio_coroutine
  114. """
  115. import threading
  116. from base64 import b64encode
  117. from functools import wraps
  118. import user_agents
  119. from .base import Session
  120. from .coroutinebased import CoroutineBasedSession
  121. from .threadbased import ThreadBasedSession, ScriptModeSession
  122. from ..exceptions import SessionNotFoundException, SessionException
  123. from ..utils import iscoroutinefunction, isgeneratorfunction, run_as_function, to_coroutine, ObjectDictProxy, \
  124. ReadOnlyObjectDict
  125. # 当前进程中正在使用的会话实现的列表
  126. # List of session implementations currently in use
  127. _active_session_cls = []
  128. __all__ = ['run_async', 'run_asyncio_coroutine', 'register_thread', 'hold', 'defer_call', 'data', 'get_info',
  129. 'run_js', 'eval_js', 'download', 'set_env', 'go_app', 'local', 'info']
  130. def register_session_implement(cls):
  131. if cls not in _active_session_cls:
  132. _active_session_cls.append(cls)
  133. return cls
  134. def register_session_implement_for_target(target_func):
  135. """根据target_func函数类型注册会话实现,并返回会话实现
  136. Register the session implementation according to the target_func function type, and return the session implementation"""
  137. if iscoroutinefunction(target_func) or isgeneratorfunction(target_func):
  138. cls = CoroutineBasedSession
  139. else:
  140. cls = ThreadBasedSession
  141. if ScriptModeSession in _active_session_cls:
  142. raise RuntimeError("Already in script mode, can't start server")
  143. if cls not in _active_session_cls:
  144. _active_session_cls.append(cls)
  145. return cls
  146. def get_session_implement():
  147. """获取当前会话实现。仅供内部实现使用。应在会话上下文中调用
  148. Get the current session implementation. For internal implementation use only. Should be called in session context"""
  149. if not _active_session_cls:
  150. _active_session_cls.append(ScriptModeSession)
  151. _start_script_mode_server()
  152. # 当前正在使用的会话实现只有一个
  153. # There is only one session implementation currently in use
  154. if len(_active_session_cls) == 1:
  155. return _active_session_cls[0]
  156. # 当前有多个正在使用的会话实现
  157. # There are currently multiple session implementations in use
  158. for cls in _active_session_cls:
  159. try:
  160. cls.get_current_session()
  161. return cls
  162. except SessionNotFoundException:
  163. pass
  164. raise SessionNotFoundException
  165. def _start_script_mode_server():
  166. from ..platform.tornado import start_server_in_current_thread_session
  167. start_server_in_current_thread_session()
  168. def get_current_session() -> "Session":
  169. return get_session_implement().get_current_session()
  170. def get_current_task_id():
  171. return get_session_implement().get_current_task_id()
  172. def check_session_impl(session_type):
  173. def decorator(func):
  174. """装饰器:在函数调用前检查当前会话实现是否满足要求
  175. Decorator: Check whether the current session implementation meets the requirements before the function call"""
  176. @wraps(func)
  177. def inner(*args, **kwargs):
  178. curr_impl = get_session_implement()
  179. # Check if 'now_impl' is a derived from session_type or is the same class
  180. if not issubclass(curr_impl, session_type):
  181. func_name = getattr(func, '__name__', str(func))
  182. require = getattr(session_type, '__name__', str(session_type))
  183. curr = getattr(curr_impl, '__name__', str(curr_impl))
  184. raise RuntimeError("Only can invoke `{func_name:s}` in {require:s} context."
  185. " You are now in {curr:s} context".format(func_name=func_name, require=require,
  186. curr=curr))
  187. return func(*args, **kwargs)
  188. return inner
  189. return decorator
  190. def chose_impl(gen_func):
  191. """
  192. 装饰器,使用chose_impl对gen_func进行装饰后,gen_func() 调用将根据当前会话实现来确定是 返回协程对象 还是 直接运行函数体
  193. Decorator, after using `choose_impl` to decorate `gen_func`, according to the current session implementation,
  194. the `gen_func()` call will either return the coroutine object or directly run the function body
  195. """
  196. @wraps(gen_func)
  197. def inner(*args, **kwargs):
  198. gen = gen_func(*args, **kwargs)
  199. if get_session_implement() == CoroutineBasedSession:
  200. return to_coroutine(gen)
  201. else:
  202. return run_as_function(gen)
  203. return inner
  204. @chose_impl
  205. def next_client_event():
  206. res = yield get_current_session().next_client_event()
  207. return res
  208. @chose_impl
  209. def hold():
  210. """Keep the session alive until the browser page is closed by user.
  211. .. attention::
  212. Since PyWebIO v1.4, in :ref:`server mode <server_mode>`, it's no need to call this function manually,
  213. PyWebIO will automatically hold the session for you when needed.
  214. The only case to use it is to prevent the application from exiting in scrip mode.
  215. In case you use the previous version of PyWebIO (we strongly recommend that you upgrade to the latest version),
  216. here is the old document for ``hold()``:
  217. After the PyWebIO session closed, the functions that need communicate with the PyWebIO server
  218. (such as the event callback of `put_buttons()` and download link of `put_file()`) will not work.
  219. You can call the ``hold()`` function at the end of the task function to hold the session,
  220. so that the event callback and download link will always be available before the browser page is closed by user.
  221. """
  222. while True:
  223. try:
  224. yield next_client_event()
  225. except SessionException:
  226. return
  227. def download(name, content):
  228. """Send file to user, and the user browser will download the file to the local
  229. :param str name: File name when downloading
  230. :param content: File content. It is a bytes-like object
  231. Example:
  232. .. exportable-codeblock::
  233. :name: download
  234. :summary: `download()` usage
  235. put_button('Click to download', lambda: download('hello-world.txt', b'hello world!'))
  236. """
  237. from ..io_ctrl import send_msg
  238. content = b64encode(content).decode('ascii')
  239. send_msg('download', spec=dict(name=name, content=content))
  240. def run_js(code_, **args):
  241. """Execute JavaScript code in user browser.
  242. The code is run in the browser's JS global scope.
  243. :param str code_: JavaScript code
  244. :param args: Local variables passed to js code. Variables need to be JSON-serializable.
  245. Example::
  246. run_js('console.log(a + b)', a=1, b=2)
  247. """
  248. from ..io_ctrl import send_msg
  249. send_msg('run_script', spec=dict(code=code_, args=args))
  250. @chose_impl
  251. def eval_js(expression_, **args):
  252. """Execute JavaScript expression in the user's browser and get the value of the expression
  253. :param str expression_: JavaScript expression. The value of the expression need to be JSON-serializable.
  254. If the value of the expression is a `promise <https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise>`_,
  255. ``eval_js()`` will wait for the promise to resolve and return the value of it. When the promise is rejected, `None` is returned.
  256. :param args: Local variables passed to js code. Variables need to be JSON-serializable.
  257. :return: The value of the expression.
  258. Note: When using :ref:`coroutine-based session <coroutine_based_session>`,
  259. you need to use the ``await eval_js(expression)`` syntax to call the function.
  260. Example:
  261. .. exportable-codeblock::
  262. :name: eval_js
  263. :summary: `eval_js()` usage
  264. current_url = eval_js("window.location.href")
  265. put_text(current_url) # ..demo-only
  266. ## ----
  267. function_res = eval_js('''(function(){
  268. var a = 1;
  269. a += b;
  270. return a;
  271. })()''', b=100)
  272. put_text(function_res) # ..demo-only
  273. ## ----
  274. promise_res = eval_js('''new Promise(resolve => {
  275. setTimeout(() => {
  276. resolve('Returned inside callback.');
  277. }, 2000);
  278. });''')
  279. put_text(promise_res) # ..demo-only
  280. .. versionchanged:: 1.3
  281. The JS expression support return promise.
  282. """
  283. from ..io_ctrl import send_msg
  284. send_msg('run_script', spec=dict(code=expression_, args=args, eval=True))
  285. res = yield next_client_event()
  286. assert res['event'] == 'js_yield', "Internal Error, please report this bug on " \
  287. "https://github.com/wang0618/PyWebIO/issues"
  288. return res['data']
  289. @check_session_impl(CoroutineBasedSession)
  290. def run_async(coro_obj):
  291. """Run the coroutine object asynchronously. PyWebIO interactive functions are also available in the coroutine.
  292. ``run_async()`` can only be used in :ref:`coroutine-based session <coroutine_based_session>`.
  293. :param coro_obj: Coroutine object
  294. :return: `TaskHandle <pywebio.session.coroutinebased.TaskHandle>` instance,
  295. which can be used to query the running status of the coroutine or close the coroutine.
  296. See also: :ref:`Concurrency in coroutine-based sessions <coroutine_based_concurrency>`
  297. """
  298. return get_current_session().run_async(coro_obj)
  299. @check_session_impl(CoroutineBasedSession)
  300. async def run_asyncio_coroutine(coro_obj):
  301. """
  302. If the thread running sessions are not the same as the thread running the asyncio event loop,
  303. you need to wrap ``run_asyncio_coroutine()`` to run the coroutine in asyncio.
  304. Can only be used in :ref:`coroutine-based session <coroutine_based_session>`.
  305. :param coro_obj: Coroutine object in `asyncio`
  306. Example::
  307. async def app():
  308. put_text('hello')
  309. await run_asyncio_coroutine(asyncio.sleep(1))
  310. put_text('world')
  311. pywebio.platform.flask.start_server(app)
  312. """
  313. return await get_current_session().run_asyncio_coroutine(coro_obj)
  314. @check_session_impl(ThreadBasedSession)
  315. def register_thread(thread: threading.Thread):
  316. """Register the thread so that PyWebIO interactive functions are available in the thread.
  317. Can only be used in the thread-based session.
  318. See :ref:`Concurrent in Server mode <thread_in_server_mode>`
  319. :param threading.Thread thread: Thread object
  320. """
  321. return get_current_session().register_thread(thread)
  322. def defer_call(func):
  323. """Set the function to be called when the session closes.
  324. Whether it is because the user closes the page or the task finishes to cause session closed,
  325. the function set by ``defer_call(func)`` will be executed. Can be used for resource cleaning.
  326. You can call ``defer_call(func)`` multiple times in the session, and the set functions will
  327. be executed sequentially after the session closes.
  328. ``defer_call()`` can also be used as decorator::
  329. @defer_call
  330. def cleanup():
  331. pass
  332. .. attention:: PyWebIO interactive functions cannot be called inside the deferred functions.
  333. """
  334. get_current_session().defer_call(func)
  335. return func
  336. # session-local data object
  337. local = ObjectDictProxy(lambda: get_current_session().save)
  338. def data():
  339. """Get the session-local object of current session.
  340. .. deprecated:: 1.1
  341. Use `local <pywebio.session.local>` instead.
  342. """
  343. global local
  344. import warnings
  345. warnings.warn("`pywebio.session.data()` is deprecated since v1.1 and will remove in the future version, "
  346. "use `pywebio.session.local` instead", DeprecationWarning, stacklevel=2)
  347. return local
  348. def set_env(**env_info):
  349. """configure the environment of current session.
  350. Available configuration are:
  351. * ``title`` (str): Title of current page.
  352. * ``output_animation`` (bool): Whether to enable output animation, enabled by default
  353. * ``auto_scroll_bottom`` (bool): Whether to automatically scroll the page to the bottom after output content,
  354. it is closed by default. Note that after enabled, only outputting to ROOT scope can trigger automatic scrolling.
  355. * ``http_pull_interval`` (int): The period of HTTP polling messages (in milliseconds, default 1000ms),
  356. only available in sessions based on HTTP connection.
  357. * ``input_panel_fixed`` (bool): Whether to make input panel fixed at bottom, enabled by default
  358. * ``input_panel_min_height`` (int): The minimum height of input panel (in pixel, default 300px),
  359. it should be larger than 75px. Available only when ``input_panel_fixed=True``
  360. * ``input_panel_init_height`` (int): The initial height of input panel (in pixel, default 300px),
  361. it should be larger than 175px. Available only when ``input_panel_fixed=True``
  362. * ``input_auto_focus`` (bool): Whether to focus on input automatically after showing input panel, default is ``True``
  363. * ``output_max_width`` (str): The max width of the page content area (in pixel or percentage,
  364. e.g. ``'1080px'``, ``'80%'``. Default is 880px).
  365. Example::
  366. set_env(title='Awesome PyWebIO!!', output_animation=False)
  367. .. versionchanged:: 1.4
  368. Added the ``output_max_width`` parameter
  369. """
  370. from ..io_ctrl import send_msg
  371. assert all(k in ('title', 'output_animation', 'auto_scroll_bottom', 'http_pull_interval', 'output_max_width',
  372. 'input_panel_min_height', 'input_panel_init_height', 'input_panel_fixed', 'input_auto_focus')
  373. for k in env_info.keys())
  374. send_msg('set_env', spec=env_info)
  375. def go_app(name, new_window=True):
  376. """Jump to another task of a same PyWebIO application. Only available in PyWebIO Server mode
  377. :param str name: Target PyWebIO task name.
  378. :param bool new_window: Whether to open in a new window, the default is `True`
  379. See also: :ref:`Server mode <server_and_script_mode>`
  380. """
  381. run_js('javascript:WebIO.openApp(app, new_window)', app=name, new_window=new_window)
  382. # session info data object
  383. info = ReadOnlyObjectDict(lambda: get_current_session().internal_save['info']) # type: _SessionInfoType
  384. class _SessionInfoType:
  385. user_agent = None # type: user_agents.parsers.UserAgent
  386. user_language = '' # e.g.: zh-CN
  387. server_host = '' # e.g.: localhost:8080
  388. origin = '' # e.g.: http://localhost:8080
  389. user_ip = ''
  390. backend = '' # one of ['tornado', 'flask', 'django', 'aiohttp']
  391. protocol = '' # one of ['websocket', 'http']
  392. request = None
  393. def get_info():
  394. """Get information about the current session
  395. .. deprecated:: 1.2
  396. Use `info <pywebio.session.info>` instead.
  397. """
  398. global info
  399. import warnings
  400. warnings.warn("`pywebio.session.get_info()` is deprecated since v1.2 and will remove in the future version, "
  401. "please use `pywebio.session.info` instead", DeprecationWarning, stacklevel=2)
  402. return info