Skip to content

Commit b1629a0

Browse files
committed
add put_scope() to replace output()
1 parent ebc29a1 commit b1629a0

File tree

9 files changed

+154
-61
lines changed

9 files changed

+154
-61
lines changed

docs/guide.rst

Lines changed: 37 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -297,29 +297,6 @@ In addition, you can use `put_widget() <pywebio.output.put_widget>` to make your
297297

298298
For a full list of functions that accept ``put_xxx()`` calls as content, see :ref:`Output functions list <output_func_list>`
299299

300-
**Placeholder**
301-
302-
When using combination output, if you want to dynamically update the ``put_xxx()`` content after it has been output,
303-
you can use the `output() <pywebio.output.output>` function. `output() <pywebio.output.output>` is like a placeholder,
304-
it can be passed in anywhere that ``put_xxx()`` can passed in. And after being output, the content can also be modified:
305-
306-
.. exportable-codeblock::
307-
:name: output
308-
:summary: Output placeholder——`output()`
309-
310-
hobby = output('Coding') # equal to output(put_text('Coding'))
311-
put_table([
312-
['Name', 'Hobbies'],
313-
['Wang', hobby] # hobby is initialized to Coding
314-
])
315-
## ----
316-
317-
hobby.reset('Movie') # hobby is reset to Movie
318-
## ----
319-
hobby.append('Music', put_text('Drama')) # append Music, Drama to hobby
320-
## ----
321-
hobby.insert(0, put_markdown('**Coding**')) # insert the Coding into the top of the hobby
322-
323300
**Context Manager**
324301

325302
Some output functions that accept ``put_xxx()`` calls as content can be used as context manager:
@@ -525,11 +502,46 @@ The above code will generate the following scope layout::
525502
│ └─────────────────────┘ │
526503
└─────────────────────────┘
527504

505+
.. _put_scope:
506+
507+
**put_scope()**
508+
509+
We already know that the scope is a container of output content. So can we use this container as a sub-item
510+
of a output (like, set a cell in table as a container)? Yes, you can use `put_scope() <pywebio.output.put_scope>` to
511+
create a scope explicitly.
512+
The function name starts with ``put_``, which means it can be pass to the functions that accept ``put_xxx()`` calls.
513+
514+
.. exportable-codeblock::
515+
:name: put_scope
516+
:summary: `put_scope()`
517+
518+
put_table([
519+
['Name', 'Hobbies'],
520+
['Tom', put_scope('hobby', content=put_text('Coding'))] # hobby is initialized to coding
521+
])
522+
523+
## ----
524+
with use_scope('hobby', clear=True):
525+
put_text('Movie') # hobby is reset to Movie
526+
527+
## ----
528+
# append Music, Drama to hobby
529+
with use_scope('hobby'):
530+
put_text('Music')
531+
put_text('Drama')
532+
533+
## ----
534+
# insert the Coding into the top of the hobby
535+
put_markdown('**Coding**', scope='hobby', position=0)
536+
537+
538+
.. caution:: It is not allowed to have two scopes with the same name in the application.
539+
528540
**Scope control**
529541

530-
In addition to `use_scope() <pywebio.output.use_scope>`, PyWebIO also provides the following scope control functions:
542+
In addition to `use_scope() <pywebio.output.use_scope>` and `put_scope() <pywebio.output.put_scope>`,
543+
PyWebIO also provides the following scope control functions:
531544

532-
* `set_scope(name) <pywebio.output.set_scope>` : Create scope at current location(or specified location)
533545
* `clear(scope) <pywebio.output.clear>` : Clear the contents of the scope
534546
* `remove(scope) <pywebio.output.remove>` : Remove scope
535547
* `scroll_to(scope) <pywebio.output.scroll_to>` : Scroll the page to the scope

docs/spec.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,11 @@ Unique attributes of different types:
248248

249249
* input: input spec, same as the item of ``input_group.inputs``
250250

251+
* type: scope
252+
253+
* dom_id: the DOM id need to be set to this widget
254+
* contents list: list of output spec
255+
251256
pin_value
252257
^^^^^^^^^^^^^^^
253258

pywebio/output.py

