Browse Source

feat: make some output-related functions support context manager.

wangweimin 4 năm trước cách đây
mục cha
commit
2cecceaa2e
4 tập tin đã thay đổi với 177 bổ sung82 xóa
  1. 24 1
      docs/guide.rst
  2. 2 0
      docs/spec.rst
  3. 41 0
      pywebio/io_ctrl.py
  4. 110 81
      pywebio/output.py

+ 24 - 1
docs/guide.rst

@@ -253,7 +253,11 @@ Similarly, you can pass ``put_xxx()`` calls to `popup() <pywebio.output.popup>`
         put_buttons(['close_popup()'], onclick=lambda _: close_popup())
     ])
 
-Other output functions that accept ``put_xxx()`` calls as parameters are `put_collapse() <pywebio.output.put_collapse>`, `put_scrollable() <pywebio.output.put_scrollable>`, `put_row() <pywebio.output.put_row>`, etc. In addition, you can use `put_widget() <pywebio.output.put_widget>` to make your own output widgets that can accept ``put_xxx()`` calls. For more information, please refer to corresponding function documentation.
+In addition, you can use `put_widget() <pywebio.output.put_widget>` to make your own output widgets that can accept ``put_xxx()`` calls.
+
+For a full list of functions that accept ``put_xxx()`` calls as content, see :ref:`Output functions list <output_func_list>`
+
+**Placeholder**
 
 When using combination output, if you want to dynamically update the ``put_xxx()`` content after it has been output, you can use the `output() <pywebio.output.output>` function. `output() <pywebio.output.output>` is like a placeholder, it can be passed in anywhere that ``put_xxx()`` can passed in. And after being output, the content can also be modified:
 
@@ -274,6 +278,25 @@ When using combination output, if you want to dynamically update the ``put_xxx()
     ## ----
     hobby.insert(0, put_markdown('**Coding**'))  # insert the Coding into the top of the hobby
 
+**Context Manager**
+
+Some output functions that accept ``put_xxx()`` calls as content can be used as context manager:
+
+.. exportable-codeblock::
+    :name: output-context-manager
+    :summary: Output as context manager
+
+    with put_collapse('This is title'):
+        for i in range(4):
+            put_text(i)
+
+        put_table([
+            ['Commodity', 'Price'],
+            ['Apple', '5.5'],
+            ['Banana', '7'],
+        ])
+
+For a full list of functions that support context manager, see :ref:`Output functions list <output_func_list>`
 
 Callback
 ^^^^^^^^^^^^^^

+ 2 - 0
docs/spec.rst

@@ -185,6 +185,8 @@ The ``spec`` fields of ``output`` commands:
 
 * type: content type
 * style: str, Additional css style
+* container_selector: The css selector of output widget's container. If empty(default), use widget self as container
+* container_dom_id: The dom id set to output widget's container.
 * scope: str, CSS selector of the output container. If multiple containers are matched, the content will be output to every matched container
 * position: int, see :ref:`scope - User manual <scope_param>`
 * Other attributes of different types

+ 41 - 0
pywebio/io_ctrl.py

@@ -8,6 +8,7 @@ from collections import UserList
 from functools import partial, wraps
 
 from .session import chose_impl, next_client_event, get_current_task_id, get_current_session
+from .utils import random_str
 
 logger = logging.getLogger(__name__)
 
@@ -53,6 +54,46 @@ class Output:
             type(self).safely_destruct(spec)
             raise
 
+        # For Context manager
+        self.enabled_context_manager = False
+        self.container_selector = None
+        self.container_dom_id = None
+        self.custom_enter = None
+        self.custom_exit = None
+
+    def enable_context_manager(self, container_selector=None, container_dom_id=None, custom_enter=None, custom_exit=None):
+        self.enabled_context_manager = True
+        self.container_selector = container_selector
+        self.container_dom_id = container_dom_id
+        self.custom_enter = custom_enter
+        self.custom_exit = custom_exit
+        return self
+
+    def __enter__(self):
+        if not self.enabled_context_manager:
+            raise RuntimeError("This output function can't be used as context manager!")
+        r = self.custom_enter(self) if self.custom_enter else None
+        if r is not None:
+            return r
+        self.container_dom_id = self.container_dom_id or random_str(10)
+        self.spec['container_selector'] = self.container_selector
+        self.spec['container_dom_id'] = self.container_dom_id
+        self.send()
+        get_current_session().push_scope(self.container_dom_id)
+        return self.container_dom_id
+
+    def __exit__(self, exc_type, exc_val, exc_tb):
+        """
+        If this method returns True,
+        it means that the context manager can handle the exception,
+        so that the with statement terminates the propagation of the exception
+        """
+        r = self.custom_exit(self, exc_type=exc_type, exc_val=exc_val, exc_tb=exc_tb) if self.custom_exit else None
+        if r is not None:
+            return r
+        get_current_session().pop_scope()
+        return False  # Propagate Exception
+
     def embed_data(self):
         """返回供嵌入到其他消息中的数据,可以设置一些默认值"""
         self.processed = True

