Skip to content

Commit 2f8f853

Browse files
committed
remove starlette as dep
1 parent 540d0ce commit 2f8f853

File tree

10 files changed

+195
-201
lines changed

10 files changed

+195
-201
lines changed

Diff for: idom_router/__init__.py

+8-6
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,20 @@
11
# the version is statically loaded by setup.py
22
__version__ = "0.0.1"
33

4-
from idom_router.types import Route, RouteCompiler, RouteResolver
4+
from . import simple
5+
from .core import create_router, link, route, router_component, use_params, use_query
6+
from .types import Route, RouteCompiler, RouteResolver
57

6-
from .core import link, create_router, router_component, use_params, use_query
7-
8-
__all__ = [
8+
__all__ = (
99
"create_router",
1010
"link",
11+
"route",
12+
"route",
1113
"Route",
1214
"RouteCompiler",
1315
"router_component",
1416
"RouteResolver",
15-
"use_location",
17+
"simple",
1618
"use_params",
1719
"use_query",
18-
]
20+
)

Diff for: idom_router/core.py

+19-27
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,15 @@
1919
from idom.types import ComponentType, Context, Location
2020
from idom.web.module import export, module_from_file
2121

22-
from idom_router.types import Route, RouteCompiler, RouteResolver, Router
22+
from idom_router.types import Route, RouteCompiler, Router, RouteResolver
2323

2424
R = TypeVar("R", bound=Route)
2525

2626

27+
def route(path: str, element: Any | None, *routes: Route) -> Route:
28+
return Route(path, element, routes)
29+
30+
2731
def create_router(compiler: RouteCompiler[R]) -> Router[R]:
2832
"""A decorator that turns a route compiler into a router"""
2933

@@ -40,25 +44,19 @@ def router_component(
4044
) -> ComponentType | None:
4145
old_conn = use_connection()
4246
location, set_location = use_state(old_conn.location)
43-
router_state = use_context(_route_state_context)
44-
4547

46-
if router_state is not None:
47-
raise RuntimeError("Another router is already active in this context")
48+
resolvers = use_memo(
49+
lambda: tuple(map(compiler, _iter_routes(routes))),
50+
dependencies=(compiler, hash(routes)),
51+
)
4852

49-
# Memoize the compiled routes and the match separately so that we don't
50-
# recompile the routes on renders where only the location has changed
51-
compiled_routes = use_memo(lambda: _compile_routes(routes, compiler))
52-
match = use_memo(lambda: _match_route(compiled_routes, location))
53+
match = use_memo(lambda: _match_route(resolvers, location))
5354

5455
if match is not None:
55-
route, params = match
56+
element, params = match
5657
return ConnectionContext(
57-
_route_state_context(
58-
route.element, value=_RouteState(set_location, params)
59-
),
58+
_route_state_context(element, value=_RouteState(set_location, params)),
6059
value=Connection(old_conn.scope, location, old_conn.carrier),
61-
key=route.path,
6260
)
6361

6462
return None
@@ -98,26 +96,20 @@ def use_query(
9896
)
9997

10098

101-
def _compile_routes(
102-
routes: Sequence[R], compiler: RouteCompiler[R]
103-
) -> list[tuple[Any, RouteResolver]]:
104-
return [(r, compiler(r)) for r in _iter_routes(routes)]
105-
106-
10799
def _iter_routes(routes: Sequence[R]) -> Iterator[R]:
108100
for parent in routes:
109101
for child in _iter_routes(parent.routes):
110-
yield replace(child, path=parent.path + child.path)
102+
yield replace(child, path=parent.path + child.path) # type: ignore[misc]
111103
yield parent
112104

113105

114106
def _match_route(
115-
compiled_routes: list[tuple[R, RouteResolver]], location: Location
116-
) -> tuple[R, dict[str, Any]] | None:
117-
for route, pattern in compiled_routes:
118-
params = pattern.match(location.pathname)
119-
if params is not None: # explicitely None check (could be empty dict)
120-
return route, params
107+
compiled_routes: Sequence[RouteResolver], location: Location
108+
) -> tuple[Any, dict[str, Any]] | None:
109+
for resolver in compiled_routes:
110+
match = resolver.resolve(location.pathname)
111+
if match is not None:
112+
return match
121113
return None
122114

123115

Diff for: idom_router/routers/__init__.py

Whitespace-only changes.

Diff for: idom_router/routers/regex.py

-50
This file was deleted.

Diff for: idom_router/routers/starlette.py

-38
This file was deleted.

Diff for: idom_router/simple.py

