11from __future__ import annotations
22
3- from dataclasses import dataclass
3+ from dataclasses import dataclass , replace
44from pathlib import Path
5- from typing import Any , Callable , Iterator , Sequence
5+ from typing import Any , Callable , Iterator , Sequence , TypeVar
66from urllib .parse import parse_qs
77
88from idom import (
99 component ,
1010 create_context ,
11- use_memo ,
12- use_state ,
1311 use_context ,
1412 use_location ,
13+ use_memo ,
14+ use_state ,
1515)
16- from idom .core .types import VdomAttributesAndChildren , VdomDict
17- from idom .core .vdom import coalesce_attributes_and_children
18- from idom .types import ComponentType , Location , Context
19- from idom .web .module import export , module_from_file
2016from idom .backend .hooks import ConnectionContext , use_connection
2117from idom .backend .types import Connection , Location
22- from starlette . routing import compile_path as _compile_starlette_path
23-
24- from idom_router . types import RoutePattern , RouteCompiler , Route
18+ from idom . core . types import VdomChild , VdomDict
19+ from idom . types import ComponentType , Context , Location
20+ from idom . web . module import export , module_from_file
2521
22+ from idom_router .compilers import compile_starlette_route
23+ from idom_router .types import Route , RouteCompiler , RoutePattern
2624
27- def compile_starlette_route (route : str ) -> RoutePattern :
28- pattern , _ , converters = _compile_starlette_path (route )
29- return RoutePattern (pattern , {k : v .convert for k , v in converters .items ()})
25+ R = TypeVar ("R" , bound = Route )
3026
3127
3228@component
3329def router (
34- * routes : Route ,
35- compiler : RouteCompiler = compile_starlette_route ,
30+ * routes : R ,
31+ compiler : RouteCompiler [ R ] = compile_starlette_route ,
3632) -> ComponentType | None :
3733 old_conn = use_connection ()
3834 location , set_location = use_state (old_conn .location )
3935
40- compiled_routes = use_memo (
41- lambda : [(compiler (r ), e ) for r , e in _iter_routes (routes )],
42- dependencies = routes ,
43- )
44- for compiled_route , element in compiled_routes :
45- match = compiled_route .pattern .match (location .pathname )
46- if match :
47- convs = compiled_route .converters
48- return ConnectionContext (
49- _route_state_context (
50- element ,
51- value = _RouteState (
52- set_location ,
53- {
54- k : convs [k ](v ) if k in convs else v
55- for k , v in match .groupdict ().items ()
56- },
57- ),
58- ),
59- value = Connection (old_conn .scope , location , old_conn .carrier ),
60- key = compiled_route .pattern .pattern ,
61- )
36+ # Memoize the compiled routes and the match separately so that we don't
37+ # recompile the routes on renders where only the location has changed
38+ compiled_routes = use_memo (lambda : _compile_routes (routes , compiler ))
39+ match = use_memo (lambda : _match_route (compiled_routes , location ))
40+
41+ if match is not None :
42+ route , params = match
43+ return ConnectionContext (
44+ _route_state_context (
45+ route .element , value = _RouteState (set_location , params )
46+ ),
47+ value = Connection (old_conn .scope , location , old_conn .carrier ),
48+ key = route .path ,
49+ )
50+
6251 return None
6352
6453
6554@component
66- def link (* attributes_or_children : VdomAttributesAndChildren , to : str ) -> VdomDict :
67- attributes , children = coalesce_attributes_and_children (attributes_or_children )
55+ def link (* children : VdomChild , to : str , ** attributes : Any ) -> VdomDict :
6856 set_location = _use_route_state ().set_location
6957 attrs = {
7058 ** attributes ,
@@ -76,7 +64,7 @@ def link(*attributes_or_children: VdomAttributesAndChildren, to: str) -> VdomDic
7664
7765def use_params () -> dict [str , Any ]:
7866 """Get parameters from the currently matching route pattern"""
79- return use_context ( _route_state_context ).params
67+ return _use_route_state ( ).params
8068
8169
8270def use_query (
@@ -97,15 +85,27 @@ def use_query(
9785 )
9886
9987
100- def _use_route_state () -> _RouteState :
101- return use_context (_route_state_context )
88+ def _compile_routes (
89+ routes : Sequence [R ], compiler : RouteCompiler [R ]
90+ ) -> list [tuple [Any , RoutePattern ]]:
91+ return [(r , compiler (r )) for r in _iter_routes (routes )]
92+
93+
94+ def _iter_routes (routes : Sequence [R ]) -> Iterator [R ]:
95+ for parent in routes :
96+ for child in _iter_routes (parent .routes ):
97+ yield replace (child , path = parent .path + child .path )
98+ yield parent
10299
103100
104- def _iter_routes (routes : Sequence [Route ]) -> Iterator [tuple [str , Any ]]:
105- for r in routes :
106- for path , element in _iter_routes (r .routes ):
107- yield r .path + path , element
108- yield r .path , r .element
101+ def _match_route (
102+ compiled_routes : list [tuple [R , RoutePattern ]], location : Location
103+ ) -> tuple [R , dict [str , Any ]] | None :
104+ for route , pattern in compiled_routes :
105+ params = pattern .match (location .pathname )
106+ if params is not None : # explicitely None check (could be empty dict)
107+ return route , params
108+ return None
109109
110110
111111_link = export (
@@ -120,4 +120,10 @@ class _RouteState:
120120 params : dict [str , Any ]
121121
122122
123+ def _use_route_state () -> _RouteState :
124+ route_state = use_context (_route_state_context )
125+ assert route_state is not None
126+ return route_state
127+
128+
123129_route_state_context : Context [_RouteState | None ] = create_context (None )
0 commit comments