"""
This module provides functions to get all kinds of input of user from the browser
There are two ways to use the input functions, one is to call the input function alone to get a single input::
name = input("What's your name")
print("Your name is %s" % name)
The other is to use `input_group` to get multiple inputs at once::
info = input_group("User info",[
input('Input your name', name='name'),
input('Input your age', name='age', type=NUMBER)
])
print(info['name'], info['age'])
When use `input_group`, you needs to provide the ``name`` parameter in each input function to identify the input items in the result.
.. note::
PyWebIO determines whether the input function is in `input_group` or is called alone according to whether the
``name`` parameter is passed. So when calling an input function alone, **do not** set the ``name`` parameter;
when calling the input function in `input_group`, you **must** provide the ``name`` parameter.
By default, the user can submit empty input value. If the user must provide a non-empty input value, you need to
pass ``required=True`` to the input function (some input functions do not support the ``required`` parameter)
The input functions in this module is blocking, and the input form will be destroyed after successful submission.
If you want the form to always be displayed on the page and receive input continuously,
you can consider the :doc:`pin <./pin>` module.
Functions list
-----------------
.. list-table::
* - Function name
- Description
* - `input <pywebio.input.input>`
- Text input
* - `textarea <pywebio.input.textarea>`
- Multi-line text input
* - `select <pywebio.input.select>`
- Drop-down selection
* - `checkbox <pywebio.input.checkbox>`
- Checkbox
* - `radio <pywebio.input.radio>`
- Radio
* - `slider <pywebio.input.slider>`
- Slider
* - `actions <pywebio.input.actions>`
- Actions selection
* - `file_upload <pywebio.input.file_upload>`
- File uploading
* - `input_group <pywebio.input.input_group>`
- Input group
* - `input_update <pywebio.input.input_update>`
- Update input item
Functions doc
--------------
"""
import os.path
import logging
from collections.abc import Mapping
import copy
from .io_ctrl import single_input, input_control, output_register_callback, send_msg, single_input_kwargs
from .session import get_current_session, get_current_task_id
from .utils import Setter, is_html_safe_value, parse_file_size
from .platform import page as platform_setting
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', 'slider', 'input_group', 'input_update']
def _parse_args(kwargs, excludes=()):
"""parse the raw parameters that pass to input functions
- excludes: the parameters that don't appear in returned spec
- remove the parameters whose value is 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)
if kwargs.get('validate'):
kwargs['onblur'] = True
valid_func = kwargs.pop('validate', lambda _: None)
if kwargs.get('onchange'):
onchange_func = kwargs['onchange']
kwargs['onchange'] = True
else:
onchange_func = lambda _: None
return kwargs, valid_func, onchange_func
[docs]def textarea(label='', *, rows=6, code=None, maxlength=None, minlength=None, validate=None, name=None, value=None,
onchange=None, placeholder=None, required=None, readonly=None, help_text=None, **other_html_attrs):
r"""Text input area (multi-line text input)
:param int rows: The number of visible text lines for the input area. Scroll bar will be used when content exceeds.
:param int maxlength: The maximum number of characters (UTF-16 code units) that the user can enter.
If this value isn't specified, the user can enter an unlimited number of characters.
:param int minlength: The minimum number of characters (UTF-16 code units) required that the user should enter.
:param dict/bool code: Enable a code style editor by providing the `Codemirror <https://codemirror.net/>`_ options:
.. exportable-codeblock::
:name: textarea-code
:summary: `textarea()` code editor style
res = textarea('Text area', code={
'mode': "python",
'theme': 'darcula'
})
put_code(res, language='python') # ..demo-only
You can simply use ``code={}`` or ``code=True`` to enable code style editor.
You can use ``Esc`` or ``F11`` to toggle fullscreen of code style textarea.
Some commonly used Codemirror options are listed :ref:`here <codemirror_options>`.
:param - label, validate, name, value, onchange, placeholder, required, readonly, help_text, other_html_attrs:
Those arguments have the same meaning as for `input()`
:return: The string value that user input.
"""
item_spec, valid_func, onchange_func = _parse_args(locals())
item_spec['type'] = TEXTAREA
return single_input(item_spec, valid_func, lambda d: d, onchange_func)
def _parse_select_options(options):
# Convert the `options` parameter in the `select`, `checkbox`, and `radio` functions to a unified format
# Available forms of option:
# {value:, label:, [selected:,] [disabled:]}
# (value, label, [selected,] [disabled])
# value (label same as value)
opts_res = []
for opt in options:
opt = copy.deepcopy(opt)
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)
opts_res.append(opt)
return opts_res
def _set_options_selected(options, value):
"""set `selected` attribute for `options`"""
if not isinstance(value, (list, tuple)):
value = [value]
for opt in options:
if opt['value'] in value:
opt['selected'] = True
return options
[docs]def select(label='', options=None, *, multiple=None, validate=None, name=None, value=None, onchange=None, required=None,
help_text=None, **other_html_attrs):
r"""Drop-down selection
By default, only one option can be selected at a time, you can set ``multiple`` parameter to enable multiple selection.
:param list options: list of options. The available formats of the list items are:
* dict::
{
"label":(str) option label,
"value":(object) option value,
"selected":(bool, optional) whether the option is initially selected,
"disabled":(bool, optional) whether the option is initially disabled
}
* tuple or list: ``(label, value, [selected,] [disabled])``
* single value: label and value of option use the same value
Attention:
1. The ``value`` of option can be any JSON serializable object
2. If the ``multiple`` is not ``True``, the list of options can only have one ``selected`` item at most.
:param bool multiple: whether multiple options can be selected
:param value: The value of the initial selected item. When ``multiple=True``, ``value`` must be a list.
You can also set the initial selected option by setting the ``selected`` field in the ``options`` list item.
:type value: list or str
:param bool required: Whether to select at least one item, only available when ``multiple=True``
:param - label, validate, name, onchange, help_text, other_html_attrs: Those arguments have the same meaning as for `input()`
:return: If ``multiple=True``, return a list of the values in the ``options`` selected by the user;
otherwise, return the single value selected by the user.
"""
assert options is not None, 'Required `options` parameter in select()'
item_spec, valid_func, onchange_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=valid_func, preprocess_func=lambda d: d, onchange_func=onchange_func)
[docs]def checkbox(label='', options=None, *, inline=None, validate=None, name=None, value=None, onchange=None,
help_text=None, **other_html_attrs):
r"""A group of check box that allowing single values to be selected/deselected.
:param list options: List of options. The format is the same as the ``options`` parameter of the `select()` function
:param bool inline: Whether to display the options on one line. Default is ``False``
:param list value: The value list of the initial selected items.
You can also set the initial selected option by setting the ``selected`` field in the ``options`` list item.
:param - label, validate, name, onchange, help_text, other_html_attrs: Those arguments have the same meaning as for `input()`
:return: A list of the values in the ``options`` selected by the user
"""
assert options is not None, 'Required `options` parameter in checkbox()'
item_spec, valid_func, onchange_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'] = CHECKBOX
return single_input(item_spec, valid_func, lambda d: d, onchange_func)
[docs]def radio(label='', options=None, *, inline=None, validate=None, name=None, value=None, onchange=None, required=None,
help_text=None, **other_html_attrs):
r"""A group of radio button. Only a single button can be selected.
:param list options: List of options. The format is the same as the ``options`` parameter of the `select()` function
:param bool inline: Whether to display the options on one line. Default is ``False``
:param str value: The value of the initial selected items.
You can also set the initial selected option by setting the ``selected`` field in the ``options`` list item.
:param bool required: whether to must select one option. (the user can select nothing option by default)
:param - label, validate, name, onchange, help_text, other_html_attrs: Those arguments have the same meaning as for `input()`
:return: The value of the option selected by the user, if the user does not select any value, return ``None``
"""
assert options is not None, 'Required `options` parameter in radio()'
item_spec, valid_func, onchange_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)
# From https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/required
# In the case of a same named group of radio buttons, if a single radio button in the group has the required attribute,
# a radio button in that group must be checked, although it doesn't have to be the one with the attribute is applied
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, onchange_func)
def _parse_action_buttons(buttons):
"""
:param label:
:param actions: action list
action available format:
* dict: ``{label:button label, value:button value, [type: button type], [disabled:is disabled?]}``
* tuple or list: ``(label, value, [type], [disabled])``
* single value: label and value of button share the same value
:return: dict format
"""
act_res = []
for act in buttons:
act = copy.deepcopy(act)
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
[docs]def actions(label='', buttons=None, name=None, help_text=None):
r"""Actions selection
It is displayed as a group of buttons on the page. After the user clicks the button of it,
it will behave differently depending on the type of the button.
:param list buttons: list of buttons. The available formats of the list items are:
* dict::
{
"label":(str) button label,
"value":(object) button value,
"type":(str, optional) button type,
"disabled":(bool, optional) whether the button is disabled,
"color":(str, optional) button color
}
When ``type='reset'/'cancel'`` or ``disabled=True``, ``value`` can be omitted
* tuple or list: ``(label, value, [type], [disabled])``
* single value: label and value of button use the same value
The ``value`` of button can be any JSON serializable object.
``type`` can be:
* ``'submit'`` : After clicking the button, the entire form is submitted immediately,
and the value of this input item in the final form is the ``value`` of the button that was clicked.
``'submit'`` is the default value of ``type``
* ``'cancel'`` : Cancel form. After clicking the button, the entire form will be submitted immediately,
and the form value will return ``None``
* ``'reset'`` : Reset form. After clicking the button, the entire form will be reset,
and the input items will become the initial state.
Note: After clicking the ``type=reset`` button, the form will not be submitted,
and the ``actions()`` call will not return
The ``color`` of button can be one of: `primary`, `secondary`, `success`, `danger`, `warning`, `info`, `light`,
`dark`.
:param - label, name, help_text: Those arguments have the same meaning as for `input()`
:return: If the user clicks the ``type=submit`` button to submit the form,
return the value of the button clicked by the user.
If the user clicks the ``type=cancel`` button or submits the form by other means, ``None`` is returned.
When ``actions()`` is used as the last input item in `input_group()` and contains a button with ``type='submit'``,
the default submit button of the `input_group()` form will be replace with the current ``actions()``
**usage scenes of actions() **
.. _custom_form_ctrl_btn:
* Perform simple selection operations:
.. exportable-codeblock::
:name: actions-select
:summary: Use `actions()` to perform simple selection
confirm = actions('Confirm to delete file?', ['confirm', 'cancel'],
help_text='Unrecoverable after file deletion')
if confirm=='confirm': # ..doc-only
... # ..doc-only
put_markdown('You clicked the `%s` button' % confirm) # ..demo-only
Compared with other input items, when using `actions()`, the user only needs to click once to complete the submission.
* Replace the default submit button:
.. exportable-codeblock::
:name: actions-submit
:summary: Use `actions()` to replace the default submit button
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': 'Save', 'value': 'save'},
{'label': 'Save and add next', 'value': 'save_and_continue'},
{'label': 'Reset', 'type': 'reset', 'color': 'warning'},
{'label': 'Cancel', 'type': 'cancel', 'color': 'danger'},
], 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('Save and add next...') # ..demo-only
"""
assert buttons is not None, 'Required `buttons` parameter in actions()'
item_spec, valid_func, onchange_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, onchange_func)
[docs]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"""File uploading
:param accept: Single value or list, indicating acceptable file types. The available formats of file types are:
* A valid case-insensitive filename extension, starting with a period (".") character. For example: ``.jpg``, ``.pdf``, or ``.doc``.
* A valid MIME type string, with no extensions.
For examples: ``application/pdf``, ``audio/*``, ``video/*``, ``image/*``.
For more information, please visit: https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types
:type accept: str or list
:param str placeholder: A hint to the user of what to be uploaded. It will appear in the input field when there is no file selected.
:param bool multiple: Whether to allow upload multiple files. Default is ``False``.
:param int/str max_size: The maximum size of a single file, exceeding the limit will prohibit uploading.
The default is 0, which means there is no limit to the size.
``max_size`` can be a integer indicating the number of bytes, or a case-insensitive string ending with `K` / `M` / `G`
(representing kilobytes, megabytes, and gigabytes, respectively).
E.g: ``max_size=500``, ``max_size='40K'``, ``max_size='3M'``
:param int/str max_total_size: The maximum size of all files. Only available when ``multiple=True``.
The default is 0, which means there is no limit to the size. The format is the same as the ``max_size`` parameter
:param bool required: Indicates whether the user must specify a file for the input. Default is ``False``.
:param - label, name, help_text, other_html_attrs: Those arguments have the same meaning as for `input()`
:return: When ``multiple=False``, a dict is returned::
{
'filename': file name,
'content':content of the file (in bytes),
'mime_type': MIME type of the file,
'last_modified': Last modified time (timestamp) of the file
}
If there is no file uploaded, return ``None``.
When ``multiple=True``, a list is returned. The format of the list item is the same as the return value when ``multiple=False`` above.
If the user does not upload a file, an empty list is returned.
.. note::
If uploading large files, please pay attention to the file upload size limit setting of the web framework.
When using :func:`start_server() <pywebio.platform.tornado.start_server>` or
:func:`path_deploy() <pywebio.platform.path_deploy>` to start the PyWebIO application,
the maximum file size to be uploaded allowed by the web framework can be set through the ``max_payload_size`` parameter.
.. exportable-codeblock::
:name: file_upload_example
:summary: `file_upload()` example
# Upload a file and save to server # ..doc-only
f = input.file_upload("Upload a file") # ..doc-only
open('asset/'+f['filename'], 'wb').write(f['content']) # ..doc-only
imgs = file_upload("Select some pictures:", accept="image/*", multiple=True)
for img in imgs:
put_image(img['content'])
"""
item_spec, valid_func, onchange_func = _parse_args(locals())
item_spec['type'] = 'file'
item_spec['max_size'] = parse_file_size(max_size) or platform_setting.MAX_PAYLOAD_SIZE
item_spec['max_total_size'] = parse_file_size(max_total_size) or platform_setting.MAX_PAYLOAD_SIZE
if platform_setting.MAX_PAYLOAD_SIZE:
if item_spec['max_size'] > platform_setting.MAX_PAYLOAD_SIZE or \
item_spec['max_total_size'] > platform_setting.MAX_PAYLOAD_SIZE:
raise ValueError('The `max_size` and `max_total_size` value can not exceed the backend payload size limit. '
'Please increase the `max_total_size` of `start_server()`/`path_deploy()`')
def read_file(data):
for file in data:
# Security fix: to avoid interpreting file name as path
file['filename'] = os.path.basename(file['filename'])
if not multiple:
return data[0] if len(data) >= 1 else None
return data
return single_input(item_spec, valid_func, read_file, onchange_func)
[docs]def slider(label='', *, name=None, value=0, min_value=0, max_value=100, step=1, validate=None, onchange=None,
required=None, help_text=None, **other_html_attrs):
r"""Range input.
:param int/float value: The initial value of the slider.
:param int/float min_value: The minimum permitted value.
:param int/float max_value: The maximum permitted value.
:param int step: The stepping interval.
Only available when ``value``, ``min_value`` and ``max_value`` are all integer.
:param - label, name, validate, onchange, required, help_text, other_html_attrs: Those arguments have the same meaning as for `input()`
:return int/float: If one of ``value``, ``min_value`` and ``max_value`` is float,
the return value is a float, otherwise an int is returned.
"""
item_spec, valid_func, onchange_func = _parse_args(locals())
item_spec['type'] = 'slider'
item_spec['float'] = any(isinstance(i, float) for i in (value, min_value, max_value))
if item_spec['float']:
item_spec['step'] = 'any'
return single_input(item_spec, valid_func, lambda d: d, onchange_func)
def parse_input_update_spec(spec):
for key in spec:
assert key not in {'action', 'buttons', 'code', 'inline', 'max_size', 'max_total_size', 'multiple', 'name',
'onchange', 'type', 'validate'}, '%r can not be updated' % key
attributes = dict((k, v) for k, v in spec.items() if v is not None)
if 'options' in spec:
attributes['options'] = _parse_select_options(spec['options'])
return attributes