Pārlūkot izejas kodu

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

wangweimin 4 gadi atpakaļ
vecāks
revīzija
2cecceaa2e
4 mainītis faili ar 177 papildinājumiem un 82 dzēšanām
  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())
         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:
 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
     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
 Callback
 ^^^^^^^^^^^^^^
 ^^^^^^^^^^^^^^

+ 2 - 0
docs/spec.rst

@@ -185,6 +185,8 @@ The ``spec`` fields of ``output`` commands:
 
 
 * type: content type
 * type: content type
 * style: str, Additional css style
 * 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
 * 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>`
 * position: int, see :ref:`scope - User manual <scope_param>`
 * Other attributes of different types
 * 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 functools import partial, wraps
 
 
 from .session import chose_impl, next_client_event, get_current_task_id, get_current_session
 from .session import chose_impl, next_client_event, get_current_task_id, get_current_session
+from .utils import random_str
 
 
 logger = logging.getLogger(__name__)
 logger = logging.getLogger(__name__)
 
 
@@ -53,6 +54,46 @@ class Output:
             type(self).safely_destruct(spec)
             type(self).safely_destruct(spec)
             raise
             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):
     def embed_data(self):
         """返回供嵌入到其他消息中的数据,可以设置一些默认值"""
         """返回供嵌入到其他消息中的数据,可以设置一些默认值"""
         self.processed = True
         self.processed = True

+ 110 - 81
pywebio/output.py

@@ -1,81 +1,89 @@
 r"""
 r"""
 This module provides many functions to output all kinds of content to the user's browser, and supply flexible output control.
 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
 Functions list
 ---------------
 ---------------
 ..
 ..
   Use https://www.tablesgenerator.com/text_tables to generate/update below table
   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
 Output Scope
 --------------
 --------------
@@ -350,7 +358,7 @@ def _put_message(color, contents, closable=False, scope=Scope.Current, position=
 </div>""".strip()
 </div>""".strip()
     contents = [c if isinstance(c, Output) else put_text(c) for c in contents]
     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),
     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:
 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)
      `'warning'` 、`'info'`  、`'light'`  、 `'dark'` (default)
     :param int scope, position: Those arguments have the same meaning as for `put_text()`
     :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::
     .. exportable-codeblock::
         :name: put_loading
         :name: put_loading
@@ -900,6 +908,12 @@ def put_loading(shape='border', color='dark', scope=Scope.Current, position=Outp
                 put_text(shape, color)
                 put_text(shape, color)
                 put_loading(shape=shape, color=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
         # using style() to set the size of the loading prompt
         style(put_loading(), 'width:4rem; height:4rem')
         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">
     html = """<div class="spinner-{shape} text-{color}" role="status">
                 <span class="sr-only">Loading...</span>
                 <span class="sr-only">Loading...</span>
             </div>""".format(shape=shape, color=color)
             </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')
 @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
     """Output collapsible content
 
 
     :param str title: Title of 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}}
             {{& pywebio_output_parse}}
         {{/contents}}
         {{/contents}}
     </details>"""
     </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')
 @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:
                    scope=Scope.Current, position=OutputPosition.BOTTOM, **kwargs) -> Output:
     """Output a fixed height content area. scroll bar is displayed when the content exceeds the limit
     """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,
                       data=dict(dom_id=dom_id, contents=content, min_height=min_height,
                                 max_height=max_height, keep_bottom=keep_bottom,
                                 max_height=max_height, keep_bottom=keep_bottom,
                                 horizon_scroll=horizon_scroll, border=border),
                                 horizon_scroll=horizon_scroll, border=border),
-                      scope=scope, position=position)
+                      scope=scope, position=position).enable_context_manager()
 
 
 
 
 @safely_destruct_output_when_exp('data')
 @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')
 @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
     """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
     :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%')
         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')
 @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
     """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
     :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()`
     :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:
 def _row_column_layout(content, flow, size, scope=Scope.Current, position=OutputPosition.BOTTOM) -> Output: