Skip to content

Commit 1674675

Browse files
authored
feat: Improved handle_on alternative #1484. (#2113)
1 parent 4bc771d commit 1674675

File tree

25 files changed

+754
-440
lines changed

25 files changed

+754
-440
lines changed

client.go

+3-1
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,8 @@ var (
4646
// BootMsg represents the initial message sent to an app when a client first connects to it.
4747
type BootMsg struct {
4848
Data struct {
49-
Hash string `json:"#,omitempty"` // location hash
49+
Hash string `json:"#,omitempty"` // location hash
50+
SubmissionName string `json:"__wave_submission_name__,omitempty"` // mark the cause of the serve invocation
5051
} `json:"data"`
5152
Headers http.Header `json:"headers"` // forwarded headers
5253
}
@@ -171,6 +172,7 @@ func (c *Client) listen() {
171172

172173
if len(m.data) > 0 { // location hash
173174
boot.Data.Hash = string(m.data)
175+
boot.Data.SubmissionName = "#"
174176
}
175177

176178
body, err := json.Marshal(boot)

py/examples/hash_routing_parameters.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
# Use the browser's [location hash](https://developer.mozilla.org/en-US/docs/Web/API/Location/hash)
33
# for #routing using URLs, with parameters.
44
# ---
5-
from h2o_wave import main, app, Q, ui, on, handle_on
5+
from h2o_wave import main, app, Q, ui, on, run_on
66

77
air_passengers_fields = ['Year', 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun']
88
air_passengers_rows = [
@@ -62,6 +62,6 @@ async def serve(q: Q):
6262
content='Click on a cell in the table above!',
6363
)
6464

65-
await handle_on(q)
65+
await run_on(q)
6666

6767
await q.page.save()

py/examples/plot_events_routing.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# Plot / Events / Routing
22
# Handle #events on a #plot card using routing.
33
# ---
4-
from h2o_wave import main, app, on, handle_on, Q, ui, data
4+
from h2o_wave import main, app, on, run_on, Q, ui, data
55

66

77
@on('pricing.select_marks')
@@ -32,4 +32,4 @@ async def serve(q: Q):
3232
)
3333
await q.page.save()
3434
else:
35-
await handle_on(q)
35+
await run_on(q)

py/examples/routing.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# Routing
2-
# Use `on` and `handle_on` to simplify query handling by #routing queries to designated functions.
2+
# Use `on` and `run_on` to simplify query handling by #routing queries to designated functions.
33
# ---
4-
from h2o_wave import main, app, Q, ui, on, handle_on
4+
from h2o_wave import main, app, Q, ui, on, run_on
55

66

77
# This function is called when q.args['empty_cart'] is True.
@@ -60,4 +60,4 @@ async def serve(q: Q):
6060
],
6161
)
6262
await q.page.save()
63-
await handle_on(q)
63+
await run_on(q)

py/examples/routing_predicates.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# Routing / Predicates
2-
# Use `on` and `handle_on` with predicates to handle routing with custom conditions.
2+
# Use `on` and `run_on` with predicates to handle routing with custom conditions.
33
# ---
4-
from h2o_wave import main, app, Q, ui, on, handle_on
4+
from h2o_wave import main, app, Q, ui, on, run_on
55

66

77
# This function is called when q.args['temperature'] < 15.
@@ -29,7 +29,7 @@ async def serve(q: Q):
2929
q.args.temperature = 20
3030
await show_slider(q, "")
3131
else:
32-
await handle_on(q)
32+
await run_on(q)
3333

3434

3535
async def show_slider(q: Q, message: str):

py/examples/tour.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from typing import Dict, List, Optional, Tuple
1515
from urllib.parse import urlparse
1616

17-
from h2o_wave import Q, app, handle_on, main, on, ui
17+
from h2o_wave import Q, app, run_on, main, on, ui
1818

1919
example_dir = os.path.dirname(os.path.realpath(__file__))
2020
tour_tmp_dir = os.path.join(example_dir, '_tour_apps_tmp')
@@ -416,7 +416,7 @@ async def serve(q: Q):
416416
q.client.path = uuid.uuid4()
417417
await setup_page(q)
418418

419-
await handle_on(q)
419+
await run_on(q)
420420

421421
search = q.args[q.args['#'] or default_example_name]
422422
if search and not q.events.editor:

py/h2o_lightwave/h2o_lightwave/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323

2424
from .core import Ref, data, pack, Expando, expando_to_dict, clone_expando, copy_expando
2525
from .server import Q, wave_serve
26-
from .routing import on, handle_on
26+
from .routing import on, run_on, handle_on
2727
from .types import *
2828
from .version import __version__
2929

py/h2o_lightwave/h2o_lightwave/routing.py