Lines changed: 44 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,17 @@
1717
+--------------------+---------------------------+------------------------------------------------------------+
1818
| | **Name** | **Description** |
1919
+--------------------+---------------------------+------------------------------------------------------------+
20-
| Output Scope | `set_scope` | Create a new scope |
20+
| Output Scope | `put_scope` | Create a new scope |
2121
| +---------------------------+------------------------------------------------------------+
22-
| | `get_scope` | Get the scope name in the runtime scope stack |
22+
| | `use_scope`:sup:`†` | Enter a scope |
23+
| +---------------------------+------------------------------------------------------------+
24+
| | `get_scope` | Get the current scope name in the runtime scope stack |
2325
| +---------------------------+------------------------------------------------------------+
2426
| | `clear` | Clear the content of scope |
2527
| +---------------------------+------------------------------------------------------------+
2628
| | `remove` | Remove the scope |
2729
| +---------------------------+------------------------------------------------------------+
2830
| | `scroll_to` | Scroll the page to the scope |
29-
| +---------------------------+------------------------------------------------------------+
30-
| | `use_scope`:sup:`†` | Open or enter a scope |
3131
+--------------------+---------------------------+------------------------------------------------------------+
3232
| Content Outputting | `put_text` | Output plain text |
3333
| +---------------------------+------------------------------------------------------------+
@@ -95,12 +95,12 @@
9595
9696
* :ref:`Use Guide: Output Scope <output_scope>`
9797
98-
.. autofunction:: set_scope
98+
.. autofunction:: put_scope
99+
.. autofunction:: use_scope
99100
.. autofunction:: get_scope
100101
.. autofunction:: clear
101102
.. autofunction:: remove
102103
.. autofunction:: scroll_to
103-
.. autofunction:: use_scope
104104
105105
Content Outputting
106106
-----------------------
@@ -232,7 +232,7 @@
232232

233233
logger = logging.getLogger(__name__)
234234

