input.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315
  1. """从浏览器接收用户输入
  2. 本模块提供了一系列函数来从浏览器接收用户不同的形式的输入
  3. .. note::
  4. 本模块的输入函数返回一个携程对象,需要配合 ``await`` 关键字使用。相信我,忽略 ``await`` 关键字将会是你使用PyWebIo时常犯的错误。
  5. 输入函数大致分为两类,一类是单项输入::
  6. name = await input("What's your name")
  7. print("Your name is %s" % name)
  8. 直接调用输入函数并对其使用 ``await`` 便可获取输入值。
  9. 另一类是使用 `input_group` 的输入组::
  10. info = await input_group("User info",[
  11. input('Input your name', name='name'),
  12. input('Input your age', name='age', type=NUMBER)
  13. ])
  14. print(info['name'], info['age'])
  15. 输入组中需要在每一项输入函数中提供 ``name`` 参数来用于在结果中标识不同输入项
  16. """
  17. import json
  18. import logging
  19. from collections.abc import Mapping
  20. from base64 import b64decode
  21. from .framework import Global
  22. from .input_ctrl import send_msg, single_input, input_control
  23. from typing import Coroutine, Callable
  24. logger = logging.getLogger(__name__)
  25. TEXT = 'text'
  26. NUMBER = "number"
  27. PASSWORD = "password"
  28. CHECKBOX = 'checkbox'
  29. RADIO = 'radio'
  30. SELECT = 'select'
  31. TEXTAREA = 'textarea'
  32. __all__ = ['TEXT', 'NUMBER', 'PASSWORD', 'CHECKBOX', 'RADIO', 'SELECT', 'TEXTAREA',
  33. 'input', 'textarea', 'select', 'checkbox', 'radio', 'actions', 'file_upload', 'input_group']
  34. def _parse_args(kwargs):
  35. """处理传给各类input函数的原始参数,
  36. :return:(spec参数,valid_func)
  37. """
  38. # 对为None的参数忽律处理
  39. kwargs = {k: v for k, v in kwargs.items() if v is not None}
  40. kwargs.update(kwargs.get('other_html_attrs', {}))
  41. kwargs.pop('other_html_attrs', None)
  42. valid_func = kwargs.pop('valid_func', lambda _: None)
  43. return kwargs, valid_func
  44. def input(label, type=TEXT, *, valid_func=None, name='data', value=None, placeholder=None, required=None,
  45. readonly=None, disabled=None, help_text=None, **other_html_attrs) -> Coroutine:
  46. r"""文本输入
  47. :param str label: 输入框标签
  48. :param str type: 输入类型. 可使用的常量:`TEXT` , `NUMBER` , `PASSWORD` , `TEXTAREA`
  49. :param Callable valid_func: 输入值校验函数. 如果提供,当用户输入完毕或提交表单后校验函数将被调用.
  50. ``valid_func`` 接收输入值作为参数,当输入值有效时,返回 ``None`` ,当输入值无效时,返回错误提示字符串. 比如::
  51. def check_age(age):
  52. if age>30:
  53. return 'Too old'
  54. elif age<10:
  55. return 'Too young'
  56. await input('Input your age', type=NUMBER, valid_func=check_age)
  57. :param name: 输入框的名字. 与 `input_group` 配合使用,用于在输入组的结果中标识不同输入项
  58. :param str value: 输入框的初始值
  59. :param str placeholder: 输入框的提示内容。提示内容会在输入框未输入值时以浅色字体显示在输入框中
  60. :param bool required: 当前输入是否为必填项
  61. :param bool readonly: 输入框是否为只读
  62. :param bool disabled: 输入框是否禁用。禁用的输入的值在提交表单时不会被提交
  63. :param str help_text: 输入框的帮助文本。帮助文本会以小号字体显示在输入框下方
  64. :param other_html_attrs: 在输入框上附加的额外html属性。参考: https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/input#%E5%B1%9E%E6%80%A7
  65. :return: 一个协程。对其 `await` 后,返回输入提交的值
  66. """
  67. item_spec, valid_func = _parse_args(locals())
  68. # 参数检查
  69. allowed_type = {TEXT, NUMBER, PASSWORD, TEXTAREA}
  70. assert type in allowed_type, 'Input type not allowed.'
  71. def preprocess_func(d):
  72. if type == NUMBER:
  73. return int(d)
  74. return d
  75. return single_input(item_spec, valid_func, preprocess_func)
  76. def textarea(label, rows=6, *, code=None, maxlength=None, minlength=None, valid_func=None, name='data', value=None,
  77. placeholder=None, required=None, readonly=None, disabled=None, help_text=None, **other_html_attrs):
  78. r"""文本输入域
  79. :param int rows: 输入文本的行数(显示的高度)。输入的文本超出设定值时会显示滚动条
  80. :param int maxlength: 允许用户输入的最大字符长度 (Unicode) 。未指定表示无限长度
  81. :param int minlength: 允许用户输入的最小字符长度(Unicode)
  82. :param dict code: 通过提供 `Codemirror <https://codemirror.net/>`_ 参数让文本输入域具有代码编辑器样式::
  83. res = await textarea('Text area', code={
  84. 'mode': "python",
  85. 'theme': 'darcula'
  86. })
  87. 更多配置可以参考 https://codemirror.net/doc/manual.html#config
  88. :param - label, valid_func, name, value, placeholder, required, readonly, disabled, help_text, other_html_attrs: 与 `input` 输入函数的同名参数含义一致
  89. :return: 一个协程。对其 `await` 后,返回输入提交的值
  90. """
  91. item_spec, valid_func = _parse_args(locals())
  92. item_spec['type'] = TEXTAREA
  93. return single_input(item_spec, valid_func, lambda d: d)
  94. def _parse_select_options(options):
  95. # option 可用形式:
  96. # {value:, label:, [selected:,] [disabled:]}
  97. # (value, label, [selected,] [disabled])
  98. # value 单值,label等于value
  99. opts_res = []
  100. for opt in options:
  101. if isinstance(opt, Mapping):
  102. assert 'value' in opt and 'label' in opt, 'options item must have value and label key'
  103. elif isinstance(opt, list):
  104. assert len(opt) > 1 and len(opt) <= 4, 'options item format error'
  105. opt = dict(zip(('value', 'label', 'selected', 'disabled'), opt))
  106. else:
  107. opt = dict(value=opt, label=opt)
  108. opts_res.append(opt)
  109. return opts_res
  110. def select(label, options, *, multiple=None, valid_func=None, name='data', value=None,
  111. placeholder=None, required=None, readonly=None, disabled=None, help_text=None,
  112. **other_html_attrs):
  113. r"""下拉选择框
  114. :param list options: 可选项列表。列表项的可用形式有:
  115. * dict: ``{value: 选项值, label:选项标签, [selected:是否默认选中,] [disabled:是否禁止选中]}``
  116. * tuple or list: ``(value, label, [selected,] [disabled])``
  117. * 单值: 此时label和value使用相同的值
  118. 注意:若 ``multiple`` 选项不为 ``True`` 则可选项列表最多仅能有一项的 ``selected`` 为 ``True``
  119. :param multiple: 是否可以多选. 默认单选
  120. :param - label, valid_func, name, value, placeholder, required, readonly, disabled, help_text, other_html_attrs: 与 `input` 输入函数的同名参数含义一致
  121. :return: 一个协程。对其 `await` 后,返回输入提交的值
  122. """
  123. item_spec, valid_func = _parse_args(locals())
  124. item_spec['options'] = _parse_select_options(options)
  125. item_spec['type'] = SELECT
  126. return single_input(item_spec, valid_func, lambda d: d)
  127. def checkbox(label, options, *, inline=None, valid_func=None, name='data', value=None,
  128. placeholder=None, required=None, readonly=None, disabled=None, help_text=None, **other_html_attrs):
  129. r"""勾选选项
  130. :param list options: 可选项列表。格式与 `select` 函数的 ``options`` 参数含义一致
  131. :param bool inline: 是否将选项显示在一行上。默认每个选项单独占一行
  132. :param - label, valid_func, name, value, placeholder, required, readonly, disabled, help_text, other_html_attrs: 与 `input` 输入函数的同名参数含义一致
  133. :return: 一个协程。对其 `await` 后,返回输入提交的值
  134. """
  135. item_spec, valid_func = _parse_args(locals())
  136. item_spec['options'] = _parse_select_options(options)
  137. item_spec['type'] = CHECKBOX
  138. return single_input(item_spec, valid_func, lambda d: d)
  139. def radio(label, options, *, inline=None, valid_func=None, name='data', value=None,
  140. placeholder=None, required=None, readonly=None, disabled=None, help_text=None,
  141. **other_html_attrs):
  142. r"""单选选项
  143. :param list options: 可选项列表。格式与 `select` 函数的 ``options`` 参数含义一致
  144. :param bool inline: 是否将选项显示在一行上。默认每个选项单独占一行
  145. :param - label, valid_func, name, value, placeholder, required, readonly, disabled, help_text, other_html_attrs: 与 `input` 输入函数的同名参数含义一致
  146. :return: 一个协程。对其 `await` 后,返回输入提交的值
  147. """
  148. item_spec, valid_func = _parse_args(locals())
  149. item_spec['options'] = _parse_select_options(options)
  150. item_spec['type'] = RADIO
  151. return single_input(item_spec, valid_func, lambda d: d)
  152. def _parse_action_buttons(buttons):
  153. """
  154. :param label:
  155. :param actions: action 列表
  156. action 可用形式:
  157. {value:, label:, [disabled:]}
  158. (value, label, [disabled])
  159. value 单值,label等于value
  160. :return:
  161. """
  162. act_res = []
  163. for act in buttons:
  164. if isinstance(act, Mapping):
  165. assert 'value' in act and 'label' in act, 'actions item must have value and label key'
  166. elif isinstance(act, list):
  167. assert len(act) in (2, 3), 'actions item format error'
  168. act = dict(zip(('value', 'label', 'disabled'), act))
  169. else:
  170. act = dict(value=act, label=act)
  171. act_res.append(act)
  172. return act_res
  173. def actions(label, buttons, name='data', help_text=None):
  174. r"""按钮选项。
  175. 在浏览器上显示为多个按钮,与其他输入元素不同,用户点击按钮选项后会立即将整个表单提交,其他输入元素不同则需要手动点击表单的"提交"按钮。
  176. :param list buttons: 选项列表。列表项的可用形式有:
  177. * dict: ``{value:选项值, label:选项标签, [disabled:是否禁止选择]}``
  178. * tuple or list: ``(value, label, [disabled])``
  179. * 单值: 此时label和value使用相同的值
  180. :param - label, name, help_text: 与 `input` 输入函数的同名参数含义一致
  181. :return: 一个协程。对其 `await` 后,返回输入提交的值
  182. """
  183. item_spec, valid_func = _parse_args(locals())
  184. item_spec['type'] = 'actions'
  185. item_spec['buttons'] = _parse_action_buttons(buttons)
  186. return single_input(item_spec, valid_func, lambda d: d)
  187. def file_upload(label, accept=None, name='data', placeholder='Choose file', help_text=None, **other_html_attrs):
  188. r"""文件上传。
  189. :param accept: 单值或列表, 表示可接受的文件类型。单值或列表项支持的形式有:
  190. * 以 ``.`` 字符开始的文件扩展名(例如:``.jpg, .png, .doc``)。
  191. 注意:截止本文档编写之时,微信内置浏览器还不支持这种语法
  192. * 一个有效的 MIME 类型。
  193. 例如: ``application/pdf`` 、 ``audio/*`` 表示音频文件、``video/*`` 表示视频文件、``image/*`` 表示图片文件
  194. 参考 https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types
  195. :type accept: str or list
  196. :param - label, name, placeholder, help_text, other_html_attrs: 与 `input` 输入函数的同名参数含义一致
  197. :return: 一个协程。对其 `await` 后,返回形如 ``{'filename': 文件名, 'content':文件二进制数据(bytes object)}`` 的 ``dict``
  198. """
  199. item_spec, valid_func = _parse_args(locals())
  200. item_spec['type'] = 'file'
  201. def read_file(data): # data: {'filename':, 'dataurl'}
  202. header, encoded = data['dataurl'].split(",", 1)
  203. data['content'] = b64decode(encoded)
  204. return data
  205. return single_input(item_spec, valid_func, read_file)
  206. def input_group(label, inputs, valid_func=None):
  207. r"""输入组。向页面上展示一组输入
  208. :param str label: 输入组标签
  209. :param list inputs: 输入项列表。每一项为单项输入函数的返回值
  210. :param Callable valid_func: 输入组校验函数。
  211. 函数签名:``callback(data) -> (name, error_msg)``
  212. ``valid_func`` 接收整个表单的值为参数,当校验表单值有效时,返回 ``None`` ,当某项输入值无效时,返回出错输入项的 ``name`` 值和错误提示. 比如::
  213. def check_form(data):
  214. if len(data['name']) > 6:
  215. return ('name', '名字太长!')
  216. if data['age'] <= 0:
  217. return ('age', '年龄不能为负数!')
  218. data = await input_group("Basic info",[
  219. input('Input your name', name='name'),
  220. input('Repeat your age', name='age', type=NUMBER)
  221. ], valid_func=check_form)
  222. print(data['name'], data['age'])
  223. :return: 一个协程。对其 `await` 后,返回一个 ``dict`` , 其键为输入项的 ``name`` 值,字典值为输入项的值
  224. """
  225. spec_inputs = []
  226. preprocess_funcs = {}
  227. item_valid_funcs = {}
  228. for single_input_cr in inputs:
  229. input_kwargs = dict(single_input_cr.cr_frame.f_locals) # 拷贝一份,不可以对locals进行修改
  230. single_input_cr.close()
  231. input_name = input_kwargs['item_spec']['name']
  232. preprocess_funcs[input_name] = input_kwargs['preprocess_func']
  233. item_valid_funcs[input_name] = input_kwargs['valid_func']
  234. spec_inputs.append(input_kwargs['item_spec'])
  235. # def add_autofocus(spec_inputs):
  236. if all('auto_focus' not in i for i in spec_inputs): # 每一个输入项都没有设置autofocus参数
  237. for i in spec_inputs:
  238. text_inputs = {TEXT, NUMBER, PASSWORD, SELECT} # todo update
  239. if i.get('type') in text_inputs:
  240. i['auto_focus'] = True
  241. break
  242. spec = dict(label=label, inputs=spec_inputs)
  243. return input_control(spec, preprocess_funcs=preprocess_funcs, item_valid_funcs=item_valid_funcs,
  244. form_valid_funcs=valid_func)