+79-11
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414

1515
from typing import Optional, Callable
1616
from inspect import signature
17-
import asyncio
1817
import logging
1918
from starlette.routing import compile_path
2019
from .core import expando_to_dict
@@ -25,6 +24,8 @@
2524
_event_handlers = {} # dictionary of event_source => [(event_type, predicate, handler)]
2625
_arg_handlers = {} # dictionary of arg_name => [(predicate, handler)]
2726
_path_handlers = []
27+
_arg_with_params_handlers = []
28+
_handle_on_deprecated_warning_printed = False
2829

2930

3031
def _get_arity(func: Callable) -> int:
@@ -86,9 +87,8 @@ def wrap(func):
8687
# if not asyncio.iscoroutinefunction(func):
8788
# raise ValueError(f"@on function '{func_name}' must be async")
8889

89-
if predicate:
90-
if not callable(predicate):
91-
raise ValueError(f"@on predicate must be callable for '{func_name}'")
90+
if predicate and not callable(predicate):
91+
raise ValueError(f"@on predicate must be callable for '{func_name}'")
9292
if isinstance(arg, str) and len(arg):
9393
if arg.startswith('#'): # location hash
9494
rx, _, conv = compile_path(arg[1:])
@@ -100,6 +100,9 @@ def wrap(func):
100100
if not len(event):
101101
raise ValueError(f"@on event type cannot be empty in '{arg}' for '{func_name}'")
102102
_add_event_handler(source, event, func, predicate)
103+
elif "{" in arg and "}" in arg:
104+
rx, _, conv = compile_path(arg)
105+
_arg_with_params_handlers.append((predicate, func, _get_arity(func), rx, conv))
103106
else:
104107
_add_handler(arg, func, predicate)
105108
else:
@@ -110,28 +113,32 @@ def wrap(func):
110113
return wrap
111114

112115

113-
async def _invoke_handler(func: Callable, arity: int, q: Q, arg: any):
116+
async def _invoke_handler(func: Callable, arity: int, q: Q, arg: any, **params: any):
114117
if arity == 0:
115118
await func()
116119
elif arity == 1:
117120
await func(q)
118-
else:
121+
elif len(params) == 0:
119122
await func(q, arg)
123+
elif arity == len(params) + 1:
124+
await func(q, **params)
125+
else:
126+
await func(q, arg, **params)
120127

121128

122-
async def _match_predicate(predicate: Callable, func: Callable, arity: int, q: Q, arg: any) -> bool:
129+
async def _match_predicate(predicate: Callable, func: Callable, arity: int, q: Q, arg: any, **params: any) -> bool:
123130
if predicate:
124131
if predicate(arg):
125-
await _invoke_handler(func, arity, q, arg)
132+
await _invoke_handler(func, arity, q, arg, **params)
126133
return True
127134
else:
128-
if arg:
129-
await _invoke_handler(func, arity, q, arg)
135+
if arg is not None:
136+
await _invoke_handler(func, arity, q, arg, **params)
130137
return True
131138
return False
132139

133140

134-
async def handle_on(q: Q) -> bool:
141+
async def run_on(q: Q) -> bool:
135142
"""
136143
Handle the query using a query handler (a function annotated with `@on()`).
137144
@@ -141,6 +148,67 @@ async def handle_on(q: Q) -> bool:
141148
Returns:
142149
True if a matching query handler was found and invoked, else False.
143150
"""
151+
submitted = str(q.args['__wave_submission_name__'])
152+
153+
# Event handlers.
154+
for event_source in expando_to_dict(q.events):
155+
for entry in _event_handlers.get(event_source, []):
156+
event_type, predicate, func, arity = entry
157+
event = q.events[event_source]
158+
if event_type in event:
159+
arg_value = event[event_type]
160+
if await _match_predicate(predicate, func, arity, q, arg_value):
161+
return True
162+
163+
# Hash handlers.
164+
if submitted == '#':
165+
for rx, conv, func, arity in _path_handlers:
166+
match = rx.match(q.args[submitted])
167+
if match:
168+
params = match.groupdict()
169+
for key, value in params.items():
170+
params[key] = conv[key].convert(value)
171+
if len(params):
172+
if arity <= 1:
173+
await _invoke_handler(func, arity, q, None)
174+
else:
175+
await func(q, **params)
176+
else:
177+
await _invoke_handler(func, arity, q, None)
178+
return True
179+
180+
# Arg handlers.
181+
for entry in _arg_handlers.get(submitted, []):
182+
predicate, func, arity = entry
183+
if await _match_predicate(predicate, func, arity, q, q.args[submitted]):
184+
return True
185+
for predicate, func, arity, rx, conv in _arg_with_params_handlers:
186+
match = rx.match(submitted)
187+
if match:
188+
params = match.groupdict()
189+
for key, value in params.items():
190+
params[key] = conv[key].convert(value)
191+
if await _match_predicate(predicate, func, arity, q, q.args[submitted], **params):
192+
return True
193+
194+
return False
195+
196+
197+
async def handle_on(q: Q) -> bool:
198+
"""
199+
DEPRECATED: Handle the query using a query handler (a function annotated with `@on()`).
200+
201+
Args:
202+
q: The query context.
203+
204+
Returns:
205+
True if a matching query handler was found and invoked, else False.
206+
"""
207+
global _handle_on_deprecated_warning_printed
208+
if not _handle_on_deprecated_warning_printed:
209+
print('\033[93m' + 'WARNING: handle_on() is deprecated, use run_on() instead.' + '\033[0m')
210+
_handle_on_deprecated_warning_printed = True
211+
144212
event_sources = expando_to_dict(q.events)
145213
for event_source in event_sources:
146214
event = q.events[event_source]

