1
0

pin.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407
  1. """
  2. ``pywebio.pin`` --- Persistent input
  3. ===========================================================================
  4. *pin == Persistent input == Pinning input widget to the page*
  5. Overview
  6. ------------------
  7. As you already know, the input function of PyWebIO is blocking
  8. and the input form will be destroyed after successful submission.
  9. In most cases, it enough to use this way to get input.
  10. However in some cases, you may want to make the input form **not** disappear after submission,
  11. and can continue to receive input.
  12. So PyWebIO provides the ``pin`` module to achieve persistent input by pinning input widgets to the page.
  13. The ``pin`` module achieves persistent input in 3 parts:
  14. First, this module provides some pin widgets.
  15. Pin widgets are not different from output widgets in ``pywebio.output`` module,
  16. besides that they can also receive input.
  17. This code outputs an text input pin widget:
  18. .. exportable-codeblock::
  19. :name: pin-put_input
  20. :summary: `put_input()` example
  21. put_input('input', label='This is a input widget')
  22. In fact, the usage of pin widget function is same as the output function.
  23. You can use it as part of the combined output, or you can output pin widget to a scope:
  24. .. exportable-codeblock::
  25. :name: pin-basic
  26. :summary: Pin widget as output function
  27. put_row([
  28. put_input('input'),
  29. put_select('select', options=['A', 'B', 'C'])
  30. ])
  31. with use_scope('search-area'):
  32. put_input('search', placeholder='Search')
  33. Then, you can use the `pin` object to get the value of pin widget:
  34. .. exportable-codeblock::
  35. :name: get-pin-value
  36. :summary: Use the `pin` object to get the value of pin widget
  37. put_input('pin_name')
  38. put_buttons(['Get Pin Value'], lambda _: put_text(pin.pin_name))
  39. The first parameter that the pin widget function receives is the name of the pin widget.
  40. You can get the current value of the pin widget via the attribute of the same name of the `pin` object.
  41. In addition, the `pin` object also supports getting the value of the pin widget by index, that is to say::
  42. pin['pin_name'] == pin.pin_name
  43. There are also two useful functions when you use the pin module: `pin_wait_change()` and `pin_update()`.
  44. Since the pin widget functions is not blocking,
  45. `pin_wait_change()` is used to wait for the value of one of a list of pin widget to change, it 's a blocking function.
  46. `pin_update()` can be used to update attributes of pin widgets.
  47. Pin widgets
  48. ------------------
  49. Each pin widget function corresponds to an input function of :doc:`input <./input>` module.
  50. The function of pin widget supports most of the parameters of the corresponding input function.
  51. Here lists the difference between the two in parameters:
  52. * The first parameter of pin widget function is always the name of the widget,
  53. and if you output two pin widgets with the same name, the previous one will expire.
  54. * Pin functions don't support the ``on_change`` and ``validate`` callbacks, and the ``required`` parameter.
  55. (There is a :func:`pin_on_change()` function as an alternative to ``on_change``)
  56. * Pin functions have additional ``scope`` and ``position`` parameters for output control.
  57. .. autofunction:: put_input
  58. .. autofunction:: put_textarea
  59. .. autofunction:: put_select
  60. .. autofunction:: put_checkbox
  61. .. autofunction:: put_radio
  62. .. autofunction:: put_slider
  63. .. autofunction:: put_actions
  64. .. autofunction:: put_file_upload
  65. Pin utils
  66. ------------------
  67. .. data:: pin
  68. Pin widgets value getter and setter.
  69. You can use attribute or key index of ``pin`` object to get the current value of a pin widget.
  70. By default, when accessing the value of a widget that does not exist, it returns ``None`` instead of
  71. throwing an exception. You can enable the error raising by ``pin.use_strict()`` method.
  72. You can also use the ``pin`` object to set the value of pin widget:
  73. .. exportable-codeblock::
  74. :name: set-pin-value
  75. :summary: Use the `pin` object to set the value of pin widget
  76. import time # ..demo-only
  77. put_input('counter', type='number', value=0)
  78. while True:
  79. pin.counter = pin.counter + 1 # Equivalent to: pin['counter'] = pin['counter'] + 1
  80. time.sleep(1)
  81. Note: When using :ref:`coroutine-based session <coroutine_based_session>`,
  82. you need to use the ``await pin.name`` (or ``await pin['name']``) syntax to get pin widget value.
  83. Use `pin.pin.use_strict()` to enable strict mode for getting pin widget value.
  84. An ``AssertionError`` will be raised when try to get value of pin widgets that are currently not in the page.
  85. .. autofunction:: get_pin_values
  86. .. autofunction:: pin_wait_change
  87. .. autofunction:: pin_update
  88. .. autofunction:: pin_on_change
  89. """
  90. import string
  91. from typing import Any, Callable, Dict, List, Optional, Tuple, Union
  92. from pywebio.input import parse_input_update_spec
  93. from pywebio.output import Output, OutputPosition, _get_output_spec
  94. from .io_ctrl import output_register_callback, send_msg, single_input_kwargs
  95. from .session import chose_impl, next_client_event
  96. from .utils import check_dom_name_value
  97. _pin_name_chars = set(string.ascii_letters + string.digits + '_-')
  98. __all__ = ['put_input', 'put_textarea', 'put_select', 'put_checkbox', 'put_radio', 'put_slider', 'put_actions',
  99. 'put_file_upload', 'pin', 'pin_update', 'pin_wait_change', 'pin_on_change', 'get_pin_values']
  100. def _pin_output(single_input_return, scope, position):
  101. input_kwargs = single_input_kwargs(single_input_return)
  102. spec = _get_output_spec('pin', input=input_kwargs['item_spec'], scope=scope, position=position)
  103. return Output(spec)
  104. def put_input(name: str, type: str = 'text', *, label: str = '', value: str = None, placeholder: str = None,
  105. readonly: bool = None, datalist: List[str] = None, help_text: str = None, scope: str = None,
  106. position: int = OutputPosition.BOTTOM) -> Output:
  107. """Output an input widget. Refer to: `pywebio.input.input()`"""
  108. from pywebio.input import input
  109. check_dom_name_value(name, 'pin `name`')
  110. single_input_return = input(name=name, label=label, value=value, type=type, placeholder=placeholder,
  111. readonly=readonly, datalist=datalist, help_text=help_text)
  112. return _pin_output(single_input_return, scope, position)
  113. def put_textarea(name: str, *, label: str = '', rows: int = 6, code: Union[bool, Dict] = None, maxlength: int = None,
  114. minlength: int = None, value: str = None, placeholder: str = None, readonly: bool = None,
  115. help_text: str = None, scope: str = None, position: int = OutputPosition.BOTTOM) -> Output:
  116. """Output a textarea widget. Refer to: `pywebio.input.textarea()`"""
  117. from pywebio.input import textarea
  118. check_dom_name_value(name, 'pin `name`')
  119. single_input_return = textarea(
  120. name=name, label=label, rows=rows, code=code, maxlength=maxlength,
  121. minlength=minlength, value=value, placeholder=placeholder, readonly=readonly, help_text=help_text)
  122. return _pin_output(single_input_return, scope, position)
  123. def put_select(name: str, options: List[Union[Dict[str, Any], Tuple, List, str]] = None, *, label: str = '',
  124. multiple: bool = None, value: Union[List, str] = None, native: bool = None, help_text: str = None,
  125. scope: str = None, position: int = OutputPosition.BOTTOM) -> Output:
  126. """Output a select widget. Refer to: `pywebio.input.select()`
  127. .. note::
  128. Unlike `pywebio.input.select()`, when ``multiple=True`` and the user is using PC/macOS, `put_select()` will use
  129. `bootstrap-select <https://github.com/snapappointments/bootstrap-select>`_ by default. Setting
  130. ``native=True`` will force PyWebIO to use native select component on all platforms and vice versa.
  131. """
  132. from pywebio.input import select
  133. check_dom_name_value(name, 'pin `name`')
  134. single_input_return = select(name=name, options=options, label=label, multiple=multiple,
  135. value=value, help_text=help_text, native=native)
  136. return _pin_output(single_input_return, scope, position)
  137. def put_checkbox(name: str, options: List[Union[Dict[str, Any], Tuple, List, str]] = None, *, label: str = '',
  138. inline: bool = None, value: List = None, help_text: str = None, scope: str = None,
  139. position: int = OutputPosition.BOTTOM) -> Output:
  140. """Output a checkbox widget. Refer to: `pywebio.input.checkbox()`"""
  141. from pywebio.input import checkbox
  142. check_dom_name_value(name, 'pin `name`')
  143. single_input_return = checkbox(name=name, options=options, label=label, inline=inline, value=value,
  144. help_text=help_text)
  145. return _pin_output(single_input_return, scope, position)
  146. def put_radio(name: str, options: List[Union[Dict[str, Any], Tuple, List, str]] = None, *, label: str = '',
  147. inline: bool = None, value: str = None, help_text: str = None, scope: str = None,
  148. position: int = OutputPosition.BOTTOM) -> Output:
  149. """Output a radio widget. Refer to: `pywebio.input.radio()`"""
  150. from pywebio.input import radio
  151. check_dom_name_value(name, 'pin `name`')
  152. single_input_return = radio(name=name, options=options, label=label, inline=inline, value=value,
  153. help_text=help_text)
  154. return _pin_output(single_input_return, scope, position)
  155. def put_slider(name: str, *, label: str = '', value: Union[int, float] = 0, min_value: Union[int, float] = 0,
  156. max_value: Union[int, float] = 100, step: int = 1, required: bool = None, help_text: str = None,
  157. scope: str = None, position: int = OutputPosition.BOTTOM) -> Output:
  158. """Output a slide widget. Refer to: `pywebio.input.slider()`"""
  159. from pywebio.input import slider
  160. check_dom_name_value(name, 'pin `name`')
  161. single_input_return = slider(name=name, label=label, value=value, min_value=min_value, max_value=max_value,
  162. step=step, required=required, help_text=help_text)
  163. return _pin_output(single_input_return, scope, position)
  164. def put_actions(name: str, *, label: str = '', buttons: List[Union[Dict[str, Any], Tuple, List, str]] = None,
  165. help_text: str = None, scope: str = None, position: int = OutputPosition.BOTTOM) -> Output:
  166. """Output a group of action button. Refer to: `pywebio.input.actions()`
  167. Unlike the ``actions()``, ``put_actions()`` won't submit any form, it will only set the value of the pin widget.
  168. Only 'submit' type button is available in pin widget version.
  169. .. versionadded:: 1.4
  170. """
  171. from pywebio.input import actions
  172. check_dom_name_value(name, 'pin `name`')
  173. single_input_return = actions(name=name, label=label, buttons=buttons, help_text=help_text)
  174. input_kwargs = single_input_kwargs(single_input_return)
  175. for btn in input_kwargs['item_spec']['buttons']:
  176. assert btn['type'] == 'submit', "The `put_actions()` pin widget only accept 'submit' type button."
  177. return _pin_output(input_kwargs, scope, position)
  178. def put_file_upload(name: str, *, label: str = '', accept: Union[List, str] = None, placeholder: str = 'Choose file',
  179. multiple: bool = False, max_size: Union[int, str] = 0, max_total_size: Union[int, str] = 0,
  180. help_text: str = None, scope: str = None, position: int = OutputPosition.BOTTOM) -> Output:
  181. """Output a file uploading widget. Refer to: `pywebio.input.file_upload()`"""
  182. from pywebio.input import file_upload
  183. check_dom_name_value(name, 'pin `name`')
  184. single_input_return = file_upload(label=label, accept=accept, name=name, placeholder=placeholder, multiple=multiple,
  185. max_size=max_size, max_total_size=max_total_size, help_text=help_text)
  186. return _pin_output(single_input_return, scope, position)
  187. @chose_impl
  188. def get_client_val():
  189. res = yield next_client_event()
  190. assert res['event'] == 'js_yield', "Internal Error, please report this bug on " \
  191. "https://github.com/wang0618/PyWebIO/issues"
  192. return res['data']
  193. @chose_impl
  194. def _get_pin_value(name, strict):
  195. send_msg('pin_values', spec=dict(names=[name]))
  196. data = yield get_client_val()
  197. if strict:
  198. assert name in data, 'pin widget "%s" doesn\'t exist.' % name
  199. return data.get(name)
  200. @chose_impl
  201. def _get_pin_values(names: list[str]):
  202. send_msg('pin_values', spec=dict(names=names))
  203. data = yield get_client_val()
  204. return data
  205. def get_pin_values(names: list[str]) -> dict[str, Any]:
  206. """
  207. Get the value of multiple pin widgets.
  208. Compared to using the :data:`pin` object to get the value of the pin widget one by one,
  209. this function can get the value of multiple pin widgets at once and is more efficient
  210. when getting the value of multiple pin widgets.
  211. :return: A dict, the key is the name of the pin widget, and the value is the value of the pin widget.
  212. If the pin widget does not exist, the dict will not contain the corresponding key.
  213. """
  214. return _get_pin_values(names)
  215. class Pin_:
  216. _strict = False
  217. def use_strict(self):
  218. """
  219. Enable strict mode for getting pin widget value.
  220. An AssertionError will be raised when try to get value of pin widgets that are currently not in the page.
  221. """
  222. object.__setattr__(self, '_strict', True)
  223. def __getattr__(self, name: str):
  224. """__getattr__ is only invoked if the attribute wasn't found the usual ways"""
  225. if name.startswith('__'):
  226. raise AttributeError('Pin object has no attribute %r' % name)
  227. return self.__getitem__(name)
  228. def __getitem__(self, name: str):
  229. check_dom_name_value(name, 'pin `name`')
  230. return _get_pin_value(name, self._strict)
  231. def __setattr__(self, name: str, value):
  232. """
  233. __setattr__ will be invoked regardless of whether the attribute be found
  234. """
  235. assert name != 'use_strict', "'use_strict' is a reserve name, can't use as pin widget name"
  236. check_dom_name_value(name, 'pin `name`')
  237. self.__setitem__(name, value)
  238. def __setitem__(self, name: str, value):
  239. send_msg('pin_update', spec=dict(name=name, attributes={"value": value}))
  240. # pin widgets value getter (and setter).
  241. pin = Pin_()
  242. def pin_wait_change(*names, timeout: Optional[int] = None):
  243. """``pin_wait_change()`` listens to a list of pin widgets, when the value of any widgets changes,
  244. the function returns with the name and value of the changed widget.
  245. :param str names: List of names of pin widget
  246. :param int/None timeout: If ``timeout`` is a positive number, ``pin_wait_change()`` blocks at most ``timeout`` seconds
  247. and returns ``None`` if no changes to the widgets within that time. Set to ``None`` (the default) to disable timeout.
  248. :return dict/None: ``{"name": name of the changed widget, "value": current value of the changed widget }`` ,
  249. when a timeout occurs, return ``None``.
  250. Example:
  251. .. exportable-codeblock::
  252. :name: pin_wait_change
  253. :summary: `pin_wait_change()` example
  254. put_input('a', type='number', value=0)
  255. put_input('b', type='number', value=0)
  256. while True:
  257. changed = pin_wait_change('a', 'b')
  258. with use_scope('res', clear=True):
  259. put_code(changed)
  260. put_text("a + b = %s" % (pin.a + pin.b))
  261. :demo_host:`Here </markdown_previewer>` is an demo of using `pin_wait_change()` to make a markdown previewer.
  262. Note that: updating value with the :data:`pin` object or `pin_update()`
  263. does not trigger `pin_wait_change()` to return.
  264. When using :ref:`coroutine-based session <coroutine_based_session>`,
  265. you need to use the ``await pin_wait_change()`` syntax to invoke this function.
  266. """
  267. assert len(names) >= 1, "`names` can't be empty."
  268. if len(names) == 1 and isinstance(names[0], (list, tuple)):
  269. names = names[0]
  270. send_msg('pin_wait', spec=dict(names=names, timeout=timeout))
  271. return get_client_val()
  272. def pin_update(name: str, **spec):
  273. """Update attributes of pin widgets.
  274. :param str name: The ``name`` of the target input widget.
  275. :param spec: The pin widget parameters need to be updated.
  276. Note that those parameters can not be updated: ``type``, ``name``, ``code``, ``multiple``
  277. """
  278. check_dom_name_value(name, 'pin `name`')
  279. attributes = parse_input_update_spec(spec)
  280. send_msg('pin_update', spec=dict(name=name, attributes=attributes))
  281. def pin_on_change(name: str, onchange: Callable[[Any], None] = None, clear: bool = False, init_run: bool = False, **callback_options):
  282. """
  283. Bind a callback function to pin widget, the function will be called when user change the value of the pin widget.
  284. The ``onchange`` callback is invoked with one argument, the changed value of the pin widget.
  285. You can bind multiple functions to one pin widget, those functions will be invoked sequentially
  286. (default behavior, can be changed by `clear` parameter).
  287. :param str name: pin widget name
  288. :param callable onchange: callback function
  289. :param bool clear: whether to clear the previous callbacks bound to this pin widget.
  290. If you just want to clear callbacks and not set new callback, use ``pin_on_change(name, clear=True)``.
  291. :param bool init_run: whether to run the ``onchange`` callback once immediately before the pin widget changed.
  292. This parameter can be used to initialize the output.
  293. :param callback_options: Other options of the ``onclick`` callback.
  294. Refer to the ``callback_options`` parameter of :func:`put_buttons() <pywebio.output.put_buttons>`
  295. .. versionadded:: 1.6
  296. """
  297. assert not (onchange is None and clear is False), "When `onchange` is `None`, `clear` must be `True`"
  298. if onchange is not None:
  299. callback = lambda data: onchange(data['value'])
  300. callback_id = output_register_callback(callback, **callback_options)
  301. if init_run:
  302. onchange(pin[name])
  303. else:
  304. callback_id = None
  305. send_msg('pin_onchange', spec=dict(name=name, callback_id=callback_id, clear=clear))