Skip to content

Commit eec38db

Browse files
committed
improve coverage
1 parent 87e7e75 commit eec38db

File tree

4 files changed

+231
-60
lines changed

4 files changed

+231
-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

+78-33
Original file line numberDiff line numberDiff line change
@@ -2,73 +2,79 @@
22

33
import re
44
from dataclasses import dataclass
5-
from fnmatch import translate as fnmatch_translate
65
from pathlib import Path
76
from typing import Any, Callable, Iterator, Sequence
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-
from typing_extensions import Protocol
18+
except ImportError: # pragma: no cover
19+
from typing_extensions import Protocol # type: ignore
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) -> 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(
45+
lambda: _iter_compile_routes(routes), dependencies=routes
46+
)
47+
for r in compiled_routes:
48+
match = r.pattern.match(location.pathname)
4549
if match:
4650
return _LocationStateContext(
4751
r.element,
48-
value=_LocationState(location, set_location, match),
49-
key=p.pattern,
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,
5058
)
5159
return None
5260

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
61+
return routes
6262

6363

6464
@dataclass
6565
class Route:
66-
path: str | re.Pattern[str]
66+
path: str
6767
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
6874

6975

7076
@component
71-
def Link(*attributes_or_children: VdomAttributesAndChildren, to: str) -> VdomDict:
77+
def link(*attributes_or_children: VdomAttributesAndChildren, to: str) -> VdomDict:
7278
attributes, children = coalesce_attributes_and_children(attributes_or_children)
7379
set_location = _use_location_state().set_location
7480
attrs = {
@@ -79,15 +85,54 @@ def Link(*attributes_or_children: VdomAttributesAndChildren, to: str) -> VdomDic
7985
return _Link(attrs, *children)
8086

8187

82-
def _compile_routes(routes: Sequence[Route]) -> Iterator[tuple[re.Pattern[str], Route]]:
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,
113+
)
114+
115+
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]]:
83125
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
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
91136

92137

93138
def _use_location_state() -> _LocationState:
@@ -100,7 +145,7 @@ def _use_location_state() -> _LocationState:
100145
class _LocationState:
101146
location: Location
102147
set_location: Callable[[Location], None]
103-
match: re.Match[str]
148+
params: dict[str, Any]
104149

105150

106151
_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)