py/h2o_wave/h2o_wave/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
"""
3232
from .core import Site, AsyncSite, site, Page, Ref, data, pack, Expando, expando_to_dict, clone_expando, copy_expando
3333
from .server import Q, app, main
34-
from .routing import on, handle_on
34+
from .routing import on, handle_on, run_on
3535
from .db import connect, WaveDBConnection, WaveDB, WaveDBError
3636
from .types import *
3737
from .test import cypress, Cypress

py/h2o_wave/h2o_wave/cli.py

+16-15
Original file line numberDiff line numberDiff line change
@@ -147,17 +147,17 @@ def run(app: str, no_reload: bool, no_autostart: bool):
147147
else:
148148
autostart = os.environ.get('H2O_WAVE_NO_AUTOSTART', 'false').lower() in ['false', '0', 'f']
149149

150-
waved = 'waved.exe' if IS_WINDOWS else './waved'
150+
waved_path = os.path.join(sys.exec_prefix, 'waved.exe' if IS_WINDOWS else 'waved')
151151
# OS agnostic wheels do not include waved - needed for HAC.
152-
is_waved_present = os.path.isfile(os.path.join(sys.exec_prefix, waved))
152+
is_waved_present = os.path.isfile(waved_path)
153153

154154
try:
155155
if autostart and is_waved_present and server_not_running:
156156
kwargs = {}
157157
if IS_WINDOWS:
158158
kwargs['creationflags'] = subprocess.CREATE_NEW_PROCESS_GROUP
159159

160-
waved_process = subprocess.Popen([waved], cwd=sys.exec_prefix, env=os.environ.copy(), **kwargs)
160+
waved_process = subprocess.Popen([waved_path], cwd=sys.exec_prefix, env=os.environ.copy(), **kwargs)
161161
time.sleep(1)
162162
server_not_running = _scan_free_port(server_port) == server_port
163163
retries = 3
@@ -166,21 +166,22 @@ def run(app: str, no_reload: bool, no_autostart: bool):
166166
time.sleep(2)
167167
server_not_running = _scan_free_port(server_port) == server_port
168168
retries = retries - 1
169-
finally:
169+
170170
if autostart and server_not_running:
171171
print('Could not connect to Wave server. Please start the Wave server (waved or waved.exe) prior to running any app.')
172172
return
173-
try:
174-
if not os.environ.get('H2O_WAVE_WAVED_DIR') and is_waved_present:
175-
os.environ['H2O_WAVE_WAVED_DIR'] = sys.exec_prefix
176-
reload_exclude = os.environ.get('H2O_WAVE_RELOAD_EXCLUDE', None)
177-
if reload_exclude:
178-
reload_exclude = reload_exclude.split(os.pathsep)
179-
uvicorn.run(f'{app}:main', host=host, port=port, reload=not no_reload, reload_excludes=reload_exclude)
180-
except Exception as e:
181-
if waved_process:
182-
waved_process.kill()
183-
raise e
173+
174+
if not os.environ.get('H2O_WAVE_WAVED_DIR') and is_waved_present:
175+
os.environ['H2O_WAVE_WAVED_DIR'] = sys.exec_prefix
176+
reload_exclude = os.environ.get('H2O_WAVE_RELOAD_EXCLUDE', None)
177+
if reload_exclude:
178+
reload_exclude = reload_exclude.split(os.pathsep)
179+
uvicorn.run(f'{app}:main', host=host, port=port, reload=not no_reload, reload_excludes=reload_exclude)
180+
except Exception as e:
181+
raise e
182+
finally:
183+
if waved_process:
184+
waved_process.kill()
184185

185186

186187
@main.command()

0 commit comments

Comments
 (0)