input.py 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673
  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. 输入默认可以忽略,如果需要用户必须提供值,则需要在输入函数中传入 ``required=True`` (部分输入函数不支持 ``required`` 参数)
  17. 函数清单
  18. ------------
  19. .. list-table::
  20. * - 函数
  21. - 简介
  22. * - `input <pywebio.input.input>`
  23. - 文本输入
  24. * - `textarea <pywebio.input.textarea>`
  25. - 多行文本输入
  26. * - `select <pywebio.input.select>`
  27. - 下拉选择框
  28. * - `checkbox <pywebio.input.checkbox>`
  29. - 勾选选项
  30. * - `radio <pywebio.input.radio>`
  31. - 单选选项
  32. * - `actions <pywebio.input.actions>`
  33. - 按钮选项
  34. * - `file_upload <pywebio.input.file_upload>`
  35. - 文件上传
  36. * - `input_group <pywebio.input.input_group>`
  37. - 输入组
  38. 函数文档
  39. ------------
  40. """
  41. import logging
  42. from base64 import b64decode
  43. from collections.abc import Mapping
  44. from functools import partial
  45. from .io_ctrl import single_input, input_control, output_register_callback
  46. from .session import get_current_session, get_current_task_id
  47. from .utils import Setter
  48. logger = logging.getLogger(__name__)
  49. TEXT = 'text'
  50. NUMBER = "number"
  51. FLOAT = "float"
  52. PASSWORD = "password"
  53. URL = "url"
  54. DATE = "date"
  55. TIME = "time"
  56. CHECKBOX = 'checkbox'
  57. RADIO = 'radio'
  58. SELECT = 'select'
  59. TEXTAREA = 'textarea'
  60. __all__ = ['TEXT', 'NUMBER', 'FLOAT', 'PASSWORD', 'URL', 'DATE', 'TIME', 'input', 'textarea', 'select',
  61. 'checkbox', 'radio', 'actions', 'file_upload', 'input_group']
  62. def _parse_args(kwargs, excludes=()):
  63. """处理传给各类输入函数的原始参数
  64. - excludes: 排除的参数
  65. - 对为None的参数忽略处理
  66. :return:(spec参数,valid_func)
  67. """
  68. kwargs = {k: v for k, v in kwargs.items() if v is not None and k not in excludes}
  69. kwargs.update(kwargs.get('other_html_attrs', {}))
  70. kwargs.pop('other_html_attrs', None)
  71. valid_func = kwargs.pop('valid_func', lambda _: None)
  72. return kwargs, valid_func
  73. def input(label='', type=TEXT, *, valid_func=None, name=None, value=None, action=None, placeholder=None, required=None,
  74. readonly=None, datalist=None, help_text=None, **other_html_attrs):
  75. r"""文本输入
  76. :param str label: 输入框标签
  77. :param str type: 输入类型. 可使用的常量:`TEXT` , `NUMBER` , `FLOAT` , `PASSWORD` , `URL` , `DATE` , `TIME`
  78. 其中 `DATE` , `TIME` 类型在某些浏览器上不被支持,详情见 https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#Browser_compatibility
  79. :param Callable valid_func: 输入值校验函数. 如果提供,当用户输入完毕或提交表单后校验函数将被调用.
  80. ``valid_func`` 接收输入值作为参数,当输入值有效时,返回 ``None`` ,当输入值无效时,返回错误提示字符串. 比如:
  81. .. exportable-codeblock::
  82. :name: input-valid-func
  83. :summary: `input()` 输入值校验
  84. def check_age(age):
  85. if age>30:
  86. return 'Too old'
  87. elif age<10:
  88. return 'Too young'
  89. input('Input your age', type=NUMBER, valid_func=check_age)
  90. :param name: 输入框的名字. 与 `input_group` 配合使用,用于在输入组的结果中标识不同输入项. **在单个输入中,不可以设置该参数!**
  91. :param str value: 输入框的初始值
  92. :type action: tuple(label:str, callback:callable)
  93. :param action: 在输入框右侧显示一个按钮,可通过点击按钮为输入框设置值。
  94. ``label`` 为按钮的显示文本, ``callback`` 为按钮点击的回调函数。
  95. 回调函数需要接收一个 ``set_value`` 位置参数, ``set_value`` 是一个可调用对象,接受单参数调用和双参数调用。
  96. 单参数调用时,签名为 ``set_value(value:str)`` ,调用set_value即可将表单项的值设置为传入的 ``value`` 参数。
  97. 双参数调用时,签名为 ``set_value(value:any, label:str)`` ,其中:
  98. * ``value`` 参数为最终输入项的返回值,可以为任意Python对象,并不会传递给用户浏览器
  99. * ``label`` 参数用于显示在用户表单项上
  100. 使用双参数调用 ``set_value`` 后,用户表单项会变为只读状态。
  101. 双参数调用的使用场景为:表单项的值通过回调动态生成,同时希望用户表单显示的和实际提交的数据不同(例如表单项上可以显示更人性化的内容,而表单项的值则可以保存更方便被处理的对象)
  102. 使用示例:
  103. .. exportable-codeblock::
  104. :name: input-action
  105. :summary: `input()`使用action参数动态设置表单项的值
  106. import time
  107. def set_now_ts(set_value):
  108. set_value(int(time.time()))
  109. ts = input('Timestamp', type=NUMBER, action=('Now', set_now_ts))
  110. put_text('Timestamp:', ts) # ..demo-only
  111. ## ----
  112. from datetime import date,timedelta
  113. def select_date(set_value):
  114. with popup('Select Date'):
  115. put_buttons(['Today'], onclick=[lambda: set_value(date.today(), 'Today')])
  116. put_buttons(['Yesterday'], onclick=[lambda: set_value(date.today() - timedelta(days=1), 'Yesterday')])
  117. d = input('Date', action=('Select', select_date), readonly=True)
  118. put_text(type(d), d)
  119. Note: 当使用 :ref:`基于协程的会话实现 <coroutine_based_session>` 时,回调函数 ``callback`` 可以为协程函数.
  120. :param str placeholder: 输入框的提示内容。提示内容会在输入框未输入值时以浅色字体显示在输入框中
  121. :param bool required: 当前输入是否为必填项
  122. :param bool readonly: 输入框是否为只读
  123. :param list datalist: 输入建议内容列表,在页面上的显示效果为下拉候选列表,用户可以忽略建议内容列表而输入其他内容。仅当输入类型 ``type`` 为 `TEXT` 时可用
  124. :param str help_text: 输入框的帮助文本。帮助文本会以小号字体显示在输入框下方
  125. :param other_html_attrs: 在输入框上附加的额外html属性。参考: https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/input#%E5%B1%9E%E6%80%A7
  126. :return: 用户输入的值
  127. """
  128. item_spec, valid_func = _parse_args(locals(), excludes=('action',))
  129. # 参数检查
  130. allowed_type = {TEXT, NUMBER, FLOAT, PASSWORD, URL, DATE, TIME}
  131. assert type in allowed_type, 'Input type not allowed.'
  132. if type == FLOAT:
  133. item_spec['type'] = TEXT
  134. value_setter = None
  135. if action:
  136. label, callback = action
  137. task_id = get_current_task_id()
  138. value_setter = Setter()
  139. def _set_value(value, label=value_setter):
  140. spec = {
  141. 'target_name': item_spec.get('name', 'data'),
  142. 'attributes': {'value': value}
  143. }
  144. if label is not value_setter:
  145. value_setter.label = label
  146. spec['attributes']['value'] = label
  147. spec['attributes']['readonly'] = True
  148. value_setter.value = value
  149. msg = dict(command='update_input', task_id=task_id, spec=spec)
  150. get_current_session().send_task_command(msg)
  151. callback_id = output_register_callback(lambda _: callback(_set_value))
  152. item_spec['action'] = dict(label=label, callback_id=callback_id)
  153. def preprocess_func(d): # 将用户提交的原始数据进行转换
  154. if value_setter is not None and value_setter.label == d:
  155. return value_setter.value
  156. if type == NUMBER:
  157. d = int(d)
  158. elif type == FLOAT:
  159. d = float(d)
  160. return d
  161. return single_input(item_spec, valid_func, preprocess_func)
  162. def textarea(label='', *, rows=6, code=None, maxlength=None, minlength=None, valid_func=None, name=None, value=None,
  163. placeholder=None, required=None, readonly=None, help_text=None, **other_html_attrs):
  164. r"""文本输入域(多行文本输入)
  165. :param int rows: 输入文本的行数(显示的高度)。输入的文本超出设定值时会显示滚动条
  166. :param int maxlength: 允许用户输入的最大字符长度 (Unicode) 。未指定表示无限长度
  167. :param int minlength: 允许用户输入的最小字符长度(Unicode)
  168. :param dict code: 通过提供 `Codemirror <https://codemirror.net/>`_ 参数让文本输入域具有代码编辑器样式:
  169. .. exportable-codeblock::
  170. :name: textarea-code
  171. :summary: `textarea()`代码编辑
  172. res = textarea('Text area', code={
  173. 'mode': "python",
  174. 'theme': 'darcula'
  175. })
  176. put_code(res, language='python') # ..demo-only
  177. 更多配置可以参考 https://codemirror.net/doc/manual.html#config
  178. :param - label, valid_func, name, value, placeholder, required, readonly, help_text, other_html_attrs: 与 `input` 输入函数的同名参数含义一致
  179. :return: 用户输入的文本
  180. """
  181. item_spec, valid_func = _parse_args(locals())
  182. item_spec['type'] = TEXTAREA
  183. return single_input(item_spec, valid_func, lambda d: d)
  184. def _parse_select_options(options):
  185. # 转换 select、checkbox、radio函数中的 options 参数为统一的格式
  186. # option 可用形式:
  187. # {value:, label:, [selected:,] [disabled:]}
  188. # (value, label, [selected,] [disabled])
  189. # value 单值,label等于value
  190. opts_res = []
  191. for opt in options:
  192. if isinstance(opt, Mapping):
  193. assert 'value' in opt and 'label' in opt, 'options item must have value and label key'
  194. elif isinstance(opt, (list, tuple)):
  195. assert len(opt) > 1 and len(opt) <= 4, 'options item format error'
  196. opt = dict(zip(('label', 'value', 'selected', 'disabled'), opt))
  197. else:
  198. opt = dict(value=opt, label=opt)
  199. opt['value'] = str(opt['value'])
  200. opts_res.append(opt)
  201. return opts_res
  202. def _set_options_selected(options, value):
  203. """使用value为options的项设置selected"""
  204. if not isinstance(value, (list, tuple)):
  205. value = [value]
  206. for opt in options:
  207. if opt['value'] in value:
  208. opt['selected'] = True
  209. return options
  210. def select(label='', options=None, *, multiple=None, valid_func=None, name=None, value=None, required=None,
  211. help_text=None, **other_html_attrs):
  212. r"""下拉选择框。
  213. 默认单选,设置 ``multiple`` 参数后,可以多选。但都至少要选择一个选项。
  214. :param list options: 可选项列表。列表项的可用形式有:
  215. * dict: ``{label:选项标签, value: 选项值, [selected:是否默认选中,] [disabled:是否禁止选中]}``
  216. * tuple or list: ``(label, value, [selected,] [disabled])``
  217. * 单值: 此时label和value使用相同的值
  218. 注意:
  219. 1. options 中的 value 最终会转换成字符串。 select 返回值也是字符串(或字符串列表)
  220. 2. 若 ``multiple`` 选项不为 ``True`` 则可选项列表最多仅能有一项的 ``selected`` 为 ``True``。
  221. :param bool multiple: 是否可以多选. 默认单选
  222. :param value: 下拉选择框初始选中项的值。当 ``multiple=True`` 时, ``value`` 需为list,否则为单个选项的值。
  223. 你也可以通过设置 ``options`` 列表项中的 ``selected`` 字段来设置默认选中选项。
  224. 最终选中项为 ``value`` 参数和 ``options`` 中设置的并集。
  225. :type value: list or str
  226. :param bool required: 是否至少选择一项
  227. :param - label, valid_func, name, help_text, other_html_attrs: 与 `input` 输入函数的同名参数含义一致
  228. :return: 字符串/字符串列表。如果 ``multiple=True`` 时,返回用户选中的 options 中的值的列表;不设置 ``multiple`` 时,返回用户选中的 options 中的值
  229. """
  230. assert options is not None, ValueError('Required `options` parameter in select()')
  231. item_spec, valid_func = _parse_args(locals(), excludes=['value'])
  232. item_spec['options'] = _parse_select_options(options)
  233. if value is not None:
  234. item_spec['options'] = _set_options_selected(item_spec['options'], value)
  235. item_spec['type'] = SELECT
  236. return single_input(item_spec, valid_func, lambda d: d)
  237. def checkbox(label='', options=None, *, inline=None, valid_func=None, name=None, value=None, help_text=None,
  238. **other_html_attrs):
  239. r"""勾选选项。可以多选,也可以不选。
  240. :param list options: 可选项列表。格式与 `select` 函数的 ``options`` 参数含义一致
  241. :param bool inline: 是否将选项显示在一行上。默认每个选项单独占一行
  242. :param list value: 勾选选项初始选中项。为选项值的列表。
  243. 你也可以通过设置 ``options`` 列表项中的 ``selected`` 字段来设置默认选中选项。
  244. :param - label, valid_func, name, help_text, other_html_attrs: 与 `input` 输入函数的同名参数含义一致
  245. :return: 用户选中的 options 中的值的列表。当用户没有勾选任何选项时,返回空列表
  246. """
  247. assert options is not None, ValueError('Required `options` parameter in checkbox()')
  248. item_spec, valid_func = _parse_args(locals())
  249. item_spec['options'] = _parse_select_options(options)
  250. if value is not None:
  251. del item_spec['value']
  252. item_spec['options'] = _set_options_selected(item_spec['options'], value)
  253. item_spec['type'] = CHECKBOX
  254. return single_input(item_spec, valid_func, lambda d: d)
  255. def radio(label='', options=None, *, inline=None, valid_func=None, name=None, value=None, required=None,
  256. help_text=None, **other_html_attrs):
  257. r"""单选选项
  258. :param list options: 可选项列表。格式与 `select` 函数的 ``options`` 参数含义一致
  259. :param bool inline: 是否将选项显示在一行上。默认每个选项单独占一行
  260. :param str value: 单选选项初始选中项的值。
  261. 你也可以通过设置 ``options`` 列表项中的 ``selected`` 字段来设置默认选中选项。
  262. :param bool required: 是否至少选择一项
  263. :param - label, valid_func, name, help_text, other_html_attrs: 与 `input` 输入函数的同名参数含义一致
  264. :return: 用户选中的选项的值(字符串)
  265. """
  266. assert options is not None, ValueError('Required `options` parameter in radio()')
  267. item_spec, valid_func = _parse_args(locals())
  268. item_spec['options'] = _parse_select_options(options)
  269. if value is not None:
  270. del item_spec['value']
  271. item_spec['options'] = _set_options_selected(item_spec['options'], value)
  272. if required is not None:
  273. del item_spec['required']
  274. item_spec['options'][-1]['required'] = required
  275. item_spec['type'] = RADIO
  276. return single_input(item_spec, valid_func, lambda d: d)
  277. def _parse_action_buttons(buttons):
  278. """
  279. :param label:
  280. :param actions: action 列表
  281. action 可用形式:
  282. * dict: ``{label:选项标签, value:选项值, [type: 按钮类型], [disabled:是否禁止选择]}``
  283. * tuple or list: ``(label, value, [type], [disabled])``
  284. * 单值: 此时label和value使用相同的值
  285. :return: 规格化后的 buttons
  286. """
  287. act_res = []
  288. for act in buttons:
  289. if isinstance(act, Mapping):
  290. assert 'label' in act, 'actions item must have label key'
  291. assert 'value' in act or act.get('type', 'submit') != 'submit' or act.get('disabled'), \
  292. 'actions item must have value key for submit type'
  293. elif isinstance(act, (list, tuple)):
  294. assert len(act) in (2, 3, 4), 'actions item format error'
  295. act = dict(zip(('label', 'value', 'type', 'disabled'), act))
  296. else:
  297. act = dict(value=act, label=act)
  298. act.setdefault('type', 'submit')
  299. assert act['type'] in ('submit', 'reset', 'cancel', 'callback'), \
  300. "submit type muse be 'submit'/'reset'/'cancel'/'callback', not %r" % act['type']
  301. act_res.append(act)
  302. return act_res
  303. def actions(label='', buttons=None, name=None, help_text=None):
  304. r"""按钮选项。
  305. 在表单上显示为一组按钮,用户点击按钮后依据按钮类型的不同有不同的表现。
  306. 当 ``actions()`` 作为 `input_group()` 的 ``inputs`` 中最后一个输入项,并且输入项中含有 ``type=submit`` 的按钮时,表单默认的提交按钮会被当前 ``actions()`` 替换
  307. :param list buttons: 选项列表。列表项的可用形式有:
  308. * dict: ``{label:选项标签, value:选项值, [type: 按钮类型], [disabled:是否禁止选择]}`` .
  309. 若 ``type='reset'/'cancel'`` 或 ``disabled=True`` 可省略 ``value``
  310. * tuple or list: ``(label, value, [type], [disabled])``
  311. * 单值: 此时label和value使用相同的值
  312. 其中 ``type`` 可选值为:
  313. * ``'submit'`` : 点击按钮后,将整个表单提交,最终表单中本项的值为被点击按钮的 ``value`` 值。 ``'submit'`` 为 ``type`` 的默认值
  314. * ``'callback'`` : 点击按钮后,将运行一个回调,回调函数通过 ``value`` 字段指定,可以在回调函数内设置表单中本项的值。具体用法见下文。
  315. * ``'cancel'`` : 取消输入。点击按钮后, ``actions()`` 将直接返回 ``None``
  316. * ``'reset'`` : 点击按钮后,将整个表单重置,输入项将变为初始状态。
  317. 注意:点击 ``type=reset`` 的按钮后,并不会提交表单, ``actions()`` 调用也不会返回
  318. :param - label, name, help_text: 与 `input` 输入函数的同名参数含义一致
  319. :return: 若用户点击点击 ``type=submit`` 按钮进行表单提交,返回用户点击的按钮的值;若用户点击点击 ``type=callback`` 按钮,返回值通过回调函数设置;
  320. 若用户点击 ``type=cancel`` 按钮或通过其它方式提交表单,则返回 ``None``
  321. **type=callback的用法**
  322. 回调函数需要接收一个 ``set_value`` 位置参数, ``set_value`` 是一个可调用对象,调用 ``set_value`` 将会设置 actions 输入项的值,
  323. 调用签名为 ``set_value(value:any, label:str)`` ,其中
  324. * ``value`` 参数为最终 actions 输入项的返回值,可以为任意Python对象,并不会传递给用户浏览器
  325. * ``label`` 参数可选,用于显示在用户表单上, ``label`` 默认为 ``value`` 的字符串表示
  326. 示例代码见下方"通过其他操作设置项值"使用场景。
  327. Note: 当使用 :ref:`基于协程的会话实现 <coroutine_based_session>` 时,回调函数可以使用协程函数.
  328. **actions使用场景**
  329. .. _custom_form_ctrl_btn:
  330. * 实现简单的选择操作:
  331. .. exportable-codeblock::
  332. :name: actions-select
  333. :summary: 使用`actions()`实现简单的选择操作
  334. confirm = actions('确认删除文件?', ['确认', '取消'], help_text='文件删除后不可恢复')
  335. if confirm=='确认':
  336. ... # ..doc-only
  337. put_text('已确认') # ..demo-only
  338. 相比于其他输入项,使用 `actions()` 用户只需要点击一次就可完成提交。
  339. * 替换默认的提交按钮:
  340. .. exportable-codeblock::
  341. :name: actions-submit
  342. :summary: 使用`actions()`替换默认的提交按钮
  343. import json # ..demo-only
  344. # ..demo-only
  345. info = input_group('Add user', [
  346. input('username', type=TEXT, name='username', required=True),
  347. input('password', type=PASSWORD, name='password', required=True),
  348. actions('actions', [
  349. {'label': '保存', 'value': 'save'},
  350. {'label': '保存并添加下一个', 'value': 'save_and_continue'},
  351. {'label': '重置', 'type': 'reset'},
  352. {'label': '取消', 'type': 'cancel'},
  353. ], name='action', help_text='actions'),
  354. ])
  355. put_code('info = ' + json.dumps(info, indent=4))
  356. if info is not None:
  357. save_user(info['username'], info['password']) # ..doc-only
  358. put_text(info['username'], info['password']) # ..demo-only
  359. if info['action'] == 'save_and_continue': # 选择了"保存并添加下一个"
  360. add_next() # ..doc-only
  361. ... # ..demo-only
  362. * 通过其他操作设置项值:
  363. .. exportable-codeblock::
  364. :name: actions-callback
  365. :summary: `actions()`callback的使用
  366. def get_name(set_val):
  367. popup('Set name', [
  368. put_buttons(['Set result'], onclick=[lambda: set_val('Wang Weimin')])
  369. ])
  370. res = input_group('', [
  371. actions('Name', [
  372. dict(label='Set name', value=get_name, type='callback'),
  373. ], name='name'),
  374. ])
  375. put_text(res['name'])
  376. """
  377. assert buttons is not None, ValueError('Required `buttons` parameter in actions()')
  378. item_spec, valid_func = _parse_args(locals())
  379. item_spec['type'] = 'actions'
  380. item_spec['buttons'] = _parse_action_buttons(buttons)
  381. def preprocess_func(data, value_setter=None):
  382. if value_setter is None:
  383. return data
  384. return data or value_setter.val
  385. value_setter = None
  386. callback_btns = [btn for btn in item_spec['buttons'] if btn['type'] == 'callback']
  387. if callback_btns:
  388. value_setter = Setter()
  389. task_id = get_current_task_id()
  390. def _set_value(value, label=None):
  391. if label is None:
  392. label = str(value)
  393. value_setter.val = value
  394. msg = dict(command='update_input', task_id=task_id, spec={
  395. 'target_name': item_spec.get('name', 'data'),
  396. 'attributes': {'action_result': label}
  397. })
  398. get_current_session().send_task_command(msg)
  399. for btn in callback_btns:
  400. callback = btn['value']
  401. btn['value'] = output_register_callback(lambda _: callback(_set_value))
  402. return single_input(item_spec, valid_func, partial(preprocess_func, value_setter=value_setter))
  403. def _parse_file_size(size):
  404. if isinstance(size, (int, float)):
  405. return int(size)
  406. assert isinstance(size, str), '`size` must be int/float/str, got %s' % type(size)
  407. for idx, i in enumerate(['k', 'm', 'g'], 1):
  408. if i in size:
  409. s = size.lower().replace(i, '')
  410. base = 2 ** (idx * 10)
  411. return int(float(s) * base)
  412. return int(size)
  413. def file_upload(label='', accept=None, name=None, placeholder='Choose file', multiple=False, max_size=0,
  414. max_total_size=0, required=None, help_text=None, **other_html_attrs):
  415. r"""文件上传。
  416. :param accept: 单值或列表, 表示可接受的文件类型。单值或列表项支持的形式有:
  417. * 以 ``.`` 字符开始的文件扩展名(例如:``.jpg, .png, .doc``)。
  418. 注意:截止本文档编写之时,微信内置浏览器还不支持这种语法
  419. * 一个有效的 MIME 类型。
  420. 例如: ``application/pdf`` 、 ``audio/*`` 表示音频文件、``video/*`` 表示视频文件、``image/*`` 表示图片文件
  421. 参考 https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types
  422. :type accept: str or list
  423. :param str placeholder: 未上传文件时,文件上传框内显示的文本
  424. :param bool multiple: 是否允许多文件上传
  425. :param int/str max_size: 单个文件的最大大小,超过限制将会禁止上传。默认为0,表示不限制上传文件的大小。
  426. 可以为数字表示的字节数,或以 `K` / `M` / `G` 结尾的表示的字符串(分别表示 千字节、兆字节、吉字节,大小写不敏感)。例如:
  427. ``max_size=500`` , ``max_size='40K'`` , ``max_size='3M'``
  428. :param int/str max_total_size: 所有文件的最大大小,超过限制将会禁止上传。仅在 ``multiple=True`` 时可用,默认不限制上传文件的大小。 格式同 ``max_size`` 参数
  429. :param bool required: 是否必须要上传文件。默认为 `False`
  430. :param - label, name, help_text, other_html_attrs: 与 `input` 输入函数的同名参数含义一致
  431. :return: ``multiple=False`` 时(默认),返回dict: ``{'filename': 文件名, 'content':文件二进制数据(bytes object), mime_type: 文件的MIME类型, last_modified: 文件上次修改时间(时间戳) }`` ;
  432. 若用户没有上传文件,返回 ``None`` 。
  433. ``multiple=True`` 时,返回列表,列表项格式同上文 ``multiple=False`` 时的返回值;若用户没有上传文件,返回空列表。
  434. .. note::
  435. 若上传大文件请留意Web框架的文件上传大小限制设置。在使用 :func:`start_server <pywebio.platform.start_server>` 启动PyWebIO应用时,
  436. 可通过 `websocket_max_message_size` 参数设置允许上传的最大文件大小
  437. """
  438. item_spec, valid_func = _parse_args(locals())
  439. item_spec['type'] = 'file'
  440. item_spec['max_size'] = _parse_file_size(max_size)
  441. item_spec['max_total_size'] = _parse_file_size(max_total_size)
  442. def read_file(data): # data: None or [{'filename':, 'dataurl', 'mime_type', 'last_modified'}, ...]
  443. for d in data:
  444. header, encoded = d['dataurl'].split(",", 1)
  445. d['content'] = b64decode(encoded)
  446. if not multiple:
  447. return data[0] if len(data) >= 1 else None
  448. return data
  449. return single_input(item_spec, valid_func, read_file)
  450. def input_group(label='', inputs=None, valid_func=None, cancelable=False):
  451. r"""输入组。向页面上展示一组输入
  452. :param str label: 输入组标签
  453. :param list inputs: 输入项列表。列表的内容为对单项输入函数的调用,并在单项输入函数中传入 ``name`` 参数。
  454. :param Callable valid_func: 输入组校验函数。
  455. 函数签名:``callback(data) -> (name, error_msg)``
  456. ``valid_func`` 接收整个表单的值为参数,当校验表单值有效时,返回 ``None`` ,当某项输入值无效时,返回出错输入项的 ``name`` 值和错误提示. 比如:
  457. .. exportable-codeblock::
  458. :name: input_group-valid_func
  459. :summary: `input_group()`输入组校验
  460. def check_form(data):
  461. if len(data['name']) > 6:
  462. return ('name', '名字太长!')
  463. if data['age'] <= 0:
  464. return ('age', '年龄不能为负数!')
  465. data = input_group("Basic info",[
  466. input('Input your name', name='name'),
  467. input('Repeat your age', name='age', type=NUMBER)
  468. ], valid_func=check_form)
  469. put_text(data['name'], data['age'])
  470. :param bool cancelable: 表单是否可以取消。若 ``cancelable=True`` 则会在表单底部显示一个"取消"按钮。
  471. 注意:若 ``inputs`` 中最后一项输入为 `actions()` ,则忽略 ``cancelable``
  472. :return: 若用户取消表单,返回 ``None`` ,否则返回一个 ``dict`` , 其键为输入项的 ``name`` 值,字典值为输入项的值
  473. """
  474. assert inputs is not None, ValueError('Required `inputs` parameter in input_group()')
  475. spec_inputs = []
  476. preprocess_funcs = {}
  477. item_valid_funcs = {}
  478. for single_input_return in inputs:
  479. try:
  480. single_input_return.send(None) # 协程模式下,带有name参数的单项输入函数通过send(None)来获取协程参数
  481. except StopIteration as e:
  482. input_kwargs = e.args[0]
  483. except AttributeError:
  484. input_kwargs = single_input_return
  485. else:
  486. raise RuntimeError("Can't get kwargs from single input")
  487. assert all(k in input_kwargs for k in ('item_spec', 'preprocess_func', 'valid_func')), RuntimeError(
  488. "`inputs` value error in `input_group`. Did you forget to add `name` parameter in input function?")
  489. input_name = input_kwargs['item_spec']['name']
  490. if input_name in preprocess_funcs:
  491. raise ValueError("Can't use same `name`:%s in different input in input group!!" % input_name)
  492. preprocess_funcs[input_name] = input_kwargs['preprocess_func']
  493. item_valid_funcs[input_name] = input_kwargs['valid_func']
  494. spec_inputs.append(input_kwargs['item_spec'])
  495. if all('auto_focus' not in i for i in spec_inputs): # 每一个输入项都没有设置auto_focus参数
  496. for i in spec_inputs:
  497. text_inputs = {TEXT, NUMBER, PASSWORD, SELECT, URL} # todo update
  498. if i.get('type') in text_inputs:
  499. i['auto_focus'] = True
  500. break
  501. spec = dict(label=label, inputs=spec_inputs, cancelable=cancelable)
  502. return input_control(spec, preprocess_funcs=preprocess_funcs, item_valid_funcs=item_valid_funcs,
  503. form_valid_funcs=valid_func)