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 212d1fc

Browse filesBrowse files
committed
improve coverage
1 parent 87e7e75 commit 212d1fc
Copy full SHA for 212d1fc

File tree

4 files changed

+229
-60
lines changed
Filter options

4 files changed

+229
-60
lines changed

‎idom_router/__init__.py

Copy file name to clipboard
+13-3Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,22 @@
11
# the version is statically loaded by setup.py
22
__version__ = "0.0.1"
33

4-
from .router import Link, Route, Routes, configure, use_location
4+
from .router import (
5+
Route,
6+
RoutesConstructor,
7+
configure,
8+
link,
9+
use_location,
10+
use_params,
11+
use_query,
12+
)
513

614
__all__ = [
715
"configure",
8-
"Link",
16+
"link",
917
"Route",
10-
"Routes",
18+
"RoutesConstructor",
1119
"use_location",
20+
"use_params",
21+
"use_query",
1222
]

‎idom_router/router.py

Copy file name to clipboardExpand all lines: idom_router/router.py
+76-33Lines changed: 76 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -2,73 +2,77 @@
22

33
import re
44
from dataclasses import dataclass
5-
from fnmatch import translate as fnmatch_translate
65
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
88

9-
from idom import component, create_context, use_context, use_state
9+
from idom import component, create_context, use_context, use_memo, use_state
1010
from idom.core.types import VdomAttributesAndChildren, VdomDict
1111
from idom.core.vdom import coalesce_attributes_and_children
1212
from idom.types import BackendImplementation, ComponentType, Context, Location
1313
from idom.web.module import export, module_from_file
14+
from starlette.routing import compile_path
1415

1516
try:
1617
from typing import Protocol
17-
except ImportError:
18+
except ImportError: # pragma: no cover
1819
from typing_extensions import Protocol
1920

2021

21-
class Routes(Protocol):
22+
class RoutesConstructor(Protocol):
2223
def __call__(self, *routes: Route) -> ComponentType:
2324
...
2425

2526

2627
def configure(
2728
implementation: BackendImplementation[Any] | Callable[[], Location]
28-
) -> Routes:
29+
) -> RoutesConstructor:
2930
if isinstance(implementation, BackendImplementation):
3031
use_location = implementation.use_location
3132
elif callable(implementation):
3233
use_location = implementation
3334
else:
3435
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}"
3738
)
3839

3940
@component
40-
def Router(*routes: Route) -> ComponentType | None:
41+
def routes(*routes: Route | Sequence[Route]) -> ComponentType | None:
4142
initial_location = use_location()
4243
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)
4547
if match:
4648
return _LocationStateContext(
4749
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,
5056
)
5157
return None
5258

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
6260

6361

6462
@dataclass
6563
class Route:
66-
path: str | re.Pattern[str]
64+
path: str
6765
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
6872

6973

7074
@component
71-
def Link(*attributes_or_children: VdomAttributesAndChildren, to: str) -> VdomDict:
75+
def link(*attributes_or_children: VdomAttributesAndChildren, to: str) -> VdomDict:
7276
attributes, children = coalesce_attributes_and_children(attributes_or_children)
7377
set_location = _use_location_state().set_location
7478
attrs = {
@@ -79,15 +83,54 @@ def Link(*attributes_or_children: VdomAttributesAndChildren, to: str) -> VdomDic
7983
return _Link(attrs, *children)
8084

8185

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]]:
83123
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
91134

92135

93136
def _use_location_state() -> _LocationState:
@@ -100,7 +143,7 @@ def _use_location_state() -> _LocationState:
100143
class _LocationState:
101144
location: Location
102145
set_location: Callable[[Location], None]
103-
match: re.Match[str]
146+
params: dict[str, Any]
104147

105148

106149
_LocationStateContext: Context[_LocationState | None] = create_context(None)

‎requirements/pkg-deps.txt

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

0 commit comments

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