+86
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
from __future__ import annotations
2+
3+
import re
4+
import uuid
5+
from typing import Any, Callable
6+
7+
from typing_extensions import TypeAlias, TypedDict
8+
9+
from idom_router.core import create_router
10+
from idom_router.types import Route
11+
12+
__all__ = ["router"]
13+
14+
ConversionFunc: TypeAlias = "Callable[[str], Any]"
15+
ConverterMapping: TypeAlias = "dict[str, ConversionFunc]"
16+
17+
PARAM_REGEX = re.compile(r"{(?P<name>\w+)(?P<type>:\w+)?}")
18+
19+
20+
class SimpleResolver:
21+
def __init__(self, route: Route) -> None:
22+
self.element = route.element
23+
self.pattern, self.converters = parse_path(route.path)
24+
self.key = self.pattern.pattern
25+
26+
def resolve(self, path: str) -> tuple[Any, dict[str, Any]] | None:
27+
print(path)
28+
print(self.key)
29+
match = self.pattern.match(path)
30+
if match:
31+
return (
32+
self.element,
33+
{k: self.converters[k](v) for k, v in match.groupdict().items()},
34+
)
35+
return None
36+
37+
38+
def parse_path(path: str) -> tuple[re.Pattern[str], ConverterMapping]:
39+
pattern = "^"
40+
last_match_end = 0
41+
converters: ConverterMapping = {}
42+
for match in PARAM_REGEX.finditer(path):
43+
param_name = match.group("name")
44+
param_type = (match.group("type") or "str").lstrip(":")
45+
try:
46+
param_conv = CONVERSION_TYPES[param_type]
47+
except KeyError:
48+
raise ValueError(f"Unknown conversion type {param_type!r} in {path!r}")
49+
pattern += re.escape(path[last_match_end : match.start()])
50+
pattern += f"(?P<{param_name}>{param_conv['regex']})"
51+
converters[param_name] = param_conv["func"]
52+
last_match_end = match.end()
53+
pattern += re.escape(path[last_match_end:]) + "$"
54+
return re.compile(pattern), converters
55+
56+
57+
class ConversionInfo(TypedDict):
58+
regex: str
59+
func: ConversionFunc
60+
61+
62+
CONVERSION_TYPES: dict[str, ConversionInfo] = {
63+
"str": {
64+
"regex": r"[^/]+",
65+
"func": str,
66+
},
67+
"int": {
68+
"regex": r"\d+",
69+
"func": int,
70+
},
71+
"float": {
72+
"regex": r"\d+(\.\d+)?",
73+
"func": float,
74+
},
75+
"uuid": {
76+
"regex": r"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}",
77+
"func": uuid.UUID,
78+
},
79+
"path": {
80+
"regex": r".+",
81+
"func": str,
82+
},
83+
}
84+
85+
86+
router = create_router(SimpleResolver)

Diff for: idom_router/types.py

+14-20
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,33 @@
11
from __future__ import annotations
22

3-
from dataclasses import dataclass
3+
from dataclasses import dataclass, field
44
from typing import Any, Sequence, TypeVar
55

6-
from idom.types import Key, ComponentType
6+
from idom.core.vdom import is_vdom
7+
from idom.types import ComponentType, Key
78
from typing_extensions import Protocol, Self
89

910

10-
@dataclass
11+
@dataclass(frozen=True)
1112
class Route:
1213
path: str
13-
element: Any
14+
element: Any = field(hash=False)
1415
routes: Sequence[Self]
1516

16-
def __init__(
17-
self,
18-
path: str,
19-
element: Any | None,
20-
*routes_: Self,
21-
# we need kwarg in order to play nice with the expected dataclass interface
22-
routes: Sequence[Self] = (),
23-
) -> None:
24-
self.path = path
25-
self.element = element
26-
self.routes = (*routes_, *routes)
27-
28-
29-
class Router(Protocol):
30-
def __call__(self, *routes: Route) -> ComponentType:
31-
"""Return a component that renders the first matching route"""
17+
def __hash__(self) -> int:
18+
el = self.element
19+
key = el["key"] if is_vdom(el) and "key" in el else getattr(el, "key", id(el))
20+
return hash((self.path, key, self.routes))
3221

3322

3423
R = TypeVar("R", bound=Route, contravariant=True)
3524

3625

26+
class Router(Protocol[R]):
27+
def __call__(self, *routes: R) -> ComponentType:
28+
"""Return a component that renders the first matching route"""
29+
30+
3731
class RouteCompiler(Protocol[R]):
3832
def __call__(self, route: R) -> RouteResolver:
3933
"""Compile a route into a resolver that can be matched against a path"""

Diff for: requirements/pkg-deps.txt

-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,2 @@
11
idom >=1
22
typing_extensions
3-
starlette

0 commit comments

Comments
 (0)