From f4201b405ffcaec8ed309e1e47b27c163573c9a2 Mon Sep 17 00:00:00 2001 From: nikto_b Date: Sat, 19 Jul 2025 05:02:14 +0300 Subject: [PATCH 1/2] Add router `add_subroute` method --- src/turbosloth/router.py | 82 ++++++++++++++++++++++++++++++++++++---- tests/test_router.py | 29 ++++++++++++++ 2 files changed, 103 insertions(+), 8 deletions(-) diff --git a/src/turbosloth/router.py b/src/turbosloth/router.py index c125180..4cd0922 100644 --- a/src/turbosloth/router.py +++ b/src/turbosloth/router.py @@ -15,10 +15,19 @@ class Route: regexp_subroutes: list[tuple[Pattern, list[str], Route]] handler: dict[MethodType, InternalHandlerType] - def __init__(self) -> None: - self.static_subroutes = {} - self.regexp_subroutes = [] - self.handler = {} + def __init__(self, + static_subroutes: Optional[dict[str, Route]] = None, + regexp_subroutes: Optional[list[tuple[Pattern, list[str], Route]]] = None, + handler: Optional[dict[MethodType, InternalHandlerType]] = None) -> None: + if static_subroutes is None: + static_subroutes = {} + if regexp_subroutes is None: + regexp_subroutes = [] + if handler is None: + handler = {} + self.static_subroutes = static_subroutes + self.regexp_subroutes = regexp_subroutes + self.handler = handler def _find_regexp_subroute(self, p: Pattern) -> Optional[tuple[list[str], Route]]: for _p, _n, _r in self.regexp_subroutes: @@ -31,10 +40,9 @@ class Route: self.regexp_subroutes.append((p, names, r)) self.regexp_subroutes.sort(key=lambda x: len(x[0].pattern), reverse=True) - def add(self, method: MethodType, sequence: Sequence[str], handler: InternalHandlerType) -> None: + def walk_into_route(self, sequence: Sequence[str]) -> Route: if len(sequence) == 0: - self.handler[method] = handler - return + return self part = sequence[0] @@ -57,8 +65,18 @@ class Route: if subroute is None: subroute = Route() self.static_subroutes[part] = subroute + return subroute.walk_into_route(sequence[1:]) - subroute.add(method, sequence[1:], handler) + def add(self, + method: MethodType, + sequence: Sequence[str], + handler: InternalHandlerType) -> None: + if len(sequence) == 0: + self.handler[method] = handler + return + + subroute = self.walk_into_route(sequence) + subroute.handler[method] = handler def get(self, method: MethodType, sequence: Sequence[str]) -> tuple[dict[str, str], Optional[InternalHandlerType]]: if len(sequence) == 0: @@ -90,6 +108,32 @@ class Route: submatches |= matches return submatches, handler + def compose_routes(self, other: Route) -> Route: + new_static_subroutes = self.static_subroutes.copy() + for k, v in other.static_subroutes.items(): + old_r = new_static_subroutes.get(k) + if old_r is not None: + v = old_r.compose_routes(v) + new_static_subroutes[k] = v + + new_regexp_subroutes = self.regexp_subroutes.copy() + + for p, n, r in other.regexp_subroutes: + found = False + for i, (_p, _n, _r) in enumerate(new_regexp_subroutes): + if p == _p: + new_regexp_subroutes[i] = (_p, n, _r.compose_routes(r)) + found = True + break + if not found: + new_regexp_subroutes.append((p, n, r)) + + return Route( + new_static_subroutes, + new_regexp_subroutes, + self.handler | other.handler + ) + class Router: _root: Route @@ -132,3 +176,25 @@ class Router: if h is None: raise NotFoundException(path or '/') return h + + def add_subroute(self, basepath: str, subr: Route | Router): + if isinstance(subr, Router): + subr = subr._root + + segments = basepath.split('/') + while len(segments) > 0 and len(segments[0]) == 0: + segments = segments[1:] + if len(segments) > 1: + tgt = self._root.walk_into_route(segments[:-1]) + old_tail = tgt.static_subroutes.get(segments[-1]) + if old_tail is not None: + subr = old_tail.compose_routes(subr) + + tgt.static_subroutes[segments[-1]] = subr + elif len(segments) == 1: + old_tail = self._root.static_subroutes.get(segments[-1]) + if old_tail is not None: + subr = old_tail.compose_routes(subr) + self._root.static_subroutes[segments[0]] = subr + else: + self._root = self._root.compose_routes(subr) diff --git a/tests/test_router.py b/tests/test_router.py index 040fcba..9af2bfd 100644 --- a/tests/test_router.py +++ b/tests/test_router.py @@ -79,3 +79,32 @@ def test_router_pattern_match(): with pytest.raises(NotFoundException, match='404\tNot Found: asd'): r.match('GET', 'asd/basdf') + + +def test_subroutes(): + async def f(*args): + pass + + async def d(*args): + pass + + r1 = Router() + r2 = Router() + + r1.add('GET', '/asdf', f) + r2.add('GET', '/asdf/a', d) + r1.add_subroute('', r2) + + assert r1.match('GET', '/asdf') == ({}, f) + assert r1.match('GET', '/asdf/a') == ({}, d) + r1.add_subroute('/asdf', r2) + assert r1.match('GET', '/asdf/asdf/a') == ({}, d) + + r1.add_subroute('/asdf' * 5, r2) + assert r1.match('GET', '/asdf' * 5 + '/asdf/a') == ({}, d) + + with pytest.raises(NotFoundException): + r1.match('GET', '/asdf/' * 5 + '/asdf/a') + + r1.add_subroute('/asdf/' * 5, r2) + assert r1.match('GET', '/asdf/' * 5 + '/asdf/a') == ({}, d) -- 2.49.1 From dd0c896df6285f51c0aee420dbf9953a0df131ad Mon Sep 17 00:00:00 2001 From: nikto_b Date: Sat, 19 Jul 2025 05:08:56 +0300 Subject: [PATCH 2/2] Add an app add subroute fn --- src/turbosloth/app.py | 5 ++++- src/turbosloth/router.py | 2 +- tests/test_router.py | 8 ++++---- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/turbosloth/app.py b/src/turbosloth/app.py index 476be8e..f3b99a5 100644 --- a/src/turbosloth/app.py +++ b/src/turbosloth/app.py @@ -12,7 +12,7 @@ from .interfaces.serialize_selector import SerializeSelector from .interfaces.serialized import SerializedResponse, SerializedRequest from .interfaces.serialized.text import TextSerializedResponse from .req_schema import UnwrappedRequest -from .router import Router +from .router import Router, Route from .types import HandlerType, InternalHandlerType, ContentType from .internal_types import Scope, Receive, Send, MethodType, QTYPE, BTYPE, PTYPE from breakshaft.convertor import ConvRepo @@ -26,6 +26,9 @@ class ASGIApp(Protocol): def route(self, method: MethodType, path_pattern: str): raise RuntimeError('stub!') + def add_subroute(self, subr: Route | Router, basepath: str) -> None: + self.router.add_subroute(subr, basepath) + class HTTPApp(ASGIApp): serialize_selector: SerializeSelector diff --git a/src/turbosloth/router.py b/src/turbosloth/router.py index 4cd0922..e5051be 100644 --- a/src/turbosloth/router.py +++ b/src/turbosloth/router.py @@ -177,7 +177,7 @@ class Router: raise NotFoundException(path or '/') return h - def add_subroute(self, basepath: str, subr: Route | Router): + def add_subroute(self, subr: Route | Router, basepath: str): if isinstance(subr, Router): subr = subr._root diff --git a/tests/test_router.py b/tests/test_router.py index 9af2bfd..62c6269 100644 --- a/tests/test_router.py +++ b/tests/test_router.py @@ -93,18 +93,18 @@ def test_subroutes(): r1.add('GET', '/asdf', f) r2.add('GET', '/asdf/a', d) - r1.add_subroute('', r2) + r1.add_subroute(r2, '') assert r1.match('GET', '/asdf') == ({}, f) assert r1.match('GET', '/asdf/a') == ({}, d) - r1.add_subroute('/asdf', r2) + r1.add_subroute(r2, '/asdf') assert r1.match('GET', '/asdf/asdf/a') == ({}, d) - r1.add_subroute('/asdf' * 5, r2) + r1.add_subroute(r2, '/asdf' * 5) assert r1.match('GET', '/asdf' * 5 + '/asdf/a') == ({}, d) with pytest.raises(NotFoundException): r1.match('GET', '/asdf/' * 5 + '/asdf/a') - r1.add_subroute('/asdf/' * 5, r2) + r1.add_subroute(r2, '/asdf/' * 5) assert r1.match('GET', '/asdf/' * 5 + '/asdf/a') == ({}, d) -- 2.49.1