Skip to content

Commit

Permalink
feat: Add arg pattern matching. #1008 #1484
Browse files Browse the repository at this point in the history
  • Loading branch information
mturoci committed Sep 7, 2023
1 parent 77191c9 commit 2aec8d5
Show file tree
Hide file tree
Showing 2 changed files with 63 additions and 8 deletions.
26 changes: 21 additions & 5 deletions py/h2o_wave/h2o_wave/routing.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
_event_handlers = {} # dictionary of event_source => [(event_type, predicate, handler)]
_arg_handlers = {} # dictionary of arg_name => [(predicate, handler)]
_path_handlers = []
_arg_with_params_handlers = []
_handle_on_deprecated_warning_printed = False


Expand Down Expand Up @@ -100,6 +101,9 @@ def wrap(func):
if not len(event):
raise ValueError(f"@on event type cannot be empty in '{arg}' for '{func_name}'")
_add_event_handler(source, event, func, predicate)
elif "{" in arg and "}" in arg:
rx, _, conv = compile_path(arg)
_arg_with_params_handlers.append((predicate, func, _get_arity(func), rx, conv))
else:
_add_handler(arg, func, predicate)
else:
Expand All @@ -110,23 +114,27 @@ def wrap(func):
return wrap


async def _invoke_handler(func: Callable, arity: int, q: Q, arg: any):
async def _invoke_handler(func: Callable, arity: int, q: Q, arg: any, **params: any):
if arity == 0:
await func()
elif arity == 1:
await func(q)
else:
elif len(params) == 0:
await func(q, arg)
elif arity == len(params) + 1:
await func(q, **params)
else:
await func(q, arg, **params)


async def _match_predicate(predicate: Callable, func: Callable, arity: int, q: Q, arg: any) -> bool:
async def _match_predicate(predicate: Callable, func: Callable, arity: int, q: Q, arg: any, **params: any) -> bool:
if predicate:
if predicate(arg):
await _invoke_handler(func, arity, q, arg)
await _invoke_handler(func, arity, q, arg, **params)
return True
else:
if arg is not None:
await _invoke_handler(func, arity, q, arg)
await _invoke_handler(func, arity, q, arg, **params)
return True
return False

Expand Down Expand Up @@ -175,6 +183,14 @@ async def run_on(q: Q) -> bool:
predicate, func, arity = entry
if await _match_predicate(predicate, func, arity, q, q.args[submitted]):
return True
for predicate, func, arity, rx, conv in _arg_with_params_handlers:
match = rx.match(submitted)
if match:
params = match.groupdict()
for key, value in params.items():
params[key] = conv[key].convert(value)
if await _match_predicate(predicate, func, arity, q, q.args[submitted], **params):
return True

return False

Expand Down
45 changes: 42 additions & 3 deletions py/tests/test_routing.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,9 @@ def mock_q(args={}, events={}):
arg_handlers = {
'button': AsyncMock(),
'checkbox': AsyncMock(),
'#': AsyncMock(),
}
path_handlers = {
'#': AsyncMock(),
'#page': AsyncMock(),
}
pattern_path_handlers = {
Expand All @@ -49,6 +49,13 @@ def mock_q(args={}, events={}):
'#page/pies/{pie_name:float}': AsyncMock(),
'#page/orders/{order_id:uuid}': AsyncMock(),
}
pattern_arg_handlers = {
'page/donuts/{donut_name}': AsyncMock(),
'page/muffins/{muffin_name:str}': AsyncMock(),
'page/cakes/{cake_name:int}': AsyncMock(),
'page/pies/{pie_name:float}': AsyncMock(),
'page/orders/{order_id:uuid}': AsyncMock(),
}
event_handlers = {
'source.event': AsyncMock(),
}
Expand All @@ -61,14 +68,18 @@ def mock_q(args={}, events={}):
for k, h in pattern_path_handlers.items():
rx, _, conv = compile_path(k[1:])
h2o_wave.routing._path_handlers.append((rx, conv, h, 2))
for k, h in pattern_arg_handlers.items():
rx, _, conv = compile_path(k)
h2o_wave.routing._arg_with_params_handlers.append((None, h, 2, rx, conv))
for k, h in event_handlers.items():
source, event = k.split('.', 1)
h2o_wave.routing._add_event_handler(source, event, h, None)


class TestRouting(unittest.IsolatedAsyncioTestCase):
def setUp(self):
for h in {**arg_handlers, **path_handlers, **pattern_path_handlers, **event_handlers}.values():
handlers = {**arg_handlers, **path_handlers, **pattern_path_handlers, **pattern_arg_handlers, **event_handlers}
for h in handlers.values():
h.reset_mock()

async def test_args(self):
Expand All @@ -93,7 +104,7 @@ async def test_hash(self):

async def test_empty_hash(self):
await run_on(mock_q(args={'#': '', '__wave_submission_name__': '#'}))
arg_handlers['#'].assert_called_once()
path_handlers['#'].assert_called_once()

async def test_events(self):
await run_on(mock_q(args={'__wave_submission_name__': 'source'}, events={'source': {'event': True}}))
Expand Down Expand Up @@ -132,3 +143,31 @@ async def test_hash_pattern_matching_uuid(self):
await run_on(q)
uuid = UUID('123e4567-e89b-12d3-a456-426655440000')
pattern_path_handlers['#page/orders/{order_id:uuid}'].assert_called_once_with(q, order_id=uuid)

async def test_arg_pattern_matching(self):
q = mock_q(args={'page/donuts/1': True, '__wave_submission_name__': 'page/donuts/1'})
await run_on(q)
pattern_arg_handlers['page/donuts/{donut_name}'].assert_called_once_with(q, donut_name='1')

async def test_arg_pattern_matching_str(self):
q = mock_q(args={'page/muffins/1': True, '__wave_submission_name__': 'page/muffins/1'})
await run_on(q)
pattern_arg_handlers['page/muffins/{muffin_name:str}'].assert_called_once_with(q, muffin_name='1')

async def test_arg_pattern_matching_int(self):
q = mock_q(args={'page/cakes/1': True, '__wave_submission_name__': 'page/cakes/1'})
await run_on(q)
pattern_arg_handlers['page/cakes/{cake_name:int}'].assert_called_once_with(q, cake_name=1)

async def test_arg_pattern_matching_float(self):
q = mock_q(args={'page/pies/3.14': True, '__wave_submission_name__': 'page/pies/3.14'})
await run_on(q)
pattern_arg_handlers['page/pies/{pie_name:float}'].assert_called_once_with(q, pie_name=3.14)

async def test_arg_pattern_matching_uuid(self):
uuid_str = '123e4567-e89b-12d3-a456-426655440000'
arg = f'page/orders/{uuid_str}'
q = mock_q(args={arg: True, '__wave_submission_name__': arg})
await run_on(q)
uuid = UUID(uuid_str)
pattern_arg_handlers['page/orders/{order_id:uuid}'].assert_called_once_with(q, order_id=uuid)

0 comments on commit 2aec8d5

Please sign in to comment.