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_processbar` | Output a process bar | | +---------------------------+------------------------------------------------------------+ | | `put_loading`:sup:`†` | Output loading prompt | | +---------------------------+------------------------------------------------------------+ | | `put_code` | Output code block | | +---------------------------+------------------------------------------------------------+ | | `put_table`:sup:`*` | Output table | | +---------------------------+------------------------------------------------------------+ | | | `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_processbar .. autofunction:: set_processbar .. 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_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 logging import string from base64 import b64encode from collections.abc import Mapping, Sequence from functools import wraps from typing import Union 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, is_html_safe_value try: from PIL.Image import Image as PILImage except ImportError: PILImage = type('MockPILImage', (), dict(__init__=None)) logger = logging.getLogger(__name__) __all__ = ['Position', '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_processbar', 'set_processbar', 'put_loading', 'output', 'toast', 'get_scope', 'put_info', 'put_error', 'put_warning', 'put_success'] # 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, container_scope=None, position=OutputPosition.BOTTOM, if_exist=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() assert is_html_safe_value(name), "Scope name only allow letter/digit/'_'/'-' char." 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=-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=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=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=None, position=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, sep=' ', inline=False, scope=None, position=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, closable=False, scope=None, position=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, closable=False, scope=None, position=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, closable=False, scope=None, position=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, closable=False, scope=None, position=OutputPosition.BOTTOM) -> Output: """Output error message. .. seealso:: `put_info()` """ return _put_message(color='danger', contents=contents, closable=closable, scope=scope, position=position) def put_html(html, sanitize=False, scope=None, position=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, language='', rows=None, scope=None, position=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, lstrip=True, options=None, sanitize=True, scope=None, position=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, row=1, col=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, header=None, scope=None, position=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`` is used to specify the order of table headers, which cannot be omitted. In this case, the ``header`` can be a list of dict key or a list of ``(