input.py 17 KB

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