r""" This module provides functions to output all kinds of content to the user's browser, and supply flexible output control. .. _output_func_list: Functions list --------------- .. Use https://www.tablesgenerator.com/text_tables to generate/update below table | The following table shows the output-related functions provided by PyWebIO. | The functions marked with ``*`` indicate that they accept ``put_xxx`` calls as arguments. | The functions marked with ``†`` indicate that they can use as context manager. +--------------------+---------------------------+------------------------------------------------------------+ | | **Name** | **Description** | +--------------------+---------------------------+------------------------------------------------------------+ | Output Scope | `put_scope` | Create a new scope | | +---------------------------+------------------------------------------------------------+ | | `use_scope`:sup:`†` | Enter a scope | | +---------------------------+------------------------------------------------------------+ | | `get_scope` | Get the current scope name in the runtime scope stack | | +---------------------------+------------------------------------------------------------+ | | `clear` | Clear the content of scope | | +---------------------------+------------------------------------------------------------+ | | `remove` | Remove the scope | | +---------------------------+------------------------------------------------------------+ | | `scroll_to` | Scroll the page to the scope | +--------------------+---------------------------+------------------------------------------------------------+ | Content Outputting | `put_text` | Output plain text | | +---------------------------+------------------------------------------------------------+ | | `put_markdown` | Output Markdown | | +---------------------------+------------------------------------------------------------+ | | | `put_info`:sup:`*†` | Output Messages. | | | | `put_success`:sup:`*†` | | | | | `put_warning`:sup:`*†` | | | | | `put_error`:sup:`*†` | | | +---------------------------+------------------------------------------------------------+ | | `put_html` | Output html | | +---------------------------+------------------------------------------------------------+ | | `put_link` | Output link | | +---------------------------+------------------------------------------------------------+ | | `put_progressbar` | Output a progress bar | | +---------------------------+------------------------------------------------------------+ | | `put_loading`:sup:`†` | Output loading prompt | | +---------------------------+------------------------------------------------------------+ | | `put_code` | Output code block | | +---------------------------+------------------------------------------------------------+ | | `put_table`:sup:`*` | Output table | | +---------------------------+------------------------------------------------------------+ | | | `put_datatable` | Output and update data table | | | | `datatable_update` | | | | | `datatable_insert` | | | | | `datatable_remove` | | | +---------------------------+------------------------------------------------------------+ | | | `put_button` | Output button and bind click event | | | | `put_buttons` | | | +---------------------------+------------------------------------------------------------+ | | `put_image` | Output image | | +---------------------------+------------------------------------------------------------+ | | `put_file` | Output a link to download a file | | +---------------------------+------------------------------------------------------------+ | | `put_tabs`:sup:`*` | Output tabs | | +---------------------------+------------------------------------------------------------+ | | `put_collapse`:sup:`*†` | Output collapsible content | | +---------------------------+------------------------------------------------------------+ | | `put_scrollable`:sup:`*†` | | Output a fixed height content area, | | | | | scroll bar is displayed when the content | | | | | exceeds the limit | | +---------------------------+------------------------------------------------------------+ | | `put_widget`:sup:`*` | Output your own widget | +--------------------+---------------------------+------------------------------------------------------------+ | Other Interactions | `toast` | Show a notification message | | +---------------------------+------------------------------------------------------------+ | | `popup`:sup:`*†` | Show popup | | +---------------------------+------------------------------------------------------------+ | | `close_popup` | Close the current popup window. | +--------------------+---------------------------+------------------------------------------------------------+ | Layout and Style | `put_row`:sup:`*†` | Use row layout to output content | | +---------------------------+------------------------------------------------------------+ | | `put_column`:sup:`*†` | Use column layout to output content | | +---------------------------+------------------------------------------------------------+ | | `put_grid`:sup:`*` | Output content using grid layout | | +---------------------------+------------------------------------------------------------+ | | `span` | Cross-cell content | | +---------------------------+------------------------------------------------------------+ | | `style`:sup:`*` | Customize the css style of output content | +--------------------+---------------------------+------------------------------------------------------------+ Output Scope -------------- .. seealso:: * :ref:`Use Guide: Output Scope ` .. autofunction:: put_scope .. autofunction:: use_scope .. autofunction:: get_scope .. autofunction:: clear .. autofunction:: remove .. autofunction:: scroll_to Content Outputting ----------------------- .. _scope_param: **Scope related parameters of output function** The output function will output the content to the "current scope" by default, and the "current scope" for the runtime context can be set by `use_scope()`. In addition, all output functions support a ``scope`` parameter to specify the destination scope to output: .. exportable-codeblock:: :name: put-xxx-scope :summary: ``scope`` parameter of the output function with use_scope('scope3'): put_text('text1 in scope3') # output to current scope: scope3 put_text('text in ROOT scope', scope='ROOT') # output to ROOT Scope put_text('text2 in scope3', scope='scope3') # output to scope3 The results of the above code are as follows:: text1 in scope3 text2 in scope3 text in ROOT scope A scope can contain multiple output items, the default behavior of output function is to append its content to target scope. The ``position`` parameter of output function can be used to specify the insert position in target scope. Each output item in a scope has an index, the first item's index is 0, and the next item's index is incremented by one. You can also use a negative number to index the items in the scope, -1 means the last item, -2 means the item before the last, ... The ``position`` parameter of output functions accepts an integer. When ``position>=0``, it means to insert content before the item whose index equal ``position``; when ``position<0``, it means to insert content after the item whose index equal ``position``: .. exportable-codeblock:: :name: put-xxx-position :summary: `position` parameter of the output function with use_scope('scope1'): put_text('A') ## ---- with use_scope('scope1'): # ..demo-only put_text('B', position=0) # insert B before A -> B A ## ---- with use_scope('scope1'): # ..demo-only put_text('C', position=-2) # insert C after B -> B C A ## ---- with use_scope('scope1'): # ..demo-only put_text('D', position=1) # insert D before C B -> B D C A **Output functions** .. autofunction:: put_text .. autofunction:: put_markdown .. py:function:: put_info(*contents, closable=False, scope=None, position=-1) -> Output: put_success(*contents, closable=False, scope=None, position=-1) -> Output: put_warning(*contents, closable=False, scope=None, position=-1) -> Output: put_error(*contents, closable=False, scope=None, position=-1) -> Output: Output Messages. :param contents: Message contents. The item is ``put_xxx()`` call, and any other type will be converted to ``put_text(content)``. :param bool closable: Whether to show a dismiss button on the right of the message. :param int scope, position: Those arguments have the same meaning as for `put_text()` .. versionadded:: 1.2 .. autofunction:: put_html .. autofunction:: put_link .. autofunction:: put_progressbar .. autofunction:: set_progressbar .. autofunction:: put_loading .. autofunction:: put_code .. autofunction:: put_table .. autofunction:: span .. autofunction:: put_buttons .. autofunction:: put_button .. autofunction:: put_image .. autofunction:: put_file .. autofunction:: put_tabs .. autofunction:: put_collapse .. autofunction:: put_scrollable .. autofunction:: put_datatable .. autofunction:: datatable_update .. autofunction:: datatable_insert .. autofunction:: datatable_remove .. autofunction:: put_widget Other Interactions -------------------- .. autofunction:: toast .. autofunction:: popup .. autofunction:: close_popup .. _style_and_layout: Layout and Style ------------------ .. autofunction:: put_row .. autofunction:: put_column .. autofunction:: put_grid .. autofunction:: style """ import copy import html import io import json import logging import string from base64 import b64encode from collections.abc import Mapping, Sequence from functools import wraps from typing import ( Any, Callable, Dict, List, Tuple, Union, Sequence as SequenceType, Mapping as MappingType ) try: from typing import Literal # added in Python 3.8 except ImportError: pass from .io_ctrl import output_register_callback, send_msg, Output, \ safely_destruct_output_when_exp, OutputList, scope2dom from .session import get_current_session, download from .utils import random_str, iscoroutinefunction, check_dom_name_value try: from PIL.Image import Image as PILImage except ImportError: PILImage = type('MockPILImage', (), dict(__init__=None)) logger = logging.getLogger(__name__) __all__ = ['Position', 'OutputPosition', 'remove', 'scroll_to', 'put_tabs', 'put_scope', 'put_text', 'put_html', 'put_code', 'put_markdown', 'use_scope', 'set_scope', 'clear', 'remove', 'put_table', 'put_buttons', 'put_image', 'put_file', 'PopupSize', 'popup', 'put_button', 'close_popup', 'put_widget', 'put_collapse', 'put_link', 'put_scrollable', 'style', 'put_column', 'put_row', 'put_grid', 'span', 'put_progressbar', 'set_progressbar', 'put_processbar', 'set_processbar', 'put_loading', 'output', 'toast', 'get_scope', 'put_info', 'put_error', 'put_warning', 'put_success', 'put_datatable', 'datatable_update', 'datatable_insert', 'datatable_remove', 'JSFunction'] # popup size class PopupSize: LARGE = 'large' NORMAL = 'normal' SMALL = 'small' class Position: TOP = 'top' MIDDLE = 'middle' BOTTOM = 'bottom' # position value of `put_xxx()` class OutputPosition: TOP = 0 BOTTOM = -1 _scope_name_allowed_chars = set(string.ascii_letters + string.digits + '_-') def set_scope(name: str, container_scope: str = None, position: int = OutputPosition.BOTTOM, if_exist: str = None): """Create a new scope. :param str name: scope name :param str container_scope: Specify the parent scope of this scope. When the scope doesn't exist, no operation is performed. :param int position: The location where this scope is created in the parent scope. (see :ref:`Scope related parameters `) :param str if_exist: What to do when the specified scope already exists: - `None`: Do nothing - `'remove'`: Remove the old scope first and then create a new one - `'clear'`: Just clear the contents of the old scope, but don't create a new scope Default is `None` """ if container_scope is None: container_scope = get_scope() check_dom_name_value(name, 'scope name') send_msg('output_ctl', dict(set_scope=scope2dom(name, no_css_selector=True), container=scope2dom(container_scope), position=position, if_exist=if_exist)) def get_scope(stack_idx: int = -1): """Get the scope name of runtime scope stack :param int stack_idx: The index of the runtime scope stack. Default is -1. 0 means the top level scope(the ``ROOT`` Scope), -1 means the current Scope, -2 means the scope used before entering the current scope, ... :return: Returns the scope name with the index, and returns ``None`` when occurs index error """ try: return get_current_session().get_scope_name(stack_idx) except IndexError: logger.exception("Scope stack index error") return None def clear(scope: str = None): """Clear the content of the specified scope :param str scope: Target scope name. Default is the current scope. """ if scope is None: scope = get_scope() send_msg('output_ctl', dict(clear=scope2dom(scope))) def remove(scope: str = None): """Remove the specified scope :param str scope: Target scope name. Default is the current scope. """ if scope is None: scope = get_scope() assert scope != 'ROOT', "Can not remove `ROOT` scope." send_msg('output_ctl', dict(remove=scope2dom(scope))) def scroll_to(scope: str = None, position: str = Position.TOP): """ Scroll the page to the specified scope :param str scope: Target scope. Default is the current scope. :param str position: Where to place the scope in the visible area of the page. Available value: * ``'top'`` : Keep the scope at the top of the visible area of the page * ``'middle'`` : Keep the scope at the middle of the visible area of the page * ``'bottom'`` : Keep the scope at the bottom of the visible area of the page """ if scope is None: scope = get_scope() send_msg('output_ctl', dict(scroll_to=scope2dom(scope), position=position)) def _get_output_spec(type, scope, position, **other_spec): """ get the spec dict of output functions :param str type: output type :param str scope: target scope :param int position: :param other_spec: Additional output parameters, the None value will not be included in the return value :return dict: ``spec`` field of ``output`` command """ spec = dict(type=type) # add non-None arguments to spec spec.update({k: v for k, v in other_spec.items() if v is not None}) if not scope: scope_name = get_scope() else: scope_name = scope spec['scope'] = scope2dom(scope_name) spec['position'] = position return spec def put_text(*texts: Any, sep: str = ' ', inline: bool = False, scope: str = None, position: int = OutputPosition.BOTTOM) -> Output: """ Output plain text :param texts: Texts need to output. The type can be any object, and the `str()` function will be used for non-string objects. :param str sep: The separator between the texts :param bool inline: Use text as an inline element (no line break at the end of the text). Default is ``False`` :param str scope: The target scope to output. If the scope does not exist, no operation will be performed. Can specify the scope name or use a integer to index the runtime scope stack. :param int position: The position where the content is output in target scope For more information about ``scope`` and ``position`` parameter, please refer to :ref:`User Manual ` """ content = sep.join(str(i) for i in texts) spec = _get_output_spec('text', content=content, inline=inline, scope=scope, position=position) return Output(spec) def _put_message(color, contents, closable=False, scope=None, position=OutputPosition.BOTTOM) -> Output: tpl = r""" """.strip() contents = [c if isinstance(c, Output) else put_text(c) for c in contents] return put_widget(template=tpl, data=dict(color=color, contents=contents, dismissible=closable), scope=scope, position=position).enable_context_manager() def put_info(*contents: Any, closable: bool = False, scope: str = None, position: int = OutputPosition.BOTTOM) -> Output: """Output information message. :param contents: Message contents. The item is ``put_xxx()`` call, and any other type will be converted to ``put_text(content)``. :param bool closable: Whether to show a dismiss button on the right of the message. :param int scope, position: Those arguments have the same meaning as for `put_text()` .. versionadded:: 1.2 """ return _put_message(color='info', contents=contents, closable=closable, scope=scope, position=position) def put_success(*contents: Any, closable: bool = False, scope: str = None, position: int = OutputPosition.BOTTOM) -> Output: """Output success message. .. seealso:: `put_info()` .. versionadded:: 1.2 """ return _put_message(color='success', contents=contents, closable=closable, scope=scope, position=position) def put_warning(*contents: Any, closable: bool = False, scope: str = None, position: int = OutputPosition.BOTTOM) -> Output: """Output warning message. .. seealso:: `put_info()` """ return _put_message(color='warning', contents=contents, closable=closable, scope=scope, position=position) def put_error(*contents: Any, closable: bool = False, scope: str = None, position: int = OutputPosition.BOTTOM) -> Output: """Output error message. .. seealso:: `put_info()` """ return _put_message(color='danger', contents=contents, closable=closable, scope=scope, position=position) # Due to the IPython rich output compatibility, # declare argument `html` to type `str` will cause type check error # so leave this argument's type `Any` def put_html(html: Any, sanitize: bool = False, scope: str = None, position: int = OutputPosition.BOTTOM) -> Output: """ Output HTML content :param html: html string :param bool sanitize: Whether to use `DOMPurify `_ to filter the content to prevent XSS attacks. :param int scope, position: Those arguments have the same meaning as for `put_text()` """ # Compatible with ipython rich output # See: https://ipython.readthedocs.io/en/stable/config/integrating.html?highlight=Rich%20display#rich-display if hasattr(html, '__html__'): html = html.__html__() elif hasattr(html, '_repr_html_'): html = html._repr_html_() spec = _get_output_spec('html', content=html, sanitize=sanitize, scope=scope, position=position) return Output(spec) def put_code(content: str, language: str = '', rows: int = None, scope: str = None, position: int = OutputPosition.BOTTOM) -> Output: """ Output code block :param str content: code string :param str language: language of code :param int rows: The max lines of code can be displayed, no limit by default. The scroll bar will be displayed when the content exceeds. :param int scope, position: Those arguments have the same meaning as for `put_text()` """ if not isinstance(content, str): content = str(content) # For fenced code blocks, escaping the backtick need to use more backticks backticks = '```' while backticks in content: backticks += '`' code = "%s%s\n%s\n%s" % (backticks, language, content, backticks) out = put_markdown(code, scope=scope, position=position) if rows is not None: max_height = rows * 19 + 32 # 32 is the code css padding out.style("max-height: %spx" % max_height) return out def _left_strip_multiple_line_string_literal(s): """Remove the indent for code format in string literal * The first line may have no leading whitespace * There may be empty line in s (since PyCharm will remove the line trailing whitespace) """ lines = s.splitlines() if len(lines) < 2: return s line = '' for line in lines[1:]: if line: break strip_cnt = 1 while line[:strip_cnt] in (' ' * strip_cnt, '\t' * strip_cnt): strip_cnt += 1 for line in lines[1:]: while line.strip() and line[:strip_cnt] not in (' ' * strip_cnt, '\t' * strip_cnt): strip_cnt -= 1 lines_ = [i[strip_cnt:] for i in lines[1:]] return '\n'.join(lines[:1] + lines_) def put_markdown(mdcontent: str, lstrip: bool = True, options: Dict[str, Union[str, bool]] = None, sanitize: bool = True, scope: str = None, position: int = OutputPosition.BOTTOM, **kwargs) -> Output: """ Output Markdown :param str mdcontent: Markdown string :param bool lstrip: Whether to remove the leading whitespace in each line of ``mdcontent``. The number of the whitespace to remove will be decided cleverly. :param dict options: Configuration when parsing Markdown. PyWebIO uses `marked `_ library to parse Markdown, the parse options see: https://marked.js.org/using_advanced#options (Only supports members of string and boolean type) :param bool sanitize: Whether to use `DOMPurify `_ to filter the content to prevent XSS attacks. :param int scope, position: Those arguments have the same meaning as for `put_text()` When using Python triple quotes syntax to output multi-line Markdown in a function, you can indent the Markdown text to keep a good code format. PyWebIO will cleverly remove the indent for you when show the Markdown:: # good code format def hello(): put_markdown(r\""" # H1 This is content. \""") .. versionchanged:: 1.5 Enable `lstrip` by default. Deprecate `strip_indent`. """ if 'strip_indent' in kwargs: import warnings # use stacklevel=2 to make the warning refer to put_markdown() call warnings.warn("`strip_indent` parameter is deprecated in `put_markdown()`", DeprecationWarning, stacklevel=2) if lstrip: mdcontent = _left_strip_multiple_line_string_literal(mdcontent) spec = _get_output_spec('markdown', content=mdcontent, options=options, sanitize=sanitize, scope=scope, position=position) return Output(spec) class span_: def __init__(self, content, row=1, col=1): self.content, self.row, self.col = content, row, col @safely_destruct_output_when_exp('content') def span(content: Union[str, Output], row: int = 1, col: int = 1): """Create cross-cell content in :func:`put_table()` and :func:`put_grid()` :param content: cell content. It can be a string or ``put_xxx()`` call. :param int row: Vertical span, that is, the number of spanning rows :param int col: Horizontal span, that is, the number of spanning columns :Example: .. exportable-codeblock:: :name: span :summary: Create cross-cell content with `span()` put_table([ ['C'], [span('E', col=2)], # 'E' across 2 columns ], header=[span('A', row=2), 'B']) # 'A' across 2 rows put_grid([ [put_text('A'), put_text('B')], [span(put_text('A'), col=2)], # 'A' across 2 columns ]) """ return span_(content, row, col) @safely_destruct_output_when_exp('tdata') def put_table(tdata: List[Union[List, Dict]], header: List[Union[str, Tuple[Any, str]]] = None, scope: str = None, position: int = OutputPosition.BOTTOM) -> Output: """ Output table :param list tdata: Table data, which can be a two-dimensional list or a list of dict. The table cell can be a string or ``put_xxx()`` call. The cell can use the :func:`span()` to set the cell span. :param list header: Table header. When the item of ``tdata`` is of type ``list``, if the ``header`` parameter is omitted, the first item of ``tdata`` will be used as the header. The header item can also use the :func:`span()` function to set the cell span. When ``tdata`` is list of dict, ``header`` can be used to specify the order of table headers. In this case, the ``header`` can be a list of dict key or a list of ``(