+ 110 - 81
pywebio/output.py

@@ -1,81 +1,89 @@
 r"""
 This module provides many 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
 
-+--------------------+------------------+------------------------------------------------------------+
-|                    | **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`      | Open or enter a scope                                      |
-+--------------------+------------------+------------------------------------------------------------+
-| Content Outputting | `put_text`       | Output plain text                                          |
-|                    +------------------+------------------------------------------------------------+
-|                    | `put_markdown`   | Output Markdown                                            |
-|                    +------------------+------------------------------------------------------------+
-|                    | | `put_info`     | Output Messages.                                           |
-|                    | | `put_success`  |                                                            |
-|                    | | `put_warning`  |                                                            |
-|                    | | `put_error`    |                                                            |
-|                    +------------------+------------------------------------------------------------+
-|                    | `put_html`       | Output html                                                |
-|                    +------------------+------------------------------------------------------------+
-|                    | `put_link`       | Output link                                                |
-|                    +------------------+------------------------------------------------------------+
-|                    | `put_processbar` | Output a process bar                                       |
-|                    +------------------+------------------------------------------------------------+
-|                    | `set_processbar` | Set the progress of progress bar                           |
-|                    +------------------+------------------------------------------------------------+
-|                    | `put_loading`    | Output loading prompt                                      |
-|                    +------------------+------------------------------------------------------------+
-|                    | `put_code`       | Output code block                                          |
-|                    +------------------+------------------------------------------------------------+
-|                    | `put_table`      | 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_collapse`   | Output collapsible content                                 |
-|                    +------------------+------------------------------------------------------------+
-|                    | `put_scrollable` | | Output a fixed height content area,                      |
-|                    |                  | | scroll bar is displayed when the content                 |
-|                    |                  | | exceeds the limit                                        |
-|                    +------------------+------------------------------------------------------------+
-|                    | `put_widget`     | Output your own widget                                     |
-+--------------------+------------------+------------------------------------------------------------+
-| Other Interactions | `toast`          | Show a notification message                                |
-|                    +------------------+------------------------------------------------------------+
-|                    | `popup`          | Show popup                                                 |
-|                    +------------------+------------------------------------------------------------+
-|                    | `close_popup`    | Close the current popup window.                            |
-+--------------------+------------------+------------------------------------------------------------+
-| Layout and Style   | `put_row`        | Use row layout to output content                           |
-|                    +------------------+------------------------------------------------------------+
-|                    | `put_column`     | Use column layout to output content                        |
-|                    +------------------+------------------------------------------------------------+
-|                    | `put_grid`       | Output content using grid layout                           |
-|                    +------------------+------------------------------------------------------------+
-|                    | `span`           | Cross-cell content                                         |
-|                    +------------------+------------------------------------------------------------+
-|                    | `style`          | Customize the css style of output content                  |
-+--------------------+------------------+------------------------------------------------------------+
-| Other              | `output`         | Placeholder of output                                      |
-+--------------------+------------------+------------------------------------------------------------+
+| 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_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
 --------------
@@ -350,7 +358,7 @@ def _put_message(color, contents, closable=False, scope=Scope.Current, position=
 </div>""".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)
+                      scope=scope, position=position).enable_context_manager()
 
 
 def put_info(*contents, closable=False, scope=Scope.Current, position=OutputPosition.BOTTOM) -> Output:
