1
1
from __future__ import annotations
2
2
3
- from dataclasses import dataclass
3
+ from dataclasses import dataclass , replace
4
4
from pathlib import Path
5
- from typing import Any , Callable , Iterator , Sequence
5
+ from typing import Any , Callable , Iterator , Sequence , TypeVar
6
6
from urllib .parse import parse_qs
7
7
8
8
from idom import (
9
9
component ,
10
10
create_context ,
11
- use_memo ,
12
- use_state ,
13
11
use_context ,
14
12
use_location ,
13
+ use_memo ,
14
+ use_state ,
15
15
)
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
20
16
from idom .backend .hooks import ConnectionContext , use_connection
21
17
from 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
25
21
22
+ from idom_router .compilers import compile_starlette_route
23
+ from idom_router .types import Route , RouteCompiler , RoutePattern
26
24
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 )
30
26
31
27
32
28
@component
33
29
def router (
34
- * routes : Route ,
35
- compiler : RouteCompiler = compile_starlette_route ,
30
+ * routes : R ,
31
+ compiler : RouteCompiler [ R ] = compile_starlette_route ,
36
32
) -> ComponentType | None :
37
33
old_conn = use_connection ()
38
34
location , set_location = use_state (old_conn .location )
39
35
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
+
62
51
return None
63
52
64
53
65
54
@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 :
68
56
set_location = _use_route_state ().set_location
69
57
attrs = {
70
58
** attributes ,
@@ -76,7 +64,7 @@ def link(*attributes_or_children: VdomAttributesAndChildren, to: str) -> VdomDic
76
64
77
65
def use_params () -> dict [str , Any ]:
78
66
"""Get parameters from the currently matching route pattern"""
79
- return use_context ( _route_state_context ).params
67
+ return _use_route_state ( ).params
80
68
81
69
82
70
def use_query (
@@ -97,15 +85,27 @@ def use_query(
97
85
)
98
86
99
87
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
102
99
103
100
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
109
109
110
110
111
111
_link = export (
@@ -120,4 +120,10 @@ class _RouteState:
120
120
params : dict [str , Any ]
121
121
122
122
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
+
123
129
_route_state_context : Context [_RouteState | None ] = create_context (None )
0 commit comments