235-
__all__ = ['Position', 'remove', 'scroll_to', 'put_tabs',
235+
__all__ = ['Position', 'remove', 'scroll_to', 'put_tabs', 'put_scope',
236236
'put_text', 'put_html', 'put_code', 'put_markdown', 'use_scope', 'set_scope', 'clear', 'remove',
237237
'put_table', 'put_buttons', 'put_image', 'put_file', 'PopupSize', 'popup', 'put_button',
238238
'close_popup', 'put_widget', 'put_collapse', 'put_link', 'put_scrollable', 'style', 'put_column',
@@ -1390,10 +1390,30 @@ def put_grid(content, cell_width='auto', cell_height='auto', cell_widths=None, c
13901390
return put_widget(template=tpl, data=dict(contents=content), scope=scope, position=position)
13911391

13921392

1393+
@safely_destruct_output_when_exp('content')
1394+
def put_scope(name, content=[], scope=None, position=OutputPosition.BOTTOM) -> Output:
1395+
"""Output a scope
1396+
1397+
:param str name:
1398+
:param list/put_xxx() content: The initial content of the scope, can be ``put_xxx()`` or a list of it.
1399+
:param int scope, position: Those arguments have the same meaning as for `put_text()`
1400+
"""
1401+
if not isinstance(content, list):
1402+
content = [content]
1403+
1404+
dom_id = scope2dom(name, no_css_selector=True)
1405+
1406+
spec = _get_output_spec('scope', dom_id=dom_id, contents=content, scope=scope, position=position)
1407+
return Output(spec)
1408+
1409+
13931410
@safely_destruct_output_when_exp('contents')
13941411
def output(*contents):
13951412
"""Placeholder of output
13961413
1414+
.. deprecated:: 1.5
1415+
See :ref:`User Guide <put_scope>` for new way to set css style for output.
1416+
13971417
``output()`` can be passed in anywhere that ``put_xxx()`` can passed in. A handler it returned by ``output()``,
13981418
and after being output, the content can also be modified by the handler (See code example below).
13991419
@@ -1431,6 +1451,10 @@ def output(*contents):
14311451
14321452
"""
14331453

1454+
import warnings
1455+
warnings.warn("`pywebio.output.output()` is deprecated since v1.5 and will remove in the future version, "
1456+
"use `pywebio.output.put_scope()` instead", DeprecationWarning, stacklevel=2)
1457+
14341458
class OutputHandler(Output):
14351459
"""
14361460
与 `Output` 的不同在于, 不会在销毁时(__del__)自动输出
@@ -1687,17 +1711,16 @@ def show_msg():
16871711
clear_scope = clear
16881712

16891713

1690-
def use_scope(name=None, clear=False, create_scope=True, **scope_params):
1691-
"""Open or enter a scope. Can be used as context manager and decorator.
1714+
def use_scope(name=None, clear=False, **kwargs):
1715+
"""use_scope(name=None, clear=False)
1716+
1717+
Open or enter a scope. Can be used as context manager and decorator.
16921718
16931719
See :ref:`User manual - use_scope() <use_scope>`
16941720
16951721
:param str name: Scope name. If it is None, a globally unique scope name is generated.
16961722
(When used as context manager, the context manager will return the scope name)
16971723
:param bool clear: Whether to clear the contents of the scope before entering the scope.
1698-
:param bool create_scope: Whether to create scope when scope does not exist.
1699-
:param scope_params: Extra parameters passed to `set_scope()` when need to create scope.
1700-
Only available when ``create_scope=True``.
17011724
17021725
:Usage:
17031726
@@ -1711,17 +1734,22 @@ def app():
17111734
put_xxx()
17121735
17131736
"""
1737+
# For backward compatible
1738+
# :param bool create_scope: Whether to create scope when scope does not exist.
1739+
# :param scope_params: Extra parameters passed to `set_scope()` when need to create scope.
1740+
# Only available when ``create_scope=True``.
1741+
create_scope = kwargs.pop('create_scope', True)
1742+
scope_params = kwargs
1743+
17141744
if name is None:
17151745
name = random_str(10)
17161746
else:
17171747
assert is_html_safe_value(name), "Scope name only allow letter/digit/'_'/'-' char."
17181748

17191749
def before_enter():
17201750
if create_scope:
1721-
set_scope(name, **scope_params)
1722-
1723-
if clear:
1724-
clear_scope(name)
1751+
if_exist = 'clear' if clear else None
1752+
set_scope(name, if_exist=if_exist, **scope_params)
17251753

17261754
return use_scope_(name=name, before_enter=before_enter)
17271755

test/template.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,19 @@ def edit_row(choice, row):
297297
hobby.append(put_text('Music'), put_text('Drama'))
298298
hobby.insert(0, put_markdown('**Coding**'))
299299

300+
put_table([
301+
['Name', 'Hobbies'],
302+
['Tom', put_scope('hobby', content=put_text('Coding'))]
303+
])
304+
305+
with use_scope('hobby', clear=True):
306+
put_text('Movie') # hobby is reset to Movie
307+
308+
with use_scope('hobby'):
309+
put_text('Music')
310+
put_text('Drama')
311+
312+
put_markdown('**Coding**', scope='hobby', position=0)
300313

301314

302315
def background_output():

webiojs/src/handlers/output.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,27 @@ import {body_scroll_to} from "../utils";
44

55
import {getWidgetElement} from "../models/output"
66
import {CommandHandler} from "./base";
7-
import {AfterPinShow} from "../models/pin";
87

98
const DISPLAY_NONE_TAGS = ['script', 'style'];
109

10+
let after_show_callbacks: (() => void) [] = [];
11+
12+
// register a callback to execute after the current output widget showing
13+
export function AfterCurrentOutputWidgetShow(callback: () => void){
14+
after_show_callbacks.push(callback);
15+
}
16+
17+
export function trigger_output_widget_show_event() {
18+
for (let cb of after_show_callbacks) {
19+
try {
20+
cb.call(this);
21+
} catch (e) {
22+
console.error('Error in callback of pin widget show event.');
23+
}
24+
}
25+
after_show_callbacks = [];
26+
}
27+
1128
export class OutputHandler implements CommandHandler {
1229
session: Session;
1330

@@ -79,7 +96,7 @@ export class OutputHandler implements CommandHandler {
7996
else if (state.AutoScrollBottom && output_to_root)
8097
this.scroll_bottom();
8198
}
82-
AfterPinShow();
99+
trigger_output_widget_show_event();
83100
} else if (msg.command === 'output_ctl') {
84101
this.handle_output_ctl(msg);
85102
}

webiojs/src/handlers/popup.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import {Command, Session} from "../session";
22

33
import {render_tpl} from "../models/output"
44
import {CommandHandler} from "./base";
5-
import {AfterPinShow} from "../models/pin";
5+
import {trigger_output_widget_show_event} from "./output";
66

77

88
export class PopupHandler implements CommandHandler {
@@ -32,7 +32,7 @@ export class PopupHandler implements CommandHandler {
3232

3333
let elem = PopupHandler.get_element(msg.spec);
3434
this.body.append(elem);
35-
AfterPinShow();
35+
trigger_output_widget_show_event();
3636

3737
// 弹窗关闭后就立即销毁
3838
elem.on('hidden.bs.modal', function (e) {

webiojs/src/i18n.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,9 @@ const translations: { [lang: string]: { [msgid: string]: string } } = {
1414
"submit": "Submit",
1515
"reset": "Reset",
1616
"cancel": "Cancel",
17-
"duplicated_pin_name": "This pin widget has expired (due to the output of a new pin widget with the same name ).",
17+
"duplicated_pin_name": "This pin widget has expired (due to the output of a new pin widget with the same name).",
1818
"browse_file": "Browse",
19+
"duplicated_scope_name": "Error: The name of this scope is duplicated with the previous one!",
1920
},
2021
"zh": {
2122
"disconnected_with_server": "与服务器连接已断开,请刷新页面重新操作",
@@ -28,6 +29,7 @@ const translations: { [lang: string]: { [msgid: string]: string } } = {
2829
"cancel": "取消",
2930
"duplicated_pin_name": "该 Pin widget 已失效(由于输出了新的同名 pin widget)",
3031
"browse_file": "浏览文件",
32+
"duplicated_scope_name": "错误: 此scope与已有scope重复!",
3133
},
3234
"ru": {
3335
"disconnected_with_server": "Соединение с сервером потеряно, пожалуйста перезагрузите страницу",

webiojs/src/models/output.ts

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import {b64toBlob, randomid} from "../utils";
22
import * as marked from 'marked';
33
import {pushData} from "../session";
44
import {PinWidget} from "./pin";
5+
import {t} from "../i18n";
6+
import {AfterCurrentOutputWidgetShow} from "../handlers/output";
57

68
export interface Widget {
79
handle_type: string;
@@ -199,14 +201,39 @@ let TabsWidget = {
199201
}
200202
};
201203

204+
205+
const SCOPE_TPL = `<div>
206+
{{#contents}}
207+
{{& pywebio_output_parse}}
208+
{{/contents}}
209+
</div>`;
210+
let ScopeWidget = {
211+
handle_type: 'scope',
212+
get_element: function (spec: {dom_id:string, contents: any[]}) {
213+
let elem = render_tpl(SCOPE_TPL, spec);
214+
// need to check the duplicate id after current output widget shown.
215+
// because the current widget may have multiple sub-widget which have same dom id.
216+
AfterCurrentOutputWidgetShow(()=>{
217+
if($(`#${spec.dom_id}`).length !== 0){
218+
let tip = `<p style="color: grey; border:1px solid #ced4da; padding: .375rem .75rem;">${t("duplicated_scope_name")}</p>`;
219+
elem.empty().html(tip);
220+
}else{
221+
elem.attr('id', spec.dom_id);
222+
}
223+
})
224+
return elem;
225+
}
226+
};
227+
228+
202229
let CustomWidget = {
203230
handle_type: 'custom_widget',
204231
get_element: function (spec: { template: string, data: { [i: string]: any } }) {
205232
return render_tpl(spec.template, spec.data);
206233
}
207234
};
208235

209-
let all_widgets: Widget[] = [Text, Markdown, Html, Buttons, File, Table, CustomWidget, TabsWidget, PinWidget];
236+
let all_widgets: Widget[] = [Text, Markdown, Html, Buttons, File, Table, CustomWidget, TabsWidget, PinWidget, ScopeWidget];
210237

211238

212239
let type2widget: { [i: string]: Widget } = {};
@@ -283,7 +310,7 @@ export function render_tpl(tpl: string, data: { [i: string]: any }) {
283310
let sub_elem = getWidgetElement(spec);
284311
elem.find(`#${dom_id}`).replaceWith(sub_elem);
285312
} catch (e) {
286-
console.error('Error when render widget: \n%s', JSON.stringify(spec));
313+
console.error('Error when render widget: \n%s\nSPEC:%s', e, JSON.stringify(spec));
287314
}
288315
}
289316
return elem;

0 commit comments

Comments
 (0)