Skip to content

Commit 212d1fc

Browse files
committed
improve coverage
1 parent 87e7e75 commit 212d1fc

File tree

4 files changed

+229
-60
lines changed

4 files changed

+229
-60
lines changed

Diff for: idom_router/__init__.py

+13-3
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
]

Diff for: idom_router/router.py

+76-33
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)

Diff for: requirements/pkg-deps.txt

+1
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)