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