output.py 73 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815
  1. r"""
  2. This module provides functions to output all kinds of content to the user's browser, and supply flexible output control.
  3. .. _output_func_list:
  4. Functions list
  5. ---------------
  6. ..
  7. Use https://www.tablesgenerator.com/text_tables to generate/update below table
  8. | The following table shows the output-related functions provided by PyWebIO.
  9. | The functions marked with ``*`` indicate that they accept ``put_xxx`` calls as arguments.
  10. | The functions marked with ``†`` indicate that they can use as context manager.
  11. +--------------------+---------------------------+------------------------------------------------------------+
  12. | | **Name** | **Description** |
  13. +--------------------+---------------------------+------------------------------------------------------------+
  14. | Output Scope | `put_scope` | Create a new scope |
  15. | +---------------------------+------------------------------------------------------------+
  16. | | `use_scope`:sup:`†` | Enter a scope |
  17. | +---------------------------+------------------------------------------------------------+
  18. | | `get_scope` | Get the current scope name in the runtime scope stack |
  19. | +---------------------------+------------------------------------------------------------+
  20. | | `clear` | Clear the content of scope |
  21. | +---------------------------+------------------------------------------------------------+
  22. | | `remove` | Remove the scope |
  23. | +---------------------------+------------------------------------------------------------+
  24. | | `scroll_to` | Scroll the page to the scope |
  25. +--------------------+---------------------------+------------------------------------------------------------+
  26. | Content Outputting | `put_text` | Output plain text |
  27. | +---------------------------+------------------------------------------------------------+
  28. | | `put_markdown` | Output Markdown |
  29. | +---------------------------+------------------------------------------------------------+
  30. | | | `put_info`:sup:`*†` | Output Messages. |
  31. | | | `put_success`:sup:`*†` | |
  32. | | | `put_warning`:sup:`*†` | |
  33. | | | `put_error`:sup:`*†` | |
  34. | +---------------------------+------------------------------------------------------------+
  35. | | `put_html` | Output html |
  36. | +---------------------------+------------------------------------------------------------+
  37. | | `put_link` | Output link |
  38. | +---------------------------+------------------------------------------------------------+
  39. | | `put_processbar` | Output a process bar |
  40. | +---------------------------+------------------------------------------------------------+
  41. | | `put_loading`:sup:`†` | Output loading prompt |
  42. | +---------------------------+------------------------------------------------------------+
  43. | | `put_code` | Output code block |
  44. | +---------------------------+------------------------------------------------------------+
  45. | | `put_table`:sup:`*` | Output table |
  46. | +---------------------------+------------------------------------------------------------+
  47. | | | `put_button` | Output button and bind click event |
  48. | | | `put_buttons` | |
  49. | +---------------------------+------------------------------------------------------------+
  50. | | `put_image` | Output image |
  51. | +---------------------------+------------------------------------------------------------+
  52. | | `put_file` | Output a link to download a file |
  53. | +---------------------------+------------------------------------------------------------+
  54. | | `put_tabs`:sup:`*` | Output tabs |
  55. | +---------------------------+------------------------------------------------------------+
  56. | | `put_collapse`:sup:`*†` | Output collapsible content |
  57. | +---------------------------+------------------------------------------------------------+
  58. | | `put_scrollable`:sup:`*†` | | Output a fixed height content area, |
  59. | | | | scroll bar is displayed when the content |
  60. | | | | exceeds the limit |
  61. | +---------------------------+------------------------------------------------------------+
  62. | | `put_widget`:sup:`*` | Output your own widget |
  63. +--------------------+---------------------------+------------------------------------------------------------+
  64. | Other Interactions | `toast` | Show a notification message |
  65. | +---------------------------+------------------------------------------------------------+
  66. | | `popup`:sup:`*†` | Show popup |
  67. | +---------------------------+------------------------------------------------------------+
  68. | | `close_popup` | Close the current popup window. |
  69. +--------------------+---------------------------+------------------------------------------------------------+
  70. | Layout and Style | `put_row`:sup:`*†` | Use row layout to output content |
  71. | +---------------------------+------------------------------------------------------------+
  72. | | `put_column`:sup:`*†` | Use column layout to output content |
  73. | +---------------------------+------------------------------------------------------------+
  74. | | `put_grid`:sup:`*` | Output content using grid layout |
  75. | +---------------------------+------------------------------------------------------------+
  76. | | `span` | Cross-cell content |
  77. | +---------------------------+------------------------------------------------------------+
  78. | | `style`:sup:`*` | Customize the css style of output content |
  79. +--------------------+---------------------------+------------------------------------------------------------+
  80. Output Scope
  81. --------------
  82. .. seealso::
  83. * :ref:`Use Guide: Output Scope <output_scope>`
  84. .. autofunction:: put_scope
  85. .. autofunction:: use_scope
  86. .. autofunction:: get_scope
  87. .. autofunction:: clear
  88. .. autofunction:: remove
  89. .. autofunction:: scroll_to
  90. Content Outputting
  91. -----------------------
  92. .. _scope_param:
  93. **Scope related parameters of output function**
  94. The output function will output the content to the "current scope" by default, and the "current scope" for the runtime
  95. context can be set by `use_scope()`.
  96. In addition, all output functions support a ``scope`` parameter to specify the destination scope to output:
  97. .. exportable-codeblock::
  98. :name: put-xxx-scope
  99. :summary: ``scope`` parameter of the output function
  100. with use_scope('scope3'):
  101. put_text('text1 in scope3') # output to current scope: scope3
  102. put_text('text in ROOT scope', scope='ROOT') # output to ROOT Scope
  103. put_text('text2 in scope3', scope='scope3') # output to scope3
  104. The results of the above code are as follows::
  105. text1 in scope3
  106. text2 in scope3
  107. text in ROOT scope
  108. A scope can contain multiple output items, the default behavior of output function is to append its content to target scope.
  109. The ``position`` parameter of output function can be used to specify the insert position in target scope.
  110. Each output item in a scope has an index, the first item's index is 0, and the next item's index is incremented by one.
  111. You can also use a negative number to index the items in the scope, -1 means the last item, -2 means the item before the last, ...
  112. The ``position`` parameter of output functions accepts an integer. When ``position>=0``, it means to insert content
  113. before the item whose index equal ``position``; when ``position<0``, it means to insert content after the item whose
  114. index equal ``position``:
  115. .. exportable-codeblock::
  116. :name: put-xxx-position
  117. :summary: `position` parameter of the output function
  118. with use_scope('scope1'):
  119. put_text('A')
  120. ## ----
  121. with use_scope('scope1'): # ..demo-only
  122. put_text('B', position=0) # insert B before A -> B A
  123. ## ----
  124. with use_scope('scope1'): # ..demo-only
  125. put_text('C', position=-2) # insert C after B -> B C A
  126. ## ----
  127. with use_scope('scope1'): # ..demo-only
  128. put_text('D', position=1) # insert D before C B -> B D C A
  129. **Output functions**
  130. .. autofunction:: put_text
  131. .. autofunction:: put_markdown
  132. .. py:function:: put_info(*contents, closable=False, scope=None, position=-1) -> Output:
  133. put_success(*contents, closable=False, scope=None, position=-1) -> Output:
  134. put_warning(*contents, closable=False, scope=None, position=-1) -> Output:
  135. put_error(*contents, closable=False, scope=None, position=-1) -> Output:
  136. Output Messages.
  137. :param contents: Message contents.
  138. The item is ``put_xxx()`` call, and any other type will be converted to ``put_text(content)``.
  139. :param bool closable: Whether to show a dismiss button on the right of the message.
  140. :param int scope, position: Those arguments have the same meaning as for `put_text()`
  141. .. versionadded:: 1.2
  142. .. autofunction:: put_html
  143. .. autofunction:: put_link
  144. .. autofunction:: put_processbar
  145. .. autofunction:: set_processbar
  146. .. autofunction:: put_loading
  147. .. autofunction:: put_code
  148. .. autofunction:: put_table
  149. .. autofunction:: span
  150. .. autofunction:: put_buttons
  151. .. autofunction:: put_button
  152. .. autofunction:: put_image
  153. .. autofunction:: put_file
  154. .. autofunction:: put_tabs
  155. .. autofunction:: put_collapse
  156. .. autofunction:: put_scrollable
  157. .. autofunction:: put_widget
  158. Other Interactions
  159. --------------------
  160. .. autofunction:: toast
  161. .. autofunction:: popup
  162. .. autofunction:: close_popup
  163. .. _style_and_layout:
  164. Layout and Style
  165. ------------------
  166. .. autofunction:: put_row
  167. .. autofunction:: put_column
  168. .. autofunction:: put_grid
  169. .. autofunction:: style
  170. """
  171. import copy
  172. import html
  173. import io
  174. import logging
  175. import string
  176. from base64 import b64encode
  177. from collections.abc import Mapping, Sequence
  178. from functools import wraps
  179. from typing import Union
  180. from .io_ctrl import output_register_callback, send_msg, Output, safely_destruct_output_when_exp, OutputList, scope2dom
  181. from .session import get_current_session, download
  182. from .utils import random_str, iscoroutinefunction, check_dom_name_value
  183. try:
  184. from PIL.Image import Image as PILImage
  185. except ImportError:
  186. PILImage = type('MockPILImage', (), dict(__init__=None))
  187. logger = logging.getLogger(__name__)
  188. __all__ = ['Position', 'remove', 'scroll_to', 'put_tabs', 'put_scope',
  189. 'put_text', 'put_html', 'put_code', 'put_markdown', 'use_scope', 'set_scope', 'clear', 'remove',
  190. 'put_table', 'put_buttons', 'put_image', 'put_file', 'PopupSize', 'popup', 'put_button',
  191. 'close_popup', 'put_widget', 'put_collapse', 'put_link', 'put_scrollable', 'style', 'put_column',
  192. 'put_row', 'put_grid', 'span', 'put_processbar', 'set_processbar', 'put_loading',
  193. 'output', 'toast', 'get_scope', 'put_info', 'put_error', 'put_warning', 'put_success']
  194. # popup size
  195. class PopupSize:
  196. LARGE = 'large'
  197. NORMAL = 'normal'
  198. SMALL = 'small'
  199. class Position:
  200. TOP = 'top'
  201. MIDDLE = 'middle'
  202. BOTTOM = 'bottom'
  203. # position value of `put_xxx()`
  204. class OutputPosition:
  205. TOP = 0
  206. BOTTOM = -1
  207. _scope_name_allowed_chars = set(string.ascii_letters + string.digits + '_-')
  208. def set_scope(name, container_scope=None, position=OutputPosition.BOTTOM, if_exist=None):
  209. """Create a new scope.
  210. :param str name: scope name
  211. :param str container_scope: Specify the parent scope of this scope.
  212. When the scope doesn't exist, no operation is performed.
  213. :param int position: The location where this scope is created in the parent scope.
  214. (see :ref:`Scope related parameters <scope_param>`)
  215. :param str if_exist: What to do when the specified scope already exists:
  216. - `None`: Do nothing
  217. - `'remove'`: Remove the old scope first and then create a new one
  218. - `'clear'`: Just clear the contents of the old scope, but don't create a new scope
  219. Default is `None`
  220. """
  221. if container_scope is None:
  222. container_scope = get_scope()
  223. check_dom_name_value(name, 'scope name')
  224. send_msg('output_ctl', dict(set_scope=scope2dom(name, no_css_selector=True),
  225. container=scope2dom(container_scope),
  226. position=position, if_exist=if_exist))
  227. def get_scope(stack_idx=-1):
  228. """Get the scope name of runtime scope stack
  229. :param int stack_idx: The index of the runtime scope stack. Default is -1.
  230. 0 means the top level scope(the ``ROOT`` Scope),
  231. -1 means the current Scope,
  232. -2 means the scope used before entering the current scope, ...
  233. :return: Returns the scope name with the index, and returns ``None`` when occurs index error
  234. """
  235. try:
  236. return get_current_session().get_scope_name(stack_idx)
  237. except IndexError:
  238. logger.exception("Scope stack index error")
  239. return None
  240. def clear(scope=None):
  241. """Clear the content of the specified scope
  242. :param str scope: Target scope name. Default is the current scope.
  243. """
  244. if scope is None:
  245. scope = get_scope()
  246. send_msg('output_ctl', dict(clear=scope2dom(scope)))
  247. def remove(scope=None):
  248. """Remove the specified scope
  249. :param str scope: Target scope name. Default is the current scope.
  250. """
  251. if scope is None:
  252. scope = get_scope()
  253. assert scope != 'ROOT', "Can not remove `ROOT` scope."
  254. send_msg('output_ctl', dict(remove=scope2dom(scope)))
  255. def scroll_to(scope=None, position=Position.TOP):
  256. """
  257. Scroll the page to the specified scope
  258. :param str scope: Target scope. Default is the current scope.
  259. :param str position: Where to place the scope in the visible area of the page. Available value:
  260. * ``'top'`` : Keep the scope at the top of the visible area of the page
  261. * ``'middle'`` : Keep the scope at the middle of the visible area of the page
  262. * ``'bottom'`` : Keep the scope at the bottom of the visible area of the page
  263. """
  264. if scope is None:
  265. scope = get_scope()
  266. send_msg('output_ctl', dict(scroll_to=scope2dom(scope), position=position))
  267. def _get_output_spec(type, scope, position, **other_spec):
  268. """
  269. get the spec dict of output functions
  270. :param str type: output type
  271. :param str scope: target scope
  272. :param int position:
  273. :param other_spec: Additional output parameters, the None value will not be included in the return value
  274. :return dict: ``spec`` field of ``output`` command
  275. """
  276. spec = dict(type=type)
  277. # add non-None arguments to spec
  278. spec.update({k: v for k, v in other_spec.items() if v is not None})
  279. if not scope:
  280. scope_name = get_scope()
  281. else:
  282. scope_name = scope
  283. spec['scope'] = scope2dom(scope_name)
  284. spec['position'] = position
  285. return spec
  286. def put_text(*texts, sep=' ', inline=False, scope=None, position=OutputPosition.BOTTOM) -> Output:
  287. """
  288. Output plain text
  289. :param texts: Texts need to output. The type can be any object, and the `str()` function will be used for non-string objects.
  290. :param str sep: The separator between the texts
  291. :param bool inline: Use text as an inline element (no line break at the end of the text). Default is ``False``
  292. :param str scope: The target scope to output. If the scope does not exist, no operation will be performed.
  293. Can specify the scope name or use a integer to index the runtime scope stack.
  294. :param int position: The position where the content is output in target scope
  295. For more information about ``scope`` and ``position`` parameter, please refer to :ref:`User Manual <scope_param>`
  296. """
  297. content = sep.join(str(i) for i in texts)
  298. spec = _get_output_spec('text', content=content, inline=inline, scope=scope, position=position)
  299. return Output(spec)
  300. def _put_message(color, contents, closable=False, scope=None, position=OutputPosition.BOTTOM) -> Output:
  301. tpl = r"""
  302. <div class="alert alert-{{color}} {{#dismissible}}alert-dismissible fade show{{/dismissible}}" role="alert">
  303. {{#contents}}
  304. {{& pywebio_output_parse}}
  305. {{/contents}}
  306. {{#dismissible}}
  307. <button type="button" class="close" data-dismiss="alert" aria-label="Close">
  308. <span aria-hidden="true">&times;</span>
  309. </button>
  310. {{/dismissible}}
  311. </div>""".strip()
  312. contents = [c if isinstance(c, Output) else put_text(c) for c in contents]
  313. return put_widget(template=tpl, data=dict(color=color, contents=contents, dismissible=closable),
  314. scope=scope, position=position).enable_context_manager()
  315. def put_info(*contents, closable=False, scope=None, position=OutputPosition.BOTTOM) -> Output:
  316. """Output information message.
  317. :param contents: Message contents.
  318. The item is ``put_xxx()`` call, and any other type will be converted to ``put_text(content)``.
  319. :param bool closable: Whether to show a dismiss button on the right of the message.
  320. :param int scope, position: Those arguments have the same meaning as for `put_text()`
  321. .. versionadded:: 1.2
  322. """
  323. return _put_message(color='info', contents=contents, closable=closable, scope=scope, position=position)
  324. def put_success(*contents, closable=False, scope=None, position=OutputPosition.BOTTOM) -> Output:
  325. """Output success message.
  326. .. seealso:: `put_info()`
  327. .. versionadded:: 1.2
  328. """
  329. return _put_message(color='success', contents=contents, closable=closable, scope=scope, position=position)
  330. def put_warning(*contents, closable=False, scope=None, position=OutputPosition.BOTTOM) -> Output:
  331. """Output warning message.
  332. .. seealso:: `put_info()`
  333. """
  334. return _put_message(color='warning', contents=contents, closable=closable, scope=scope, position=position)
  335. def put_error(*contents, closable=False, scope=None, position=OutputPosition.BOTTOM) -> Output:
  336. """Output error message.
  337. .. seealso:: `put_info()`
  338. """
  339. return _put_message(color='danger', contents=contents, closable=closable, scope=scope, position=position)
  340. def put_html(html, sanitize=False, scope=None, position=OutputPosition.BOTTOM) -> Output:
  341. """
  342. Output HTML content
  343. :param html: html string
  344. :param bool sanitize: Whether to use `DOMPurify <https://github.com/cure53/DOMPurify>`_ to filter the content to prevent XSS attacks.
  345. :param int scope, position: Those arguments have the same meaning as for `put_text()`
  346. """
  347. # Compatible with ipython rich output
  348. # See: https://ipython.readthedocs.io/en/stable/config/integrating.html?highlight=Rich%20display#rich-display
  349. if hasattr(html, '__html__'):
  350. html = html.__html__()
  351. elif hasattr(html, '_repr_html_'):
  352. html = html._repr_html_()
  353. spec = _get_output_spec('html', content=html, sanitize=sanitize, scope=scope, position=position)
  354. return Output(spec)
  355. def put_code(content, language='', rows=None, scope=None, position=OutputPosition.BOTTOM) -> Output:
  356. """
  357. Output code block
  358. :param str content: code string
  359. :param str language: language of code
  360. :param int rows: The max lines of code can be displayed, no limit by default. The scroll bar will be displayed when the content exceeds.
  361. :param int scope, position: Those arguments have the same meaning as for `put_text()`
  362. """
  363. if not isinstance(content, str):
  364. content = str(content)
  365. # For fenced code blocks, escaping the backtick need to use more backticks
  366. backticks = '```'
  367. while backticks in content:
  368. backticks += '`'
  369. code = "%s%s\n%s\n%s" % (backticks, language, content, backticks)
  370. out = put_markdown(code, scope=scope, position=position)
  371. if rows is not None:
  372. max_height = rows * 19 + 32 # 32 is the code css padding
  373. out.style("max-height: %spx" % max_height)
  374. return out
  375. def _left_strip_multiple_line_string_literal(s):
  376. """Remove the indent for code format in string literal
  377. * The first line may have no leading whitespace
  378. * There may be empty line in s (since PyCharm will remove the line trailing whitespace)
  379. """
  380. lines = s.splitlines()
  381. if len(lines) < 2:
  382. return s
  383. line = ''
  384. for line in lines[1:]:
  385. if line:
  386. break
  387. strip_cnt = 1
  388. while line[:strip_cnt] in (' ' * strip_cnt, '\t' * strip_cnt):
  389. strip_cnt += 1
  390. for line in lines[1:]:
  391. while line.strip() and line[:strip_cnt] not in (' ' * strip_cnt, '\t' * strip_cnt):
  392. strip_cnt -= 1
  393. lines_ = [i[strip_cnt:] for i in lines[1:]]
  394. return '\n'.join(lines[:1] + lines_)
  395. def put_markdown(mdcontent, lstrip=True, options=None, sanitize=True,
  396. scope=None, position=OutputPosition.BOTTOM, **kwargs) -> Output:
  397. """
  398. Output Markdown
  399. :param str mdcontent: Markdown string
  400. :param bool lstrip: Whether to remove the leading whitespace in each line of ``mdcontent``.
  401. The number of the whitespace to remove will be decided cleverly.
  402. :param dict options: Configuration when parsing Markdown.
  403. PyWebIO uses `marked <https://marked.js.org/>`_ library to parse Markdown,
  404. the parse options see: https://marked.js.org/using_advanced#options (Only supports members of string and boolean type)
  405. :param bool sanitize: Whether to use `DOMPurify <https://github.com/cure53/DOMPurify>`_ to filter the content to prevent XSS attacks.
  406. :param int scope, position: Those arguments have the same meaning as for `put_text()`
  407. When using Python triple quotes syntax to output multi-line Markdown in a function,
  408. you can indent the Markdown text to keep a good code format.
  409. PyWebIO will cleverly remove the indent for you when show the Markdown::
  410. # good code format
  411. def hello():
  412. put_markdown(r\""" # H1
  413. This is content.
  414. \""")
  415. .. versionchanged:: 1.5
  416. Enable `lstrip` by default.
  417. Deprecate `strip_indent`.
  418. """
  419. if 'strip_indent' in kwargs:
  420. import warnings
  421. # use stacklevel=2 to make the warning refer to put_markdown() call
  422. warnings.warn("`strip_indent` parameter is deprecated in `put_markdown()`", DeprecationWarning, stacklevel=2)
  423. if lstrip:
  424. mdcontent = _left_strip_multiple_line_string_literal(mdcontent)
  425. spec = _get_output_spec('markdown', content=mdcontent, options=options, sanitize=sanitize,
  426. scope=scope, position=position)
  427. return Output(spec)
  428. class span_:
  429. def __init__(self, content, row=1, col=1):
  430. self.content, self.row, self.col = content, row, col
  431. @safely_destruct_output_when_exp('content')
  432. def span(content, row=1, col=1):
  433. """Create cross-cell content in :func:`put_table()` and :func:`put_grid()`
  434. :param content: cell content. It can be a string or ``put_xxx()`` call.
  435. :param int row: Vertical span, that is, the number of spanning rows
  436. :param int col: Horizontal span, that is, the number of spanning columns
  437. :Example:
  438. .. exportable-codeblock::
  439. :name: span
  440. :summary: Create cross-cell content with `span()`
  441. put_table([
  442. ['C'],
  443. [span('E', col=2)], # 'E' across 2 columns
  444. ], header=[span('A', row=2), 'B']) # 'A' across 2 rows
  445. put_grid([
  446. [put_text('A'), put_text('B')],
  447. [span(put_text('A'), col=2)], # 'A' across 2 columns
  448. ])
  449. """
  450. return span_(content, row, col)
  451. @safely_destruct_output_when_exp('tdata')
  452. def put_table(tdata, header=None, scope=None, position=OutputPosition.BOTTOM) -> Output:
  453. """
  454. Output table
  455. :param list tdata: Table data, which can be a two-dimensional list or a list of dict.
  456. The table cell can be a string or ``put_xxx()`` call. The cell can use the :func:`span()` to set the cell span.
  457. :param list header: Table header.
  458. When the item of ``tdata`` is of type ``list``, if the ``header`` parameter is omitted,
  459. the first item of ``tdata`` will be used as the header.
  460. The header item can also use the :func:`span()` function to set the cell span.
  461. When ``tdata`` is list of dict, ``header`` is used to specify the order of table headers, which cannot be omitted.
  462. In this case, the ``header`` can be a list of dict key or a list of ``(<label>, <dict key>)``.
  463. :param int scope, position: Those arguments have the same meaning as for `put_text()`
  464. Example:
  465. .. exportable-codeblock::
  466. :name: put_table
  467. :summary: Output table with `put_table()`
  468. # 'Name' cell across 2 rows, 'Address' cell across 2 columns
  469. put_table([
  470. [span('Name',row=2), span('Address', col=2)],
  471. ['City', 'Country'],
  472. ['Wang', 'Beijing', 'China'],
  473. ['Liu', 'New York', 'America'],
  474. ])
  475. ## ----
  476. # Use `put_xxx()` in `put_table()`
  477. put_table([
  478. ['Type', 'Content'],
  479. ['html', put_html('X<sup>2</sup>')],
  480. ['text', '<hr/>'],
  481. ['buttons', put_buttons(['A', 'B'], onclick=...)], # ..doc-only
  482. ['buttons', put_buttons(['A', 'B'], onclick=put_text)], # ..demo-only
  483. ['markdown', put_markdown('`Awesome PyWebIO!`')],
  484. ['file', put_file('hello.text', b'hello world')],
  485. ['table', put_table([['A', 'B'], ['C', 'D']])]
  486. ])
  487. ## ----
  488. # Set table header
  489. put_table([
  490. ['Wang', 'M', 'China'],
  491. ['Liu', 'W', 'America'],
  492. ], header=['Name', 'Gender', 'Address'])
  493. ## ----
  494. # When ``tdata`` is list of dict
  495. put_table([
  496. {"Course":"OS", "Score": "80"},
  497. {"Course":"DB", "Score": "93"},
  498. ], header=["Course", "Score"]) # or header=[(put_markdown("*Course*"), "Course"), (put_markdown("*Score*") ,"Score")]
  499. .. versionadded:: 0.3
  500. The cell of table support ``put_xxx()`` calls.
  501. """
  502. # Change ``dict`` row table to list row table
  503. if tdata and isinstance(tdata[0], dict):
  504. if isinstance(header[0], (list, tuple)):
  505. header_ = [h[0] for h in header]
  506. order = [h[-1] for h in header]
  507. else:
  508. header_ = order = header
  509. tdata = [
  510. [row.get(k, '') for k in order]
  511. for row in tdata
  512. ]
  513. header = header_
  514. else:
  515. tdata = [list(i) for i in tdata] # copy data
  516. if header:
  517. # when tdata is empty, header will not be process
  518. # see https://github.com/pywebio/PyWebIO/issues/453
  519. if isinstance(header[0], (list, tuple)):
  520. header = [h[0] for h in header]
  521. tdata = [header, *tdata]
  522. span = {}
  523. for x in range(len(tdata)):
  524. for y in range(len(tdata[x])):
  525. cell = tdata[x][y]
  526. if isinstance(cell, span_):
  527. tdata[x][y] = cell.content
  528. span['%s,%s' % (x, y)] = dict(col=cell.col, row=cell.row)
  529. elif not isinstance(cell, Output):
  530. tdata[x][y] = str(cell)
  531. spec = _get_output_spec('table', data=tdata, span=span, scope=scope, position=position)
  532. return Output(spec)
  533. def _format_button(buttons):
  534. """
  535. Format `buttons` parameter in `put_buttons()`, replace its value with its idx
  536. :param buttons:
  537. {label:, value:, }
  538. (label, value, )
  539. single value, label=value
  540. :return: [{value:, label:, }, ...], values
  541. """
  542. btns = []
  543. values = []
  544. for idx, btn in enumerate(buttons):
  545. btn = copy.deepcopy(btn)
  546. if isinstance(btn, Mapping):
  547. assert 'value' in btn and 'label' in btn, 'actions item must have value and label key'
  548. elif isinstance(btn, (list, tuple)):
  549. assert len(btn) == 2, 'actions item format error'
  550. btn = dict(zip(('label', 'value'), btn))
  551. else:
  552. btn = dict(value=btn, label=btn)
  553. values.append(btn['value'])
  554. btn['value'] = idx
  555. btns.append(btn)
  556. return btns, values
  557. def put_buttons(buttons, onclick, small=None, link_style=False, outline=False, group=False, scope=None,
  558. position=OutputPosition.BOTTOM, **callback_options) -> Output:
  559. """
  560. Output a group of buttons and bind click event
  561. :param list buttons: Button list. The available formats of list items are:
  562. * dict::
  563. {
  564. "label":(str)button label,
  565. "value":(str)button value,
  566. "color":(str, optional)button color,
  567. "disabled":(bool, optional) whether the button is disabled
  568. }
  569. * tuple or list: ``(label, value)``
  570. * single value: label and value of option use the same value
  571. The ``value`` of button can be any type.
  572. The ``color`` of button can be one of: `primary`, `secondary`, `success`, `danger`, `warning`, `info`, `light`, `dark`.
  573. Example:
  574. .. exportable-codeblock::
  575. :name: put_buttons-btn_class
  576. :summary: `put_buttons()`
  577. put_buttons([dict(label='success', value='s', color='success')], onclick=...) # ..doc-only
  578. put_buttons([ # ..demo-only
  579. dict(label=i, value=i, color=i) # ..demo-only
  580. for i in ['primary', 'secondary', 'success', 'danger', 'warning', 'info', 'light', 'dark'] # ..demo-only
  581. ], onclick=put_text) # ..demo-only
  582. :type onclick: callable / list
  583. :param onclick: Callback which will be called when button is clicked. ``onclick`` can be a callable object or a list of it.
  584. If ``onclick`` is callable object, its signature is ``onclick(btn_value)``. ``btn_value`` is ``value`` of the button that is clicked.
  585. If ``onclick`` is a list, the item receives no parameter. In this case, each item in the list corresponds to the buttons one-to-one.
  586. Tip: You can use ``functools.partial`` to save more context information in ``onclick``.
  587. Note: When in :ref:`Coroutine-based session <coroutine_based_session>`, the callback can be a coroutine function.
  588. :param bool small: Whether to use small size button. Default is False.
  589. :param bool link_style: Whether to use link style button. Default is False
  590. :param bool outline: Whether to use outline style button. Default is False
  591. :param bool group: Whether to group the buttons together. Default is False
  592. :param int scope, position: Those arguments have the same meaning as for `put_text()`
  593. :param callback_options: Other options of the ``onclick`` callback. There are different options according to the session implementation
  594. When in Coroutine-based Session:
  595. * mutex_mode: Default is ``False``. If set to ``True``, new click event will be ignored when the current callback is running.
  596. This option is available only when ``onclick`` is a coroutine function.
  597. When in Thread-based Session:
  598. * serial_mode: Default is ``False``, and every time a callback is triggered,
  599. the callback function will be executed immediately in a new thread.
  600. If set ``serial_mode`` to ``True``
  601. After enabling serial_mode, the button's callback will be executed serially in a resident thread in the session,
  602. and all other new click event callbacks (including the ``serial_mode=False`` callback) will be queued for
  603. the current click event to complete. If the callback function runs for a short time,
  604. you can turn on ``serial_mode`` to improve performance.
  605. Example:
  606. .. exportable-codeblock::
  607. :name: put_buttons
  608. :summary: `put_buttons()` usage
  609. from functools import partial
  610. def row_action(choice, id):
  611. put_text("You click %s button with id: %s" % (choice, id))
  612. put_buttons(['edit', 'delete'], onclick=partial(row_action, id=1))
  613. ## ----
  614. def edit():
  615. put_text("You click edit button")
  616. def delete():
  617. put_text("You click delete button")
  618. put_buttons(['edit', 'delete'], onclick=[edit, delete])
  619. .. versionchanged:: 1.5
  620. Add ``disabled`` button support.
  621. The ``value`` of button can be any object.
  622. """
  623. btns, values = _format_button(buttons)
  624. if isinstance(onclick, Sequence):
  625. assert len(btns) == len(onclick), "`onclick` and `buttons` must be same length."
  626. def click_callback(btn_idx):
  627. if isinstance(onclick, Sequence):
  628. return onclick[btn_idx]()
  629. else:
  630. btn_val = values[btn_idx]
  631. if not btns[btn_idx].get('disabled'):
  632. return onclick(btn_val)
  633. callback_id = output_register_callback(click_callback, **callback_options)
  634. spec = _get_output_spec('buttons', callback_id=callback_id, buttons=btns, small=small,
  635. scope=scope, position=position, link=link_style, outline=outline, group=group)
  636. return Output(spec)
  637. def put_button(label, onclick, color=None, small=None, link_style=False, outline=False, disabled=False, scope=None,
  638. position=OutputPosition.BOTTOM) -> Output:
  639. """Output a single button and bind click event to it.
  640. :param str label: Button label
  641. :param callable onclick: Callback which will be called when button is clicked.
  642. :param str color: The color of the button,
  643. can be one of: `primary`, `secondary`, `success`, `danger`, `warning`, `info`, `light`, `dark`.
  644. :param bool disabled: Whether the button is disabled
  645. :param - small, link_style, outline, scope, position: Those arguments have the same meaning as for `put_buttons()`
  646. Example:
  647. .. exportable-codeblock::
  648. :name: put_button
  649. :summary: `put_button()` usage
  650. put_button("click me", onclick=lambda: toast("Clicked"), color='success', outline=True)
  651. .. versionadded:: 1.4
  652. .. versionchanged:: 1.5
  653. add ``disabled`` parameter
  654. """
  655. return put_buttons([{'label': label, 'value': '', 'color': color or 'primary', 'disabled': disabled}],
  656. onclick=[onclick], small=small, link_style=link_style, outline=outline, scope=scope,
  657. position=position)
  658. def put_image(src, format=None, title='', width=None, height=None,
  659. scope=None, position=OutputPosition.BOTTOM) -> Output:
  660. """Output image
  661. :param src: Source of image. It can be a string specifying image URL, a bytes-like object specifying
  662. the binary content of an image or an instance of ``PIL.Image.Image``
  663. :param str title: Image description.
  664. :param str width: The width of image. It can be CSS pixels (like `'30px'`) or percentage (like `'10%'`).
  665. :param str height: The height of image. It can be CSS pixels (like `'30px'`) or percentage (like `'10%'`).
  666. If only one value of ``width`` and ``height`` is specified, the browser will scale image according to its original size.
  667. :param str format: Image format, optinoal. e.g.: ``png``, ``jpeg``, ``gif``, etc. Only available when `src` is non-URL
  668. :param int scope, position: Those arguments have the same meaning as for `put_text()`
  669. Example:
  670. .. exportable-codeblock::
  671. :name: put_image
  672. :summary: `put_image()` usage
  673. from pywebio import STATIC_PATH # ..demo-only
  674. img = open(STATIC_PATH + '/image/favicon_open_32.png', 'rb').read() # ..demo-only
  675. img = open('/path/to/some/image.png', 'rb').read() # ..doc-only
  676. put_image(img, width='50px')
  677. ## ----
  678. put_image('https://www.python.org/static/img/python-logo.png')
  679. """
  680. if isinstance(src, PILImage):
  681. format = format or src.format or 'JPEG'
  682. imgByteArr = io.BytesIO()
  683. src.save(imgByteArr, format=format)
  684. src = imgByteArr.getvalue()
  685. if isinstance(src, (bytes, bytearray)):
  686. b64content = b64encode(src).decode('ascii')
  687. format = '' if format is None else ('image/%s' % format)
  688. format = html.escape(format, quote=True)
  689. src = "data:{format};base64, {b64content}".format(format=format, b64content=b64content)
  690. width = 'width="%s"' % html.escape(width, quote=True) if width is not None else ''
  691. height = 'height="%s"' % html.escape(height, quote=True) if height is not None else ''
  692. tag = r'<img src="{src}" alt="{title}" {width} {height}/>'.format(src=src, title=html.escape(title, quote=True),
  693. height=height, width=width)
  694. return put_html(tag, scope=scope, position=position)
  695. def put_file(name, content, label=None, scope=None, position=OutputPosition.BOTTOM) -> Output:
  696. """Output a link to download a file
  697. To show a link with the file name on the browser. When click the link, the browser automatically downloads the file.
  698. :param str name: File name downloaded as
  699. :param content: File content. It is a bytes-like object
  700. :param str label: The label of the download link, which is the same as the file name by default.
  701. :param int scope, position: Those arguments have the same meaning as for `put_text()`
  702. Example:
  703. .. exportable-codeblock::
  704. :name: put_file
  705. :summary: `put_file()` usage
  706. content = open('./some-file', 'rb').read() # ..doc-only
  707. content = open('README.md', 'rb').read() # ..demo-only
  708. put_file('hello-world.txt', content, 'download me')
  709. """
  710. if label is None:
  711. label = name
  712. output = put_buttons(buttons=[label], link_style=True,
  713. onclick=[lambda: download(name, content)],
  714. scope=scope, position=position)
  715. return output
  716. def put_link(name, url=None, app=None, new_window=False, scope=None,
  717. position=OutputPosition.BOTTOM) -> Output:
  718. """Output hyperlinks to other web page or PyWebIO Application page.
  719. :param str name: The label of the link
  720. :param str url: Target url
  721. :param str app: Target PyWebIO Application name. See also: :ref:`Server mode <server_and_script_mode>`
  722. :param bool new_window: Whether to open the link in a new window
  723. :param int scope, position: Those arguments have the same meaning as for `put_text()`
  724. The ``url`` and ``app`` parameters must specify one but not both
  725. """
  726. assert bool(url is None) != bool(app is None), "Must set `url` or `app` parameter but not both"
  727. href = 'javascript:WebIO.openApp(%r, %d)' % (app, new_window) if app is not None else url
  728. target = '_blank' if (new_window and url) else '_self'
  729. tag = '<a href="{href}" target="{target}">{name}</a>'.format(
  730. href=html.escape(href, quote=True), target=target, name=html.escape(name))
  731. return put_html(tag, scope=scope, position=position)
  732. def put_processbar(name, init=0, label=None, auto_close=False, scope=None,
  733. position=OutputPosition.BOTTOM) -> Output:
  734. """Output a process bar
  735. :param str name: The name of the progress bar, which is the unique identifier of the progress bar
  736. :param float init: The initial progress value of the progress bar. The value is between 0 and 1
  737. :param str label: The label of process bar. The default is the percentage value of the current progress.
  738. :param bool auto_close: Whether to remove the progress bar after the progress is completed
  739. :param int scope, position: Those arguments have the same meaning as for `put_text()`
  740. Example:
  741. .. exportable-codeblock::
  742. :name: put_processbar
  743. :summary: `put_processbar()` usage
  744. import time
  745. put_processbar('bar');
  746. for i in range(1, 11):
  747. set_processbar('bar', i / 10)
  748. time.sleep(0.1)
  749. .. seealso:: use `set_processbar()` to set the progress of progress bar
  750. """
  751. check_dom_name_value(name)
  752. processbar_id = 'webio-processbar-%s' % name
  753. percentage = init * 100
  754. label = '%.1f%%' % percentage if label is None else label
  755. tpl = """<div class="progress" style="margin-top: 4px;">
  756. <div id="{{elem_id}}" class="progress-bar bg-info progress-bar-striped progress-bar-animated" role="progressbar"
  757. style="width: {{percentage}}%;" aria-valuenow="{{init}}" aria-valuemin="0" aria-valuemax="1" data-auto-close="{{auto_close}}">{{label}}
  758. </div>
  759. </div>"""
  760. return put_widget(tpl, data=dict(elem_id=processbar_id, init=init, label=label,
  761. percentage=percentage, auto_close=int(bool(auto_close))), scope=scope,
  762. position=position)
  763. def set_processbar(name, value, label=None):
  764. """Set the progress of progress bar
  765. :param str name: The name of the progress bar
  766. :param float value: The progress value of the progress bar. The value is between 0 and 1
  767. :param str label: The label of process bar. The default is the percentage value of the current progress.
  768. See also: `put_processbar()`
  769. """
  770. from pywebio.session import run_js
  771. check_dom_name_value(name)
  772. processbar_id = 'webio-processbar-%s' % name
  773. percentage = value * 100
  774. label = '%.1f%%' % percentage if label is None else label
  775. js_code = """
  776. let bar = $("#{processbar_id}");
  777. bar[0].style.width = "{percentage}%";
  778. bar.attr("aria-valuenow", "{value}");
  779. bar.text({label!r});
  780. """.format(processbar_id=processbar_id, percentage=percentage, value=value, label=label)
  781. if value == 1:
  782. js_code += "if(bar.data('autoClose')=='1')bar.parent().remove();"
  783. run_js(js_code)
  784. def put_loading(shape='border', color='dark', scope=None, position=OutputPosition.BOTTOM) -> Output:
  785. """Output loading prompt
  786. :param str shape: The shape of loading prompt. The available values are: `'border'` (default)、 `'grow'`
  787. :param str color: The color of loading prompt. The available values are: `'primary'` 、 `'secondary'` 、
  788. `'success'` 、 `'danger'` 、 `'warning'` 、`'info'` 、`'light'` 、 `'dark'` (default)
  789. :param int scope, position: Those arguments have the same meaning as for `put_text()`
  790. `put_loading()` can be used in 2 ways: direct call and context manager:
  791. .. exportable-codeblock::
  792. :name: put_loading
  793. :summary: `put_loading()` usage
  794. for shape in ('border', 'grow'):
  795. for color in ('primary', 'secondary', 'success', 'danger', 'warning', 'info', 'light', 'dark'):
  796. put_text(shape, color)
  797. put_loading(shape=shape, color=color)
  798. ## ----
  799. import time # ..demo-only
  800. # Use as context manager, the loading prompt will disappear automatically when the context block exits.
  801. with put_loading():
  802. time.sleep(3) # Some time-consuming operations
  803. put_text("The answer of the universe is 42")
  804. ## ----
  805. # using style() to set the size of the loading prompt
  806. put_loading().style('width:4rem; height:4rem')
  807. """
  808. assert shape in ('border', 'grow'), "shape must in ('border', 'grow')"
  809. assert color in {'primary', 'secondary', 'success', 'danger', 'warning', 'info', 'light', 'dark'}
  810. html = """<div class="spinner-{shape} text-{color}" role="status">
  811. <span class="sr-only">Loading...</span>
  812. </div>""".format(shape=shape, color=color)
  813. scope_name = random_str(10)
  814. def enter(self):
  815. self.spec['container_dom_id'] = scope2dom(scope_name, no_css_selector=True)
  816. self.send()
  817. return scope_name
  818. def exit_(self, exc_type, exc_val, exc_tb):
  819. remove(scope_name)
  820. return False # Propagate Exception
  821. return put_html(html, sanitize=False, scope=scope, position=position). \
  822. enable_context_manager(custom_enter=enter, custom_exit=exit_)
  823. @safely_destruct_output_when_exp('content')
  824. def put_collapse(title, content=[], open=False, scope=None, position=OutputPosition.BOTTOM) -> Output:
  825. """Output collapsible content
  826. :param str title: Title of content
  827. :type content: list/str/put_xxx()
  828. :param content: The content can be a string, the ``put_xxx()`` calls , or a list of them.
  829. :param bool open: Whether to expand the content. Default is ``False``.
  830. :param int scope, position: Those arguments have the same meaning as for `put_text()`
  831. Example:
  832. .. exportable-codeblock::
  833. :name: put_collapse
  834. :summary: `put_collapse()` usage
  835. put_collapse('Collapse title', [
  836. 'text',
  837. put_markdown('~~Strikethrough~~'),
  838. put_table([
  839. ['Commodity', 'Price'],
  840. ['Apple', '5.5'],
  841. ])
  842. ], open=True)
  843. ## ----
  844. put_collapse('Large text', 'Awesome PyWebIO! '*30)
  845. """
  846. if not isinstance(content, (list, tuple, OutputList)):
  847. content = [content]
  848. for item in content:
  849. assert isinstance(item, (str, Output)), "put_collapse() content must be list of str/put_xxx()"
  850. tpl = """<details {{#open}}open{{/open}}>
  851. <summary>{{title}}</summary>
  852. {{#contents}}
  853. {{& pywebio_output_parse}}
  854. {{/contents}}
  855. </details>"""
  856. return put_widget(tpl, dict(title=title, contents=content, open=open), scope=scope,
  857. position=position).enable_context_manager()
  858. @safely_destruct_output_when_exp('content')
  859. def put_scrollable(content=[], height=400, keep_bottom=False, border=True,
  860. scope=None, position=OutputPosition.BOTTOM, **kwargs) -> Output:
  861. """Output a fixed height content area. scroll bar is displayed when the content exceeds the limit
  862. :type content: list/str/put_xxx()
  863. :param content: The content can be a string, the ``put_xxx()`` calls , or a list of them.
  864. :param int/tuple height: The height of the area (in pixels).
  865. ``height`` parameter also accepts ``(min_height, max_height)`` to indicate the range of height, for example,
  866. ``(100, 200)`` means that the area has a minimum height of 100 pixels and a maximum of 200 pixels.
  867. Set ``None`` if you don't want to limit the height
  868. :param bool keep_bottom: Whether to keep the content area scrolled to the bottom when updated.
  869. :param bool border: Whether to show border
  870. :param int scope, position: Those arguments have the same meaning as for `put_text()`
  871. Example:
  872. .. exportable-codeblock::
  873. :name: put_scrollable
  874. :summary: `put_scrollable()` usage
  875. import time
  876. put_scrollable(put_scope('scrollable'), height=200, keep_bottom=True)
  877. put_text("You can click the area to prevent auto scroll.", scope='scrollable')
  878. while 1:
  879. put_text(time.time(), scope='scrollable')
  880. time.sleep(0.5)
  881. .. versionchanged:: 1.1
  882. add ``height`` parameter,remove ``max_height`` parameter;
  883. add ``keep_bottom`` parameter
  884. .. versionchanged:: 1.5
  885. remove ``horizon_scroll`` parameter
  886. """
  887. if not isinstance(content, (list, tuple, OutputList)):
  888. content = [content]
  889. content = [i if isinstance(i, Output) else put_text(i) for i in content]
  890. if 'max_height' in kwargs:
  891. import warnings
  892. # use stacklevel=2 to make the warning refer to the put_scrollable() call
  893. warnings.warn("`max_height` parameter is deprecated in `put_scrollable()`, use `height` instead.",
  894. DeprecationWarning, stacklevel=2)
  895. height = kwargs['max_height'] # Backward compatible
  896. try:
  897. min_height, max_height = height
  898. except Exception:
  899. min_height, max_height = height, height
  900. spec = _get_output_spec('scrollable', contents=content, min_height=min_height, max_height=max_height,
  901. keep_bottom=keep_bottom, border=border, scope=scope, position=position)
  902. return Output(spec).enable_context_manager(container_selector='> div')
  903. @safely_destruct_output_when_exp('tabs')
  904. def put_tabs(tabs, scope=None, position=OutputPosition.BOTTOM) -> Output:
  905. """Output tabs.
  906. :param list tabs: Tab list, each item is a dict: ``{"title": "Title", "content": ...}`` .
  907. The ``content`` can be a string, the ``put_xxx()`` calls , or a list of them.
  908. :param int scope, position: Those arguments have the same meaning as for `put_text()`
  909. .. exportable-codeblock::
  910. :name: put_tabs
  911. :summary: `put_tabs()` usage
  912. put_tabs([
  913. {'title': 'Text', 'content': 'Hello world'},
  914. {'title': 'Markdown', 'content': put_markdown('~~Strikethrough~~')},
  915. {'title': 'More content', 'content': [
  916. put_table([
  917. ['Commodity', 'Price'],
  918. ['Apple', '5.5'],
  919. ['Banana', '7'],
  920. ]),
  921. put_link('pywebio', 'https://github.com/wang0618/PyWebIO')
  922. ]},
  923. ])
  924. .. versionadded:: 1.3
  925. """
  926. for tab in tabs:
  927. assert 'title' in tab and 'content' in tab
  928. spec = _get_output_spec('tabs', tabs=tabs, scope=scope, position=position)
  929. return Output(spec)
  930. @safely_destruct_output_when_exp('data')
  931. def put_widget(template, data, scope=None, position=OutputPosition.BOTTOM) -> Output:
  932. """Output your own widget
  933. :param template: html template, using `mustache.js <https://github.com/janl/mustache.js>`_ syntax
  934. :param dict data: Data used to render the template.
  935. The data can include the ``put_xxx()`` calls, and the JS function ``pywebio_output_parse`` can be used to
  936. parse the content of ``put_xxx()``. For string input, ``pywebio_output_parse`` will parse into text.
  937. ⚠️:When using the ``pywebio_output_parse`` function, you need to turn off the html escaping of mustache:
  938. ``{{& pywebio_output_parse}}``, see the example below.
  939. :param int scope, position: Those arguments have the same meaning as for `put_text()`
  940. :Example:
  941. .. exportable-codeblock::
  942. :name: put_widget
  943. :summary: Use `put_widget()` to output your own widget
  944. tpl = '''
  945. <details {{#open}}open{{/open}}>
  946. <summary>{{title}}</summary>
  947. {{#contents}}
  948. {{& pywebio_output_parse}}
  949. {{/contents}}
  950. </details>
  951. '''
  952. put_widget(tpl, {
  953. "open": True,
  954. "title": 'More content',
  955. "contents": [
  956. 'text',
  957. put_markdown('~~Strikethrough~~'),
  958. put_table([
  959. ['Commodity', 'Price'],
  960. ['Apple', '5.5'],
  961. ['Banana', '7'],
  962. ])
  963. ]
  964. })
  965. """
  966. spec = _get_output_spec('custom_widget', template=template, data=data, scope=scope, position=position)
  967. return Output(spec)
  968. @safely_destruct_output_when_exp('content')
  969. def put_row(content=[], size=None, scope=None, position=OutputPosition.BOTTOM) -> Output:
  970. """Use row layout to output content. The content is arranged horizontally
  971. :param list content: Content list, the item is ``put_xxx()`` call or ``None``. ``None`` represents the space between the output
  972. :param str size:
  973. | Used to indicate the width of the items, is a list of width values separated by space.
  974. | Each width value corresponds to the items one-to-one. (``None`` item should also correspond to a width value).
  975. | By default, ``size`` assigns a width of 10 pixels to the ``None`` item, and distributes the width equally to the remaining items.
  976. Available format of width value are:
  977. - pixels: like ``100px``
  978. - percentage: Indicates the percentage of available width. like ``33.33%``
  979. - ``fr`` keyword: Represents a scale relationship, 2fr represents twice the width of 1fr
  980. - ``auto`` keyword: Indicates that the length is determined by the browser
  981. - ``minmax(min, max)`` : Generate a length range, indicating that the length is within this range.
  982. It accepts two parameters, minimum and maximum.
  983. For example: ``minmax(100px, 1fr)`` means the length is not less than 100px and not more than 1fr
  984. :param int scope, position: Those arguments have the same meaning as for `put_text()`
  985. :Example:
  986. .. exportable-codeblock::
  987. :name: put_row
  988. :summary: `put_row()` usage
  989. # Two code blocks of equal width, separated by 10 pixels
  990. put_row([put_code('A'), None, put_code('B')])
  991. ## ----
  992. # The width ratio of the left and right code blocks is 2:3, which is equivalent to size='2fr 10px 3fr'
  993. put_row([put_code('A'), None, put_code('B')], size='40% 10px 60%')
  994. """
  995. return _row_column_layout(content, flow='column', size=size, scope=scope,
  996. position=position).enable_context_manager()
  997. @safely_destruct_output_when_exp('content')
  998. def put_column(content=[], size=None, scope=None, position=OutputPosition.BOTTOM) -> Output:
  999. """Use column layout to output content. The content is arranged vertically
  1000. :param list content: Content list, the item is ``put_xxx()`` call or ``None``. ``None`` represents the space between the output
  1001. :param str size: Used to indicate the width of the items, is a list of width values separated by space.
  1002. The format is the same as the ``size`` parameter of the `put_row()` function.
  1003. :param int scope, position: Those arguments have the same meaning as for `put_text()`
  1004. """
  1005. return _row_column_layout(content, flow='row', size=size, scope=scope, position=position).enable_context_manager()
  1006. def _row_column_layout(content, flow, size, scope=None, position=OutputPosition.BOTTOM) -> Output:
  1007. if not isinstance(content, (list, tuple, OutputList)):
  1008. content = [content]
  1009. if not size:
  1010. size = ' '.join('1fr' if c is not None else '10px' for c in content)
  1011. content = [c if c is not None else put_html('<div></div>') for c in content]
  1012. for item in content:
  1013. assert isinstance(item, Output), "put_row()/put_column()'s content must be list of put_xxx()"
  1014. style = 'grid-auto-flow: {flow}; grid-template-{flow}s: {size};'.format(flow=flow, size=size)
  1015. tpl = """
  1016. <div style="display: grid; %s">
  1017. {{#contents}}
  1018. {{& pywebio_output_parse}}
  1019. {{/contents}}
  1020. </div>""".strip() % html.escape(style, quote=True)
  1021. return put_widget(template=tpl, data=dict(contents=content), scope=scope,
  1022. position=position)
  1023. @safely_destruct_output_when_exp('content')
  1024. def put_grid(content, cell_width='auto', cell_height='auto', cell_widths=None, cell_heights=None, direction='row',
  1025. scope=None, position=OutputPosition.BOTTOM) -> Output:
  1026. """Output content using grid layout
  1027. :param content: Content of grid, which is a two-dimensional list. The item of list is ``put_xxx()`` call or ``None``.
  1028. ``None`` represents the space between the output. The item can use the `span()` to set the cell span.
  1029. :param str cell_width: The width of grid cell.
  1030. :param str cell_height: The height of grid cell.
  1031. :param str cell_widths: The width of each column of the grid. The width values are separated by a space.
  1032. Can not use ``cell_widths`` and ``cell_width`` at the same time
  1033. :param str cell_heights: The height of each row of the grid. The height values are separated by a space.
  1034. Can not use ``cell_heights`` and ``cell_height`` at the same time
  1035. :param str direction: Controls how auto-placed items get inserted in the grid.
  1036. Can be ``'row'``(default) or ``'column'`` .
  1037. | ``'row'`` : Places items by filling each row
  1038. | ``'column'`` : Places items by filling each column
  1039. :param int scope, position: Those arguments have the same meaning as for `put_text()`
  1040. The format of width/height value in ``cell_width``,``cell_height``,``cell_widths``,``cell_heights``
  1041. can refer to the ``size`` parameter of the `put_row()` function.
  1042. :Example:
  1043. .. exportable-codeblock::
  1044. :name: put_grid
  1045. :summary: `put_grid()` usage
  1046. put_grid([
  1047. [put_text('A'), put_text('B'), put_text('C')],
  1048. [None, span(put_text('D'), col=2, row=1)],
  1049. [put_text('E'), put_text('F'), put_text('G')],
  1050. ], cell_width='100px', cell_height='100px')
  1051. """
  1052. assert direction in ('row', 'column'), '"direction" parameter must be "row" or "column"'
  1053. lens = [0] * len(content)
  1054. for x in range(len(content)):
  1055. for y in range(len(content[x])):
  1056. cell = content[x][y]
  1057. if isinstance(cell, span_):
  1058. for i in range(cell.row):
  1059. lens[x + i] += cell.col
  1060. css = 'grid-row-start: span {row}; grid-column-start: span {col};'.format(row=cell.row, col=cell.col)
  1061. elem = put_html('<div></div>') if cell.content is None else cell.content
  1062. content[x][y] = elem.style(css)
  1063. else:
  1064. lens[x] += 1
  1065. if content[x][y] is None:
  1066. content[x][y] = put_html('<div></div>')
  1067. # 为长度不足的行添加空元素
  1068. # Add empty elements for rows with insufficient length
  1069. m = max(lens)
  1070. for idx, i in enumerate(content):
  1071. i.extend(put_html('<div></div>') for _ in range(m - lens[idx]))
  1072. row_cnt, col_cnt = len(content), m
  1073. if direction == 'column':
  1074. row_cnt, col_cnt = m, len(content)
  1075. if not cell_widths:
  1076. cell_widths = 'repeat({col_cnt},{cell_width})'.format(col_cnt=col_cnt, cell_width=cell_width)
  1077. if not cell_heights:
  1078. cell_heights = 'repeat({row_cnt},{cell_height})'.format(row_cnt=row_cnt, cell_height=cell_height)
  1079. css = ('grid-auto-flow: {flow};'
  1080. 'grid-template-columns: {cell_widths};'
  1081. 'grid-template-rows: {cell_heights};'
  1082. ).format(flow=direction, cell_heights=cell_heights, cell_widths=cell_widths)
  1083. tpl = """
  1084. <div style="display: grid; %s">
  1085. {{#contents}}
  1086. {{#.}}
  1087. {{& pywebio_output_parse}}
  1088. {{/.}}
  1089. {{/contents}}
  1090. </div>""".strip() % html.escape(css, quote=True)
  1091. return put_widget(template=tpl, data=dict(contents=content), scope=scope, position=position)
  1092. @safely_destruct_output_when_exp('content')
  1093. def put_scope(name, content=[], scope=None, position=OutputPosition.BOTTOM) -> Output:
  1094. """Output a scope
  1095. :param str name:
  1096. :param list/put_xxx() content: The initial content of the scope, can be ``put_xxx()`` or a list of it.
  1097. :param int scope, position: Those arguments have the same meaning as for `put_text()`
  1098. """
  1099. if not isinstance(content, list):
  1100. content = [content]
  1101. check_dom_name_value(name, 'scope name')
  1102. dom_id = scope2dom(name, no_css_selector=True)
  1103. spec = _get_output_spec('scope', dom_id=dom_id, contents=content, scope=scope, position=position)
  1104. return Output(spec)
  1105. @safely_destruct_output_when_exp('contents')
  1106. def output(*contents):
  1107. """Placeholder of output
  1108. .. deprecated:: 1.5
  1109. See :ref:`User Guide <put_scope>` for new way to set css style for output.
  1110. ``output()`` can be passed in anywhere that ``put_xxx()`` can passed in. A handler it returned by ``output()``,
  1111. and after being output, the content can also be modified by the handler (See code example below).
  1112. :param contents: The initial contents to be output.
  1113. The item is ``put_xxx()`` call, and any other type will be converted to ``put_text(content)``.
  1114. :return: An OutputHandler instance, the methods of the instance are as follows:
  1115. * ``reset(*contents)`` : Reset original contents to ``contents``
  1116. * ``append(*contents)`` : Append ``contents`` to original contents
  1117. * ``insert(idx, *contents)`` : insert ``contents`` into original contents.
  1118. | when idx>=0, the output content is inserted before the element of the ``idx`` index.
  1119. | when idx<0, the output content is inserted after the element of the ``idx`` index.
  1120. Among them, the parameter ``contents`` is the same as ``output()``.
  1121. :Example:
  1122. .. exportable-codeblock::
  1123. :name: output
  1124. :summary: `output()` usage
  1125. hobby = output(put_text('Coding')) # equal to output('Coding')
  1126. put_table([
  1127. ['Name', 'Hobbies'],
  1128. ['Wang', hobby] # hobby is initialized to Coding
  1129. ])
  1130. ## ----
  1131. hobby.reset('Movie') # hobby is reset to Movie
  1132. ## ----
  1133. hobby.append('Music', put_text('Drama')) # append Music, Drama to hobby
  1134. ## ----
  1135. hobby.insert(0, put_markdown('**Coding**')) # insert the Coding into the top of the hobby
  1136. """
  1137. import warnings
  1138. # use stacklevel=2 to make the warning refer to the output() call
  1139. warnings.warn("`pywebio.output.output()` is deprecated since v1.5 and will remove in the future version, "
  1140. "use `pywebio.output.put_scope()` instead", DeprecationWarning, stacklevel=2)
  1141. class OutputHandler(Output):
  1142. """
  1143. 与 `Output` 的不同在于, 不会在销毁时(__del__)自动输出
  1144. The difference with `Output` is that `OutputHandler` will not automatically output when destroyed (__del__)
  1145. """
  1146. def __del__(self):
  1147. pass
  1148. def __init__(self, spec, scope):
  1149. super().__init__(spec)
  1150. self.scope = scope
  1151. @safely_destruct_output_when_exp('outputs')
  1152. def reset(self, *outputs):
  1153. clear_scope(scope=self.scope)
  1154. self.append(*outputs)
  1155. @safely_destruct_output_when_exp('outputs')
  1156. def append(self, *outputs):
  1157. for o in outputs:
  1158. if not isinstance(o, Output):
  1159. o = put_text(o)
  1160. o.spec['scope'] = scope2dom(self.scope)
  1161. o.spec['position'] = OutputPosition.BOTTOM
  1162. o.send()
  1163. @safely_destruct_output_when_exp('outputs')
  1164. def insert(self, idx, *outputs):
  1165. """
  1166. idx可为负
  1167. idx can be negative
  1168. """
  1169. direction = 1 if idx >= 0 else -1
  1170. for acc, o in enumerate(outputs):
  1171. if not isinstance(o, Output):
  1172. o = put_text(o)
  1173. o.spec['scope'] = scope2dom(self.scope)
  1174. o.spec['position'] = idx + direction * acc
  1175. o.send()
  1176. contents = [c if isinstance(c, Output) else put_text(c) for c in contents]
  1177. dom_name = random_str(10)
  1178. tpl = """<div class="{{dom_class_name}}">
  1179. {{#contents}}
  1180. {{#.}}
  1181. {{& pywebio_output_parse}}
  1182. {{/.}}
  1183. {{/contents}}
  1184. </div>"""
  1185. out_spec = put_widget(template=tpl,
  1186. data=dict(contents=contents, dom_class_name=scope2dom(dom_name, no_css_selector=True)))
  1187. return OutputHandler(Output.dump_dict(out_spec), ('.', dom_name))
  1188. @safely_destruct_output_when_exp('outputs')
  1189. def style(outputs, css_style) -> Union[Output, OutputList]:
  1190. """Customize the css style of output content
  1191. .. deprecated:: 1.3
  1192. See :ref:`User Guide <style>` for new way to set css style for output.
  1193. :param outputs: The output content can be a ``put_xxx()`` call or a list of it.
  1194. :type outputs: list/put_xxx()
  1195. :param str css_style: css style string
  1196. :return: The output contents with css style added:
  1197. Note: If ``outputs`` is a list of ``put_xxx()`` calls, the style will be set for each item of the list.
  1198. And the return value can be used in anywhere accept a list of ``put_xxx()`` calls.
  1199. :Example:
  1200. .. exportable-codeblock::
  1201. :name: style-deprecated
  1202. :summary: `style()` usage
  1203. style(put_text('Red'), 'color:red')
  1204. ## ----
  1205. style([
  1206. put_text('Red'),
  1207. put_markdown('~~del~~')
  1208. ], 'color:red')
  1209. ## ----
  1210. put_table([
  1211. ['A', 'B'],
  1212. ['C', style(put_text('Red'), 'color:red')],
  1213. ])
  1214. ## ----
  1215. put_collapse('title', style([
  1216. put_text('text'),
  1217. put_markdown('~~del~~'),
  1218. ], 'margin-left:20px'))
  1219. """
  1220. import warnings
  1221. warnings.warn("`pywebio.output.style()` is deprecated since v1.3 and will remove in the future version, "
  1222. "use `put_xxx(...).style(...)` instead", DeprecationWarning, stacklevel=2)
  1223. if not isinstance(outputs, (list, tuple, OutputList)):
  1224. ol = [outputs]
  1225. else:
  1226. ol = outputs
  1227. outputs = OutputList(outputs)
  1228. for o in ol:
  1229. assert isinstance(o, Output), 'style() only accept put_xxx() input'
  1230. o.spec.setdefault('style', '')
  1231. o.spec['style'] += ';%s' % css_style
  1232. return outputs
  1233. @safely_destruct_output_when_exp('content')
  1234. def popup(title, content=None, size=PopupSize.NORMAL, implicit_close=True, closable=True):
  1235. """
  1236. Show a popup.
  1237. ⚠️: In PyWebIO, you can't show multiple popup windows at the same time. Before displaying a new pop-up window,
  1238. the existing popup on the page will be automatically closed. You can use `close_popup()` to close the popup manually.
  1239. :param str title: The title of the popup.
  1240. :type content: list/str/put_xxx()
  1241. :param content: The content of the popup can be a string, the put_xxx() calls , or a list of them.
  1242. :param str size: The size of popup window. Available values are: ``'large'``, ``'normal'`` and ``'small'``.
  1243. :param bool implicit_close: If enabled, the popup can be closed implicitly by clicking the content outside
  1244. the popup window or pressing the ``Esc`` key. Default is ``False``.
  1245. :param bool closable: Whether the user can close the popup window. By default, the user can close the popup
  1246. by clicking the close button in the upper right of the popup window.
  1247. When set to ``False``, the popup window can only be closed by :func:`popup_close()`,
  1248. at this time the ``implicit_close`` parameter will be ignored.
  1249. ``popup()`` can be used in 3 ways: direct call, context manager, and decorator.
  1250. * direct call:
  1251. .. exportable-codeblock::
  1252. :name: popup
  1253. :summary: `popup()` usage
  1254. popup('popup title', 'popup text content', size=PopupSize.SMALL)
  1255. ## ----
  1256. popup('Popup title', [
  1257. put_html('<h3>Popup Content</h3>'),
  1258. 'html: <br/>',
  1259. put_table([['A', 'B'], ['C', 'D']]),
  1260. put_buttons(['close_popup()'], onclick=lambda _: close_popup())
  1261. ])
  1262. * context manager:
  1263. .. exportable-codeblock::
  1264. :name: popup-context
  1265. :summary: `popup()` as context manager
  1266. with popup('Popup title') as s:
  1267. put_html('<h3>Popup Content</h3>')
  1268. put_text('html: <br/>')
  1269. put_buttons([('clear()', s)], onclick=clear)
  1270. put_text('Also work!', scope=s)
  1271. The context manager will open a new output scope and return the scope name.
  1272. The output in the context manager will be displayed on the popup window by default.
  1273. After the context manager exits, the popup window will not be closed.
  1274. You can still use the ``scope`` parameter of the output function to output to the popup.
  1275. * decorator:
  1276. .. exportable-codeblock::
  1277. :name: popup-decorator
  1278. :summary: `popup()` as decorator
  1279. @popup('Popup title')
  1280. def show_popup():
  1281. put_html('<h3>Popup Content</h3>')
  1282. put_text("I'm in a popup!")
  1283. ...
  1284. show_popup()
  1285. """
  1286. if content is None:
  1287. content = []
  1288. if not isinstance(content, (list, tuple, OutputList)):
  1289. content = [content]
  1290. for item in content:
  1291. assert isinstance(item, (str, Output)), "popup() content must be list of str/put_xxx()"
  1292. dom_id = random_str(10)
  1293. send_msg(cmd='popup', spec=dict(content=Output.dump_dict(content), title=title, size=size,
  1294. implicit_close=implicit_close, closable=closable,
  1295. dom_id=scope2dom(dom_id, no_css_selector=True)))
  1296. return use_scope_(dom_id)
  1297. def close_popup():
  1298. """Close the current popup window.
  1299. See also: `popup()`
  1300. """
  1301. send_msg(cmd='close_popup')
  1302. def toast(content, duration=2, position='center', color='info', onclick=None):
  1303. """Show a notification message.
  1304. :param str content: Notification content.
  1305. :param float duration: The duration of the notification display, in seconds. `0` means not to close automatically
  1306. (at this time, a close button will be displayed next to the message, and the user can close the message manually)
  1307. :param str position: Where to display the notification message. Available values are `'left'`, `'center'` and `'right'`.
  1308. :param str color: Background color of the notification.
  1309. Available values are `'info'`, `'error'`, `'warn'`, `'success'` or hexadecimal color value starting with `'#'`
  1310. :param callable onclick: The callback function when the notification message is clicked.
  1311. The callback function receives no parameters.
  1312. Note: When in :ref:`Coroutine-based session <coroutine_based_session>`, the callback can be a coroutine function.
  1313. Example:
  1314. .. exportable-codeblock::
  1315. :name: toast
  1316. :summary: `toast()` usage
  1317. def show_msg():
  1318. put_text("You clicked the notification.")
  1319. toast('New messages', position='right', color='#2188ff', duration=0, onclick=show_msg)
  1320. """
  1321. colors = {
  1322. 'info': '#1565c0',
  1323. 'error': '#e53935',
  1324. 'warn': '#ef6c00',
  1325. 'success': '#2e7d32'
  1326. }
  1327. color = colors.get(color, color)
  1328. callback_id = output_register_callback(lambda _: onclick()) if onclick is not None else None
  1329. send_msg(cmd='toast', spec=dict(content=content, duration=int(duration * 1000), position=position,
  1330. color=color, callback_id=callback_id))
  1331. clear_scope = clear
  1332. def use_scope(name=None, clear=False, **kwargs):
  1333. """use_scope(name=None, clear=False)
  1334. Open or enter a scope. Can be used as context manager and decorator.
  1335. See :ref:`User manual - use_scope() <use_scope>`
  1336. :param str name: Scope name. If it is None, a globally unique scope name is generated.
  1337. (When used as context manager, the context manager will return the scope name)
  1338. :param bool clear: Whether to clear the contents of the scope before entering the scope.
  1339. :Usage:
  1340. ::
  1341. with use_scope(...) as scope_name:
  1342. put_xxx()
  1343. @use_scope(...)
  1344. def app():
  1345. put_xxx()
  1346. """
  1347. # For backward compatible
  1348. # :param bool create_scope: Whether to create scope when scope does not exist.
  1349. # :param scope_params: Extra parameters passed to `set_scope()` when need to create scope.
  1350. # Only available when ``create_scope=True``.
  1351. create_scope = kwargs.pop('create_scope', True)
  1352. scope_params = kwargs
  1353. if name is None:
  1354. name = random_str(10)
  1355. check_dom_name_value(name, 'scope name')
  1356. def before_enter():
  1357. if create_scope:
  1358. if_exist = 'clear' if clear else None
  1359. set_scope(name, if_exist=if_exist, **scope_params)
  1360. return use_scope_(name=name, before_enter=before_enter)
  1361. class use_scope_:
  1362. def __init__(self, name, before_enter=None):
  1363. self.before_enter = before_enter
  1364. self.name = name
  1365. def __enter__(self):
  1366. if self.before_enter:
  1367. self.before_enter()
  1368. get_current_session().push_scope(self.name)
  1369. return self.name
  1370. def __exit__(self, exc_type, exc_val, exc_tb):
  1371. """
  1372. If this method returns True, it means that the context manager can handle the exception,
  1373. so that the with statement terminates the propagation of the exception
  1374. """
  1375. get_current_session().pop_scope()
  1376. return False # Propagate Exception
  1377. def __call__(self, func):
  1378. """decorator implement"""
  1379. @wraps(func)
  1380. def wrapper(*args, **kwargs):
  1381. self.__enter__()
  1382. try:
  1383. return func(*args, **kwargs)
  1384. finally:
  1385. self.__exit__(None, None, None)
  1386. @wraps(func)
  1387. async def coro_wrapper(*args, **kwargs):
  1388. self.__enter__()
  1389. try:
  1390. return await func(*args, **kwargs)
  1391. finally:
  1392. self.__exit__(None, None, None)
  1393. if iscoroutinefunction(func):
  1394. return coro_wrapper
  1395. else:
  1396. return wrapper