"""从浏览器接收用户输入
本模块提供了一系列函数来从浏览器接收用户不同的形式的输入
输入函数大致分为两类,一类是单项输入::
name = input("What's your name")
print("Your name is %s" % name)
另一类是使用 `input_group` 的输入组::
info = input_group("User info",[
input('Input your name', name='name'),
input('Input your age', name='age', type=NUMBER)
])
print(info['name'], info['age'])
输入组中需要在每一项输入函数中提供 ``name`` 参数来用于在结果中标识不同输入项.
.. note::
PyWebIO 根据是否在输入函数中传入 ``name`` 参数来判断输入函数是在 `input_group` 中还是被单独调用。
所以当你想要单独调用一个输入函数时,请不要设置 ``name`` 参数;而在 `input_group` 中调用输入函数时,**务必提供** ``name`` 参数
输入默认可以忽略,如果需要用户必须提供值,则需要在输入函数中传入 ``required=True`` (部分输入函数不支持 ``required`` 参数)
函数清单
------------
.. list-table::
* - 函数
- 简介
* - `input <pywebio.input.input>`
- 文本输入
* - `textarea <pywebio.input.textarea>`
- 多行文本输入
* - `select <pywebio.input.select>`
- 下拉选择框
* - `checkbox <pywebio.input.checkbox>`
- 勾选选项
* - `radio <pywebio.input.radio>`
- 单选选项
* - `actions <pywebio.input.actions>`
- 按钮选项
* - `file_upload <pywebio.input.file_upload>`
- 文件上传
* - `input_group <pywebio.input.input_group>`
- 输入组
函数文档
------------
"""
import logging
from base64 import b64decode
from collections.abc import Mapping
from functools import partial
from .io_ctrl import single_input, input_control, output_register_callback
from .session import get_current_session, get_current_task_id
from .utils import Setter, is_html_safe_value
logger = logging.getLogger(__name__)
TEXT = 'text'
NUMBER = "number"
FLOAT = "float"
PASSWORD = "password"
URL = "url"
DATE = "date"
TIME = "time"
CHECKBOX = 'checkbox'
RADIO = 'radio'
SELECT = 'select'
TEXTAREA = 'textarea'
__all__ = ['TEXT', 'NUMBER', 'FLOAT', 'PASSWORD', 'URL', 'DATE', 'TIME', 'input', 'textarea', 'select',
'checkbox', 'radio', 'actions', 'file_upload', 'input_group']
def _parse_args(kwargs, excludes=()):
"""处理传给各类输入函数的原始参数
- excludes: 排除的参数
- 对为None的参数忽略处理
:return:(spec参数,valid_func)
"""
kwargs = {k: v for k, v in kwargs.items() if v is not None and k not in excludes}
assert is_html_safe_value(kwargs.get('name', '')), '`name` can only contains a-z、A-Z、0-9、_、-'
kwargs.update(kwargs.get('other_html_attrs', {}))
kwargs.pop('other_html_attrs', None)
valid_func = kwargs.pop('validate', lambda _: None)
return kwargs, valid_func
[文档]def textarea(label='', *, rows=6, code=None, maxlength=None, minlength=None, validate=None, name=None, value=None,
placeholder=None, required=None, readonly=None, help_text=None, **other_html_attrs):
r"""文本输入域(多行文本输入)
:param int rows: 输入文本的行数(显示的高度)。输入的文本超出设定值时会显示滚动条
:param int maxlength: 允许用户输入的最大字符长度 (Unicode) 。未指定表示无限长度
:param int minlength: 允许用户输入的最小字符长度(Unicode)
:param dict code: 通过提供 `Codemirror <https://codemirror.net/>`_ 参数让文本输入域具有代码编辑器样式:
.. exportable-codeblock::
:name: textarea-code
:summary: `textarea()`代码编辑
res = textarea('Text area', code={
'mode': "python",
'theme': 'darcula'
})
put_code(res, language='python') # ..demo-only
更多配置可以参考 https://codemirror.net/doc/manual.html#config
:param - label, validate, name, value, placeholder, required, readonly, help_text, other_html_attrs: 与 `input` 输入函数的同名参数含义一致
:return: 用户输入的文本
"""
item_spec, valid_func = _parse_args(locals())
item_spec['type'] = TEXTAREA
return single_input(item_spec, valid_func, lambda d: d)
def _parse_select_options(options):
# 转换 select、checkbox、radio函数中的 options 参数为统一的格式
# option 可用形式:
# {value:, label:, [selected:,] [disabled:]}
# (value, label, [selected,] [disabled])
# value 单值,label等于value
opts_res = []
for opt in options:
if isinstance(opt, Mapping):
assert 'value' in opt and 'label' in opt, 'options item must have value and label key'
elif isinstance(opt, (list, tuple)):
assert len(opt) > 1 and len(opt) <= 4, 'options item format error'
opt = dict(zip(('label', 'value', 'selected', 'disabled'), opt))
else:
opt = dict(value=opt, label=opt)
opt['value'] = opt['value']
opts_res.append(opt)
return opts_res
def _set_options_selected(options, value):
"""使用value为options的项设置selected"""
if not isinstance(value, (list, tuple)):
value = [value]
for opt in options:
if opt['value'] in value:
opt['selected'] = True
return options
[文档]def select(label='', options=None, *, multiple=None, validate=None, name=None, value=None, required=None,
help_text=None, **other_html_attrs):
r"""下拉选择框。
默认单选,设置 ``multiple`` 参数后,可以多选。但都至少要选择一个选项。
:param list options: 可选项列表。列表项的可用形式有:
* dict: ``{label:选项标签, value: 选项值, [selected:是否默认选中,] [disabled:是否禁止选中]}``
* tuple or list: ``(label, value, [selected,] [disabled])``
* 单值: 此时label和value使用相同的值
注意:
1. ``options`` 中的 ``value`` 可以为任意可Json序列化对象
2. 若 ``multiple`` 选项不为 ``True`` 则可选项列表最多仅能有一项的 ``selected`` 为 ``True``。
:param bool multiple: 是否可以多选. 默认单选
:param value: 下拉选择框初始选中项的值。当 ``multiple=True`` 时, ``value`` 需为list,否则为单个选项的值。
你也可以通过设置 ``options`` 列表项中的 ``selected`` 字段来设置默认选中选项。
最终选中项为 ``value`` 参数和 ``options`` 中设置的并集。
:type value: list or str
:param bool required: 是否至少选择一项
:param - label, validate, name, help_text, other_html_attrs: 与 `input` 输入函数的同名参数含义一致
:return: 如果 ``multiple=True`` 时,返回用户选中的 ``options`` 中的值的列表;不设置 ``multiple`` 时,返回用户选中的 ``options`` 中的值
"""
assert options is not None, 'Required `options` parameter in select()'
item_spec, valid_func = _parse_args(locals(), excludes=['value'])
item_spec['options'] = _parse_select_options(options)
if value is not None:
item_spec['options'] = _set_options_selected(item_spec['options'], value)
item_spec['type'] = SELECT
return single_input(item_spec, valid_func, lambda d: d)
[文档]def checkbox(label='', options=None, *, inline=None, validate=None, name=None, value=None, help_text=None,
**other_html_attrs):
r"""勾选选项。可以多选,也可以不选。
:param list options: 可选项列表。格式与 `select` 函数的 ``options`` 参数含义一致
:param bool inline: 是否将选项显示在一行上。默认每个选项单独占一行
:param list value: 勾选选项初始选中项。为选项值的列表。
你也可以通过设置 ``options`` 列表项中的 ``selected`` 字段来设置默认选中选项。
:param - label, validate, name, help_text, other_html_attrs: 与 `input` 输入函数的同名参数含义一致
:return: 用户选中的 options 中的值的列表。当用户没有勾选任何选项时,返回空列表
"""
assert options is not None, 'Required `options` parameter in checkbox()'
item_spec, valid_func = _parse_args(locals())
item_spec['options'] = _parse_select_options(options)
if value is not None:
del item_spec['value']
item_spec['options'] = _set_options_selected(item_spec['options'], value)
item_spec['type'] = CHECKBOX
return single_input(item_spec, valid_func, lambda d: d)
[文档]def radio(label='', options=None, *, inline=None, validate=None, name=None, value=None, required=None,
help_text=None, **other_html_attrs):
r"""单选选项
:param list options: 可选项列表。格式与 `select` 函数的 ``options`` 参数含义一致
:param bool inline: 是否将选项显示在一行上。默认每个选项单独占一行
:param str value: 单选选项初始选中项的值。
你也可以通过设置 ``options`` 列表项中的 ``selected`` 字段来设置默认选中选项。
:param bool required: 是否至少选择一项
:param - label, validate, name, help_text, other_html_attrs: 与 `input` 输入函数的同名参数含义一致
:return: 用户选中的选项的值
"""
assert options is not None, 'Required `options` parameter in radio()'
item_spec, valid_func = _parse_args(locals())
item_spec['options'] = _parse_select_options(options)
if value is not None:
del item_spec['value']
item_spec['options'] = _set_options_selected(item_spec['options'], value)
if required is not None:
del item_spec['required']
item_spec['options'][-1]['required'] = required
item_spec['type'] = RADIO
return single_input(item_spec, valid_func, lambda d: d)
def _parse_action_buttons(buttons):
"""
:param label:
:param actions: action 列表
action 可用形式:
* dict: ``{label:选项标签, value:选项值, [type: 按钮类型], [disabled:是否禁止选择]}``
* tuple or list: ``(label, value, [type], [disabled])``
* 单值: 此时label和value使用相同的值
:return: 规格化后的 buttons
"""
act_res = []
for act in buttons:
if isinstance(act, Mapping):
assert 'label' in act, 'actions item must have label key'
assert 'value' in act or act.get('type', 'submit') != 'submit' or act.get('disabled'), \
'actions item must have value key for submit type'
elif isinstance(act, (list, tuple)):
assert len(act) in (2, 3, 4), 'actions item format error'
act = dict(zip(('label', 'value', 'type', 'disabled'), act))
else:
act = dict(value=act, label=act)
act.setdefault('type', 'submit')
assert act['type'] in ('submit', 'reset', 'cancel'), \
"submit type muse be 'submit'/'reset'/'cancel', not %r" % act['type']
act_res.append(act)
return act_res
[文档]def actions(label='', buttons=None, name=None, help_text=None):
r"""按钮选项。
在表单上显示为一组按钮,用户点击按钮后依据按钮类型的不同有不同的表现。
当 ``actions()`` 作为 `input_group()` 的 ``inputs`` 中最后一个输入项,并且输入项中含有 ``type=submit`` 的按钮时,表单默认的提交按钮会被当前 ``actions()`` 替换
:param list buttons: 选项列表。列表项的可用形式有:
* dict: ``{label:选项标签, value:选项值, [type: 按钮类型], [disabled:是否禁止选择]}`` .
若 ``type='reset'/'cancel'`` 或 ``disabled=True`` 可省略 ``value``
* tuple or list: ``(label, value, [type], [disabled])``
* 单值: 此时label和value使用相同的值
其中, ``value`` 可以为任意可json序列化的对象。 ``type`` 可选值为:
* ``'submit'`` : 点击按钮后,立即将整个表单提交,最终表单中本项的值为被点击按钮的 ``value`` 值。 ``'submit'`` 为 ``type`` 的默认值
* ``'cancel'`` : 取消输入。点击按钮后,立即将整个表单提交,表单值返回 ``None``
* ``'reset'`` : 点击按钮后,将整个表单重置,输入项将变为初始状态。
注意:点击 ``type=reset`` 的按钮后,并不会提交表单, ``actions()`` 调用也不会返回
:param - label, name, help_text: 与 `input` 输入函数的同名参数含义一致
:return: 若用户点击点击 ``type=submit`` 按钮进行表单提交,返回用户点击的按钮的值;若用户点击点击 ``type=callback`` 按钮,返回值通过回调函数设置;
若用户点击 ``type=cancel`` 按钮或通过其它方式提交表单,则返回 ``None``
**actions使用场景**
.. _custom_form_ctrl_btn:
* 实现简单的选择操作:
.. exportable-codeblock::
:name: actions-select
:summary: 使用`actions()`实现简单的选择操作
confirm = actions('确认删除文件?', ['确认', '取消'], help_text='文件删除后不可恢复')
if confirm=='确认': # ..doc-only
... # ..doc-only
put_markdown('点击了`%s`按钮' % confirm) # ..demo-only
相比于其他输入项,使用 `actions()` 用户只需要点击一次就可完成提交。
* 替换默认的提交按钮:
.. exportable-codeblock::
:name: actions-submit
:summary: 使用`actions()`替换默认的提交按钮
import json # ..demo-only
# ..demo-only
info = input_group('Add user', [
input('username', type=TEXT, name='username', required=True),
input('password', type=PASSWORD, name='password', required=True),
actions('actions', [
{'label': '保存', 'value': 'save'},
{'label': '保存并添加下一个', 'value': 'save_and_continue'},
{'label': '重置', 'type': 'reset'},
{'label': '取消', 'type': 'cancel'},
], name='action', help_text='actions'),
])
put_code('info = ' + json.dumps(info, indent=4))
if info is not None:
save_user(info['username'], info['password']) # ..doc-only
if info['action'] == 'save_and_continue': # 选择了"保存并添加下一个"
add_next() # ..doc-only
put_text('选择了"保存并添加下一个"') # ..demo-only
"""
assert buttons is not None, 'Required `buttons` parameter in actions()'
item_spec, valid_func = _parse_args(locals())
item_spec['type'] = 'actions'
item_spec['buttons'] = _parse_action_buttons(buttons)
return single_input(item_spec, valid_func, lambda d: d)
def _parse_file_size(size):
if isinstance(size, (int, float)):
return int(size)
assert isinstance(size, str), '`size` must be int/float/str, got %s' % type(size)
for idx, i in enumerate(['k', 'm', 'g'], 1):
if i in size:
s = size.lower().replace(i, '')
base = 2 ** (idx * 10)
return int(float(s) * base)
return int(size)
[文档]def file_upload(label='', accept=None, name=None, placeholder='Choose file', multiple=False, max_size=0,
max_total_size=0, required=None, help_text=None, **other_html_attrs):
r"""文件上传。
:param accept: 单值或列表, 表示可接受的文件类型。单值或列表项支持的形式有:
* 以 ``.`` 字符开始的文件扩展名(例如:``.jpg, .png, .doc``)。
注意:截止本文档编写之时,微信内置浏览器还不支持这种语法
* 一个有效的 MIME 类型。
例如: ``application/pdf`` 、 ``audio/*`` 表示音频文件、``video/*`` 表示视频文件、``image/*`` 表示图片文件
参考 https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types
:type accept: str or list
:param str placeholder: 未上传文件时,文件上传框内显示的文本
:param bool multiple: 是否允许多文件上传
:param int/str max_size: 单个文件的最大大小,超过限制将会禁止上传。默认为0,表示不限制上传文件的大小。
可以为数字表示的字节数,或以 `K` / `M` / `G` 结尾的表示的字符串(分别表示 千字节、兆字节、吉字节,大小写不敏感)。例如:
``max_size=500`` , ``max_size='40K'`` , ``max_size='3M'``
:param int/str max_total_size: 所有文件的最大大小,超过限制将会禁止上传。仅在 ``multiple=True`` 时可用,默认不限制上传文件的大小。 格式同 ``max_size`` 参数
:param bool required: 是否必须要上传文件。默认为 `False`
:param - label, name, help_text, other_html_attrs: 与 `input` 输入函数的同名参数含义一致
:return: ``multiple=False`` 时(默认),返回dict::
{
'filename': 文件名,
'content':文件二进制数据(bytes object),
'mime_type': 文件的MIME类型,
'last_modified': 文件上次修改时间(时间戳)
}
若用户没有上传文件,返回 ``None`` 。
``multiple=True`` 时,返回列表,列表项格式同上文 ``multiple=False`` 时的返回值;若用户没有上传文件,返回空列表。
.. note::
若上传大文件请留意Web框架的文件上传大小限制设置。在使用 :func:`start_server <pywebio.platform.start_server>` 启动PyWebIO应用时,
可通过 `websocket_max_message_size` 参数设置允许上传的最大文件大小
"""
item_spec, valid_func = _parse_args(locals())
item_spec['type'] = 'file'
item_spec['max_size'] = _parse_file_size(max_size)
item_spec['max_total_size'] = _parse_file_size(max_total_size)
def read_file(data): # data: None or [{'filename':, 'dataurl', 'mime_type', 'last_modified'}, ...]
for d in data:
try:
_, encoded = d['dataurl'].split(",", 1)
except ValueError:
encoded = ''
d['content'] = b64decode(encoded)
if not multiple:
return data[0] if len(data) >= 1 else None
return data
return single_input(item_spec, valid_func, read_file)