@@ -889,7 +897,7 @@ def put_loading(shape='border', color='dark', scope=Scope.Current, position=Outp
      `'warning'` 、`'info'`  、`'light'`  、 `'dark'` (default)
     :param int scope, position: Those arguments have the same meaning as for `put_text()`
 
-    Example:
+    `put_loading()` can be used in 2 ways: direct call and context manager:
 
     .. exportable-codeblock::
         :name: put_loading
@@ -900,6 +908,12 @@ def put_loading(shape='border', color='dark', scope=Scope.Current, position=Outp
                 put_text(shape, color)
                 put_loading(shape=shape, color=color)
 
+        ## ----
+        # Use as context manager
+        with put_loading():
+            time.sleep(3)  # Some time-consuming operations
+            put_text("The answer of the universe is 42")
+
         ## ----
         # using style() to set the size of the loading prompt
         style(put_loading(), 'width:4rem; height:4rem')
@@ -910,11 +924,24 @@ def put_loading(shape='border', color='dark', scope=Scope.Current, position=Outp
     html = """<div class="spinner-{shape} text-{color}" role="status">
                 <span class="sr-only">Loading...</span>
             </div>""".format(shape=shape, color=color)
-    return put_html(html, sanitize=False, scope=scope, position=position)
+
+    dom_id = random_str(10)
+
+    def enter(self):
+        self.spec['container_dom_id'] = dom_id
+        self.send()
+        return dom_id
+
+    def exit_(self, exc_type, exc_val, exc_tb):
+        remove(dom_id)
+        return False  # Propagate Exception
+
+    return put_html(html, sanitize=False, scope=scope, position=position). \
+        enable_context_manager(custom_enter=enter, custom_exit=exit_)
 
 
 @safely_destruct_output_when_exp('content')
-def put_collapse(title, content, open=False, scope=Scope.Current, position=OutputPosition.BOTTOM) -> Output:
+def put_collapse(title, content=[], open=False, scope=Scope.Current, position=OutputPosition.BOTTOM) -> Output:
     """Output collapsible content
 
     :param str title: Title of content
@@ -953,11 +980,12 @@ def put_collapse(title, content, open=False, scope=Scope.Current, position=Outpu
             {{& pywebio_output_parse}}
         {{/contents}}
     </details>"""
-    return put_widget(tpl, dict(title=title, contents=content, open=open), scope=scope, position=position)
+    return put_widget(tpl, dict(title=title, contents=content, open=open), scope=scope,
+                      position=position).enable_context_manager()
 
 
 @safely_destruct_output_when_exp('content')
-def put_scrollable(content, height=400, keep_bottom=False, horizon_scroll=False, border=True,
+def put_scrollable(content=[], height=400, keep_bottom=False, horizon_scroll=False, border=True,
                    scope=Scope.Current, position=OutputPosition.BOTTOM, **kwargs) -> Output:
     """Output a fixed height content area. scroll bar is displayed when the content exceeds the limit
 
@@ -1039,7 +1067,7 @@ def put_scrollable(content, height=400, keep_bottom=False, horizon_scroll=False,
                       data=dict(dom_id=dom_id, contents=content, min_height=min_height,
                                 max_height=max_height, keep_bottom=keep_bottom,
                                 horizon_scroll=horizon_scroll, border=border),
-                      scope=scope, position=position)
+                      scope=scope, position=position).enable_context_manager()
 
 
 @safely_destruct_output_when_exp('data')
@@ -1088,7 +1116,7 @@ def put_widget(template, data, scope=Scope.Current, position=OutputPosition.BOTT
 
 
 @safely_destruct_output_when_exp('content')
-def put_row(content, size=None, scope=Scope.Current, position=OutputPosition.BOTTOM) -> Output:
+def put_row(content=[], size=None, scope=Scope.Current, position=OutputPosition.BOTTOM) -> Output:
     """Use row layout to output content. The content is arranged horizontally
 
     :param list content: Content list, the item is ``put_xxx()`` call or ``None``. ``None`` represents the space between the output
@@ -1122,11 +1150,12 @@ def put_row(content, size=None, scope=Scope.Current, position=OutputPosition.BOT
         put_row([put_code('A'), None, put_code('B')], size='40% 10px 60%')
 
     """
-    return _row_column_layout(content, flow='column', size=size, scope=scope, position=position)
+    return _row_column_layout(content, flow='column', size=size, scope=scope,
+                              position=position).enable_context_manager()
 
 
 @safely_destruct_output_when_exp('content')
-def put_column(content, size=None, scope=Scope.Current, position=OutputPosition.BOTTOM) -> Output:
+def put_column(content=[], size=None, scope=Scope.Current, position=OutputPosition.BOTTOM) -> Output:
     """Use column layout to output content. The content is arranged vertically
 
     :param list content: Content list, the item is ``put_xxx()`` call or ``None``. ``None`` represents the space between the output
@@ -1135,7 +1164,7 @@ def put_column(content, size=None, scope=Scope.Current, position=OutputPosition.
     :param int scope, position: Those arguments have the same meaning as for `put_text()`
     """
 
-    return _row_column_layout(content, flow='row', size=size, scope=scope, position=position)
+    return _row_column_layout(content, flow='row', size=size, scope=scope, position=position).enable_context_manager()
 
 
 def _row_column_layout(content, flow, size, scope=Scope.Current, position=OutputPosition.BOTTOM) -> Output: