Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 5d7170c

Browse filesBrowse files
committed
rework route compiler interface
This allows compilers to work in a wider variety of ways.
1 parent 6d7254f commit 5d7170c
Copy full SHA for 5d7170c

File tree

Expand file treeCollapse file tree

7 files changed

+186
-97
lines changed
Filter options
Expand file treeCollapse file tree

7 files changed

+186
-97
lines changed

‎idom_router/__init__.py

Copy file name to clipboardExpand all lines: idom_router/__init__.py
+6-9Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,15 @@
11
# the version is statically loaded by setup.py
22
__version__ = "0.0.1"
33

4-
from .router import (
5-
Route,
6-
link,
7-
router,
8-
use_location,
9-
use_params,
10-
use_query,
11-
)
4+
from idom_router.types import Route, RouteCompiler, RoutePattern
5+
6+
from .router import link, router, use_params, use_query
127

138
__all__ = [
14-
"Route",
159
"link",
10+
"Route",
11+
"RouteCompiler",
12+
"RoutePattern",
1613
"router",
1714
"use_location",
1815
"use_params",

‎idom_router/compilers.py

Copy file name to clipboard
+34Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
from __future__ import annotations
2+
3+
import re
4+
from typing import Any
5+
6+
from starlette.convertors import Convertor
7+
from starlette.routing import compile_path as _compile_starlette_path
8+
9+
from idom_router.types import Route
10+
11+
12+
def compile_starlette_route(route: Route) -> StarletteRoutePattern:
13+
pattern, _, converters = _compile_starlette_path(route.path)
14+
return StarletteRoutePattern(pattern, converters)
15+
16+
17+
class StarletteRoutePattern:
18+
def __init__(
19+
self,
20+
pattern: re.Pattern[str],
21+
converters: dict[str, Convertor],
22+
) -> None:
23+
self.pattern = pattern
24+
self.key = pattern.pattern
25+
self.converters = converters
26+
27+
def match(self, path: str) -> dict[str, Any] | None:
28+
match = self.pattern.match(path)
29+
if match:
30+
return {
31+
k: self.converters[k].convert(v) if k in self.converters else v
32+
for k, v in match.groupdict().items()
33+
}
34+
return None

‎idom_router/router.py

Copy file name to clipboard
+54-48Lines changed: 54 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,70 +1,58 @@
11
from __future__ import annotations
22

3-
from dataclasses import dataclass
3+
from dataclasses import dataclass, replace
44
from pathlib import Path
5-
from typing import Any, Callable, Iterator, Sequence
5+
from typing import Any, Callable, Iterator, Sequence, TypeVar
66
from urllib.parse import parse_qs
77

88
from 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
2016
from idom.backend.hooks import ConnectionContext, use_connection
2117
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
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
3329
def 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

7765
def 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

8270
def 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)

‎idom_router/types.py

Copy file name to clipboard
+28-13Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,43 @@
11
from __future__ import annotations
22

3-
import re
43
from dataclasses import dataclass
5-
from typing import Callable, Any, Protocol, Sequence
4+
from typing import Any, Protocol, Sequence, TypeVar
5+
6+
from idom.types import Key
7+
from typing_extensions import Self
68

79

810
@dataclass
911
class Route:
1012
path: str
1113
element: Any
12-
routes: Sequence[Route]
13-
14-
def __init__(self, path: str, element: Any | None, *routes: Route) -> None:
14+
routes: Sequence[Self]
15+
16+
def __init__(
17+
self,
18+
path: str,
19+
element: Any | None,
20+
*route_args: Self,
21+
# we need kwarg in order to play nice with the expected dataclass interface
22+
routes: Sequence[Self] = (),
23+
) -> None:
1524
self.path = path
1625
self.element = element
17-
self.routes = routes
26+
self.routes = (*route_args, *routes)
1827

1928

