Skip to content

Commit 0c0c471

Browse files
committed
feat: add pin_on_change()
1 parent 753c89f commit 0c0c471

File tree

2 files changed

+63
-4
lines changed

2 files changed

+63
-4
lines changed

pywebio/pin.py

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,21 +116,26 @@
116116
117117
.. autofunction:: pin_wait_change
118118
.. autofunction:: pin_update
119+
.. autofunction:: pin_on_change
119120
120121
"""
121122

122123
import string
124+
import threading
125+
from collections import defaultdict
123126

124127
from pywebio.input import parse_input_update_spec
125128
from pywebio.output import OutputPosition, Output
126129
from pywebio.output import _get_output_spec
127130
from .io_ctrl import send_msg, single_input_kwargs
128-
from .session import next_client_event, chose_impl
131+
from .session import next_client_event, chose_impl, get_current_session, get_session_implement, CoroutineBasedSession, \
132+
run_async, register_thread, SessionException
133+
from .utils import run_as_function, to_coroutine
129134

130135
_html_value_chars = set(string.ascii_letters + string.digits + '_')
131136

132137
__all__ = ['put_input', 'put_textarea', 'put_select', 'put_checkbox', 'put_radio', 'put_slider', 'put_actions',
133-
'pin', 'pin_update', 'pin_wait_change']
138+
'pin', 'pin_update', 'pin_wait_change', 'pin_on_change']
134139

135140

136141
def check_name(name):
@@ -328,3 +333,53 @@ def pin_update(name, **spec):
328333
check_name(name)
329334
attributes = parse_input_update_spec(spec)
330335
send_msg('pin_update', spec=dict(name=name, attributes=attributes))
336+
337+
338+
def pin_on_change(name, onchange, clear=False):
339+
"""
340+
Bind a callback function to pin widget, the function will be called when user change the value of the pin widget.
341+
342+
The ``onchange`` callback is invoked with one argument, the changed value of the pin widget.
343+
You can bind multiple functions to one pin widget, those functions will be invoked sequentially
344+
(default behavior, can be changed by `clear` parameter).
345+
346+
:param str name: pin widget name
347+
:param callable onchange: callback function
348+
:param bool clear: whether to clear the previous callbacks bound to this pin widget
349+
350+
.. versionadded:: 1.6
351+
"""
352+
current_session = get_current_session()
353+
354+
def pin_on_change_gen():
355+
while True:
356+
names = list(current_session.internal_save['pin_on_change_callbacks'].keys())
357+
if not names:
358+
continue
359+
360+
info = yield pin_wait_change(*names)
361+
callbacks = current_session.internal_save['pin_on_change_callbacks'][info['name']]
362+
for callback in callbacks:
363+
try:
364+
callback(info['value'])
365+
except Exception as e:
366+
if not isinstance(e, SessionException):
367+
current_session.on_task_exception()
368+
369+
first_run = False
370+
if 'pin_on_change_callbacks' not in current_session.internal_save:
371+
current_session.internal_save['pin_on_change_callbacks'] = defaultdict(list)
372+
first_run = True
373+
374+
if clear:
375+
current_session.internal_save['pin_on_change_callbacks'][name] = [onchange]
376+
else:
377+
current_session.internal_save['pin_on_change_callbacks'][name].append(onchange)
378+
379+
if first_run:
380+
if get_session_implement() == CoroutineBasedSession:
381+
run_async(to_coroutine(pin_on_change_gen()))
382+
else:
383+
t = threading.Thread(target=lambda: run_as_function(pin_on_change_gen()), daemon=True)
384+
register_thread(t)
385+
t.start()

test/18.pin_test.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,14 +42,18 @@ def target():
4242
names = ['input', 'textarea', 'code', 'select', 'select_multiple', 'checkbox', 'checkbox_inline', 'radio',
4343
'radio_inline', 'actions']
4444
values = {}
45+
on_change_values = {}
46+
47+
for name in names:
48+
pin_on_change(name, lambda val, name=name: on_change_values.__setitem__(name, val))
4549

4650
while len(names) != len(values):
4751
info = yield pin_wait_change(*names)
4852
values[info['name']] = info['value']
4953

5054
for name in names:
51-
assert (yield pin[name]) == values.get(name), f'{name}: {pin[name]}!={values.get(name)}'
52-
put_text(name, values.get(name))
55+
assert (yield pin[name]) == values.get(name) == on_change_values.get(name)
56+
put_text(name, values.get(name), on_change_values.get(name))
5357

5458
put_text(PASSED_TEXT)
5559

0 commit comments

Comments
 (0)