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