20-
class RouteCompiler(Protocol):
21-
def __call__(self, route: str) -> RoutePattern:
22-
...
29+
R = TypeVar("R", bound=Route, contravariant=True)
2330

2431

25-
@dataclass
26-
class RoutePattern:
27-
pattern: re.Pattern[str]
28-
converters: dict[str, Callable[[Any], Any]]
32+
class RouteCompiler(Protocol[R]):
33+
def __call__(self, route: R) -> RoutePattern:
34+
"""Compile a route into a pattern that can be matched against a path"""
35+
36+
37+
class RoutePattern(Protocol):
38+
@property
39+
def key(self) -> Key:
40+
"""Uniquely identified this pattern"""
41+
42+
def match(self, path: str) -> dict[str, Any] | None:
43+
"""Returns otherwise a dict of path parameters if the path matches, else None"""

‎requirements/pkg-deps.txt

Copy file name to clipboard
+1-1Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
idom >=0.40.2,<0.41
1+
idom >=1
22
typing_extensions
33
starlette

‎tests/test_router.py

Copy file name to clipboardExpand all lines: tests/test_router.py
+14-26Lines changed: 14 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,8 @@
1-
import re
2-
31
from idom import Ref, component, html, use_location
42
from idom.testing import DisplayFixture
53

6-
from idom_router import (
7-
Route,
8-
router,
9-
link,
10-
use_params,
11-
use_query,
12-
)
13-
from idom_router.types import RoutePattern
4+
from idom_router import Route, link, router, use_params, use_query
5+
from tests.utils import compile_simple_regex_route
146

157

168
async def test_simple_router(display: DisplayFixture):
@@ -87,10 +79,10 @@ async def test_navigate_with_link(display: DisplayFixture):
8779
def sample():
8880
render_count.current += 1
8981
return router(
90-
Route("/", link({"id": "root"}, "Root", to="/a")),
91-
Route("/a", link({"id": "a"}, "A", to="/b")),
92-
Route("/b", link({"id": "b"}, "B", to="/c")),
93-
Route("/c", link({"id": "c"}, "C", to="/default")),
82+
Route("/", link("Root", to="/a", id="root")),
83+
Route("/a", link("A", to="/b", id="a")),
84+
Route("/b", link("B", to="/c", id="b")),
85+
Route("/c", link("C", to="/default", id="c")),
9486
Route("/{path:path}", html.h1({"id": "default"}, "Default")),
9587
)
9688

@@ -174,33 +166,29 @@ def check_params():
174166
def sample():
175167
return router(
176168
Route(
177-
r"/first/(?P<first>\d+)",
169+
r"/first/(?P<first__int>\d+)",
178170
check_params(),
179171
Route(
180-
r"/second/(?P<second>[\d\.]+)",
172+
r"/second/(?P<second__float>[\d\.]+)",
181173
check_params(),
182174
Route(
183-
r"/third/(?P<third>[\d,]+)",
175+
r"/third/(?P<third__list>[\d,]+)",
184176
check_params(),
185177
),
186178
),
187179
),
188-
compiler=lambda path: RoutePattern(
189-
re.compile(rf"^{path}$"),
190-
{
191-
"first": int,
192-
"second": float,
193-
"third": lambda s: list(map(int, s.split(","))),
194-
},
195-
),
180+
compiler=compile_simple_regex_route,
196181
)
197182

198183
await display.show(sample)
199184

200185
for path, expected_params in [
201186
("/first/1", {"first": 1}),
202187
("/first/1/second/2.1", {"first": 1, "second": 2.1}),
203-
("/first/1/second/2.1/third/3,3", {"first": 1, "second": 2.1, "third": [3, 3]}),
188+
(
189+
"/first/1/second/2.1/third/3,3",
190+
{"first": 1, "second": 2.1, "third": ["3", "3"]},
191+
),
204192
]:
205193
await display.goto(path)
206194
await display.page.wait_for_selector("#success")

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.