2
2
3
3
import re
4
4
from dataclasses import dataclass
5
- from fnmatch import translate as fnmatch_translate
6
5
from pathlib import Path
7
- from typing import Any , Callable , Iterator , Sequence
6
+ from typing import Any , Callable , Iterator , Sequence , TypeVar , overload
7
+ from urllib .parse import parse_qs
8
8
9
- from idom import component , create_context , use_context , use_state
9
+ from idom import component , create_context , use_context , use_memo , use_state
10
10
from idom .core .types import VdomAttributesAndChildren , VdomDict
11
11
from idom .core .vdom import coalesce_attributes_and_children
12
12
from idom .types import BackendImplementation , ComponentType , Context , Location
13
13
from idom .web .module import export , module_from_file
14
+ from starlette .routing import compile_path
14
15
15
16
try :
16
17
from typing import Protocol
17
- except ImportError :
18
+ except ImportError : # pragma: no cover
18
19
from typing_extensions import Protocol
19
20
20
21
21
- class Routes (Protocol ):
22
+ class RoutesConstructor (Protocol ):
22
23
def __call__ (self , * routes : Route ) -> ComponentType :
23
24
...
24
25
25
26
26
27
def configure (
27
28
implementation : BackendImplementation [Any ] | Callable [[], Location ]
28
- ) -> Routes :
29
+ ) -> RoutesConstructor :
29
30
if isinstance (implementation , BackendImplementation ):
30
31
use_location = implementation .use_location
31
32
elif callable (implementation ):
32
33
use_location = implementation
33
34
else :
34
35
raise TypeError (
35
- "Expected a BackendImplementation or "
36
- f"` use_location` hook, not { implementation } "
36
+ "Expected a ' BackendImplementation' or "
37
+ f"' use_location' hook, not { implementation } "
37
38
)
38
39
39
40
@component
40
- def Router (* routes : Route ) -> ComponentType | None :
41
+ def routes (* routes : Route | Sequence [ Route ] ) -> ComponentType | None :
41
42
initial_location = use_location ()
42
43
location , set_location = use_state (initial_location )
43
- for p , r in _compile_routes (routes ):
44
- match = p .match (location .pathname )
44
+ compiled_routes = use_memo (lambda : _compile_routes (routes ), dependencies = routes )
45
+ for r in compiled_routes :
46
+ match = r .pattern .match (location .pathname )
45
47
if match :
46
48
return _LocationStateContext (
47
49
r .element ,
48
- value = _LocationState (location , set_location , match ),
49
- key = p .pattern ,
50
+ value = _LocationState (
51
+ location ,
52
+ set_location ,
53
+ {k : r .converters [k ](v ) for k , v in match .groupdict ().items ()},
54
+ ),
55
+ key = r .pattern .pattern ,
50
56
)
51
57
return None
52
58
53
- return Router
54
-
55
-
56
- def use_location () -> Location :
57
- return _use_location_state ().location
58
-
59
-
60
- def use_match () -> re .Match [str ]:
61
- return _use_location_state ().match
59
+ return routes
62
60
63
61
64
62
@dataclass
65
63
class Route :
66
- path : str | re . Pattern [ str ]
64
+ path : str
67
65
element : Any
66
+ routes : Sequence [Route ]
67
+
68
+ def __init__ (self , path : str , element : Any | None , * routes : Route ) -> None :
69
+ self .path = path
70
+ self .element = element
71
+ self .routes = routes
68
72
69
73
70
74
@component
71
- def Link (* attributes_or_children : VdomAttributesAndChildren , to : str ) -> VdomDict :
75
+ def link (* attributes_or_children : VdomAttributesAndChildren , to : str ) -> VdomDict :
72
76
attributes , children = coalesce_attributes_and_children (attributes_or_children )
73
77
set_location = _use_location_state ().set_location
74
78
attrs = {
@@ -79,15 +83,54 @@ def Link(*attributes_or_children: VdomAttributesAndChildren, to: str) -> VdomDic
79
83
return _Link (attrs , * children )
80
84
81
85
82
- def _compile_routes (routes : Sequence [Route ]) -> Iterator [tuple [re .Pattern [str ], Route ]]:
86
+ def use_location () -> Location :
87
+ """Get the current route location"""
88
+ return _use_location_state ().location
89
+
90
+
91
+ def use_params () -> dict [str , Any ]:
92
+ """Get parameters from the currently matching route pattern"""
93
+ return _use_location_state ().params
94
+
95
+
96
+ def use_query (
97
+ keep_blank_values : bool = False ,
98
+ strict_parsing : bool = False ,
99
+ errors : str = "replace" ,
100
+ max_num_fields : int | None = None ,
101
+ separator : str = "&" ,
102
+ ) -> dict [str , list [str ]]:
103
+ """See :func:`urllib.parse.parse_qs` for parameter info."""
104
+ return parse_qs (
105
+ use_location ().search ,
106
+ keep_blank_values = keep_blank_values ,
107
+ strict_parsing = strict_parsing ,
108
+ errors = errors ,
109
+ max_num_fields = max_num_fields ,
110
+ separator = separator ,
111
+ )
112
+
113
+
114
+ def _compile_routes (routes : Sequence [Route ]) -> list [_CompiledRoute ]:
115
+ for path , element in _iter_routes (routes ):
116
+ pattern , _ , converters = compile_path (path )
117
+ yield _CompiledRoute (
118
+ pattern , {k : v .convert for k , v in converters .items ()}, element
119
+ )
120
+
121
+
122
+ def _iter_routes (routes : Sequence [Route ]) -> Iterator [tuple [str , Any ]]:
83
123
for r in routes :
84
- if isinstance (r .path , re .Pattern ):
85
- yield r .path , r
86
- continue
87
- if not r .path .startswith ("/" ):
88
- raise ValueError ("Path pattern must begin with '/'" )
89
- pattern = re .compile (fnmatch_translate (r .path ))
90
- yield pattern , r
124
+ for path , element in _iter_routes (r .routes ):
125
+ yield r .path + path , element
126
+ yield r .path , r .element
127
+
128
+
129
+ @dataclass
130
+ class _CompiledRoute :
131
+ pattern : re .Pattern [str ]
132
+ converters : dict [str , Callable [[Any ], Any ]]
133
+ element : Any
91
134
92
135
93
136
def _use_location_state () -> _LocationState :
@@ -100,7 +143,7 @@ def _use_location_state() -> _LocationState:
100
143
class _LocationState :
101
144
location : Location
102
145
set_location : Callable [[Location ], None ]
103
- match : re . Match [str ]
146
+ params : dict [str , Any ]
104
147
105
148
106
149
_LocationStateContext : Context [_LocationState | None ] = create_context (None )
0 commit comments