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 | `set_scope` | Create a new scope | | +---------------------------+------------------------------------------------------------+ | | `get_scope` | Get the 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 | | +---------------------------+------------------------------------------------------------+ | | `use_scope`:sup:`†` | Open or enter a 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 | | +---------------------------+------------------------------------------------------------+ | | `set_processbar` | Set the progress of progress bar | | +---------------------------+------------------------------------------------------------+ | | `put_loading`:sup:`†` | Output loading prompt | | +---------------------------+------------------------------------------------------------+ | | `put_code` | Output code block | | +---------------------------+------------------------------------------------------------+ | | `put_table`:sup:`*` | Output table | | +---------------------------+------------------------------------------------------------+ | | `put_buttons` | Output a group of buttons and bind click event | | +---------------------------+------------------------------------------------------------+ | | `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 | +--------------------+---------------------------+------------------------------------------------------------+ | Other | `output`:sup:`*` | Placeholder of output | +--------------------+---------------------------+------------------------------------------------------------+ Output Scope -------------- .. autofunction:: set_scope .. autofunction:: get_scope .. autofunction:: clear .. autofunction:: remove .. autofunction:: scroll_to .. autofunction:: use_scope Content Outputting ----------------------- .. autofunction:: put_text .. autofunction:: put_markdown .. py:function:: put_info(*contents, closable=False, scope=-1, position=-1) -> Output: put_success(*contents, closable=False, scope=-1, position=-1) -> Output: put_warning(*contents, closable=False, scope=-1, position=-1) -> Output: put_error(*contents, closable=False, scope=-1, position=-1) -> Output: Output Messages. :param contents: Message contents. The item is ``put_xxx()`` call, and any other type will be coverted 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_image .. autofunction:: put_file .. 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 Other -------------- .. autofunction:: output """ 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_text', 'put_html', 'put_code', 'put_markdown', 'use_scope', 'set_scope', 'clear', 'remove', 'put_table', 'put_buttons', 'put_image', 'put_file', 'PopupSize', 'popup', '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 class Scope: Current = -1 Root = 0 Parent = -2 _scope_name_allowed_chars = set(string.ascii_letters + string.digits + '_-') def set_scope(name, container_scope=Scope.Current, position=OutputPosition.BOTTOM, if_exist=None): """Create a new scope. :param str name: scope name :param int/str container_scope: Specify the parent scope of this scope. You can use the scope name or use a integer to index the runtime scope stack (see :ref:`User Guide `). When the scope does not exist, no operation is performed. :param int position: The location where this scope is created in the parent scope. Available values: `OutputPosition.TOP`: created at the top of the parent scope, `OutputPosition.BOTTOM`: created at the bottom of the parent scope. You can also use a integer to index the position (see :ref:`User Guide `) :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 isinstance(container_scope, int): container_scope = get_current_session().get_scope_name(container_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=Scope.Current): """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: return None def clear(scope=Scope.Current): """Clear the content of the specified scope :param int/str scope: Can specify the scope name or use a integer to index the runtime scope stack (see :ref:`User Guide `) """ if isinstance(scope, int): scope = get_current_session().get_scope_name(scope) send_msg('output_ctl', dict(clear=scope2dom(scope))) def remove(scope=Scope.Current): """Remove the specified scope :param int/str scope: Can specify the scope name or use a integer to index the runtime scope stack (see :ref:`User Guide `) """ if isinstance(scope, int): scope = get_current_session().get_scope_name(scope) assert scope != 'ROOT', "Can not remove `ROOT` scope." send_msg('output_ctl', dict(remove=scope2dom(scope))) def scroll_to(scope=Scope.Current, position=Position.TOP): """ Scroll the page to the specified scope :param str/int scope: Target scope. Can specify the scope name or use a integer to index the runtime scope stack (see :ref:`User Guide `) :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 isinstance(scope, int): scope = get_current_session().get_scope_name(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 int/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 isinstance(scope, int): scope_name = get_current_session().get_scope_name(scope) else: scope_name = scope spec['scope'] = scope2dom(scope_name) spec['position'] = position return spec def put_text(*texts, sep=' ', inline=False, scope=Scope.Current, 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 int/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=Scope.Current, 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=Scope.Current, position=OutputPosition.BOTTOM) -> Output: """Output information message. :param contents: Message contents. The item is ``put_xxx()`` call, and any other type will be coverted 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=Scope.Current, 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=Scope.Current, 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=Scope.Current, 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=Scope.Current, 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()` """ if hasattr(html, '__html__'): html = html.__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=Scope.Current, 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(out, "max-height: %spx" % max_height) return out def put_markdown(mdcontent, strip_indent=0, lstrip=False, options=None, sanitize=True, scope=Scope.Current, position=OutputPosition.BOTTOM) -> Output: """ Output Markdown :param str mdcontent: Markdown string :param int strip_indent: For each line of ``mdcontent``, if the first ``strip_indent`` characters are spaces, remove them :param bool lstrip: Whether to remove the whitespace at the beginning of each line of ``mdcontent`` :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, if you indent the Markdown text, you can use ``strip_indent`` or ``lstrip`` to prevent Markdown from parsing errors (But do not use ``strip_indent`` and ``lstrip`` at the same time):: # It is ugly without strip_indent or lstrip def hello(): put_markdown(r\""" # H1 This is content. \""") # Using lstrip to get beautiful indent def hello(): put_markdown(r\""" # H1 This is content. \""", lstrip=True) # Using strip_indent to get beautiful indent def hello(): put_markdown(r\""" # H1 This is content. \""", strip_indent=4) """ if strip_indent: lines = ( i[strip_indent:] if (i[:strip_indent] == ' ' * strip_indent) else i for i in mdcontent.splitlines() ) mdcontent = '\n'.join(lines) if lstrip: lines = (i.lstrip() for i in mdcontent.splitlines()) mdcontent = '\n'.join(lines) 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=Scope.Current, 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 ``(