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 eec38db

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

File tree

4 files changed

+231
-60
lines changed
Filter options

4 files changed

+231
-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
+78-33Lines changed: 78 additions & 33 deletions
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)

‎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.