Skip to content

Commit

Permalink
remove starlette as dep
Browse files Browse the repository at this point in the history
  • Loading branch information
rmorshea committed May 10, 2023
1 parent 540d0ce commit 2f8f853
Show file tree
Hide file tree
Showing 10 changed files with 195 additions and 201 deletions.
14 changes: 8 additions & 6 deletions idom_router/__init__.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
# the version is statically loaded by setup.py
__version__ = "0.0.1"

from idom_router.types import Route, RouteCompiler, RouteResolver
from . import simple
from .core import create_router, link, route, router_component, use_params, use_query
from .types import Route, RouteCompiler, RouteResolver

from .core import link, create_router, router_component, use_params, use_query

__all__ = [
__all__ = (
"create_router",
"link",
"route",
"route",
"Route",
"RouteCompiler",
"router_component",
"RouteResolver",
"use_location",
"simple",
"use_params",
"use_query",
]
)
46 changes: 19 additions & 27 deletions idom_router/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,15 @@
from idom.types import ComponentType, Context, Location
from idom.web.module import export, module_from_file

from idom_router.types import Route, RouteCompiler, RouteResolver, Router
from idom_router.types import Route, RouteCompiler, Router, RouteResolver

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


def route(path: str, element: Any | None, *routes: Route) -> Route:
return Route(path, element, routes)


def create_router(compiler: RouteCompiler[R]) -> Router[R]:
"""A decorator that turns a route compiler into a router"""

Expand All @@ -40,25 +44,19 @@ def router_component(
) -> ComponentType | None:
old_conn = use_connection()
location, set_location = use_state(old_conn.location)
router_state = use_context(_route_state_context)


if router_state is not None:
raise RuntimeError("Another router is already active in this context")
resolvers = use_memo(
lambda: tuple(map(compiler, _iter_routes(routes))),
dependencies=(compiler, hash(routes)),
)

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

if match is not None:
route, params = match
element, params = match
return ConnectionContext(
_route_state_context(
route.element, value=_RouteState(set_location, params)
),
_route_state_context(element, value=_RouteState(set_location, params)),
value=Connection(old_conn.scope, location, old_conn.carrier),
key=route.path,
)

return None
Expand Down Expand Up @@ -98,26 +96,20 @@ def use_query(
)


def _compile_routes(
routes: Sequence[R], compiler: RouteCompiler[R]
) -> list[tuple[Any, RouteResolver]]:
return [(r, compiler(r)) for r in _iter_routes(routes)]


def _iter_routes(routes: Sequence[R]) -> Iterator[R]:
for parent in routes:
for child in _iter_routes(parent.routes):
yield replace(child, path=parent.path + child.path)
yield replace(child, path=parent.path + child.path) # type: ignore[misc]
yield parent


def _match_route(
compiled_routes: list[tuple[R, RouteResolver]], location: Location
) -> tuple[R, dict[str, Any]] | None:
for route, pattern in compiled_routes:
params = pattern.match(location.pathname)
if params is not None: # explicitely None check (could be empty dict)
return route, params
compiled_routes: Sequence[RouteResolver], location: Location
) -> tuple[Any, dict[str, Any]] | None:
for resolver in compiled_routes:
match = resolver.resolve(location.pathname)
if match is not None:
return match
return None


Expand Down
Empty file removed idom_router/routers/__init__.py
Empty file.
50 changes: 0 additions & 50 deletions idom_router/routers/regex.py

This file was deleted.

38 changes: 0 additions & 38 deletions idom_router/routers/starlette.py

This file was deleted.

86 changes: 86 additions & 0 deletions idom_router/simple.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
from __future__ import annotations

import re
import uuid
from typing import Any, Callable

from typing_extensions import TypeAlias, TypedDict

from idom_router.core import create_router
from idom_router.types import Route

__all__ = ["router"]

ConversionFunc: TypeAlias = "Callable[[str], Any]"
ConverterMapping: TypeAlias = "dict[str, ConversionFunc]"

PARAM_REGEX = re.compile(r"{(?P<name>\w+)(?P<type>:\w+)?}")


class SimpleResolver:
def __init__(self, route: Route) -> None:
self.element = route.element
self.pattern, self.converters = parse_path(route.path)
self.key = self.pattern.pattern

def resolve(self, path: str) -> tuple[Any, dict[str, Any]] | None:
print(path)
print(self.key)
match = self.pattern.match(path)
if match:
return (
self.element,
{k: self.converters[k](v) for k, v in match.groupdict().items()},
)
return None


def parse_path(path: str) -> tuple[re.Pattern[str], ConverterMapping]:
pattern = "^"
last_match_end = 0
converters: ConverterMapping = {}
for match in PARAM_REGEX.finditer(path):
param_name = match.group("name")
param_type = (match.group("type") or "str").lstrip(":")
try:
param_conv = CONVERSION_TYPES[param_type]
except KeyError:
raise ValueError(f"Unknown conversion type {param_type!r} in {path!r}")
pattern += re.escape(path[last_match_end : match.start()])
pattern += f"(?P<{param_name}>{param_conv['regex']})"
converters[param_name] = param_conv["func"]
last_match_end = match.end()
pattern += re.escape(path[last_match_end:]) + "$"
return re.compile(pattern), converters


class ConversionInfo(TypedDict):
regex: str
func: ConversionFunc


CONVERSION_TYPES: dict[str, ConversionInfo] = {
"str": {
"regex": r"[^/]+",
"func": str,
},
"int": {
"regex": r"\d+",
"func": int,
},
"float": {
"regex": r"\d+(\.\d+)?",
"func": float,
},
"uuid": {
"regex": r"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}",
"func": uuid.UUID,
},
"path": {
"regex": r".+",
"func": str,
},
}


router = create_router(SimpleResolver)
34 changes: 14 additions & 20 deletions idom_router/types.py
Original file line number Diff line number Diff line change
@@ -1,39 +1,33 @@
from __future__ import annotations

from dataclasses import dataclass
from dataclasses import dataclass, field
from typing import Any, Sequence, TypeVar

from idom.types import Key, ComponentType
from idom.core.vdom import is_vdom
from idom.types import ComponentType, Key
from typing_extensions import Protocol, Self


@dataclass
@dataclass(frozen=True)
class Route:
path: str
element: Any
element: Any = field(hash=False)
routes: Sequence[Self]

def __init__(
self,
path: str,
element: Any | None,
*routes_: Self,
# we need kwarg in order to play nice with the expected dataclass interface
routes: Sequence[Self] = (),
) -> None:
self.path = path
self.element = element
self.routes = (*routes_, *routes)


class Router(Protocol):
def __call__(self, *routes: Route) -> ComponentType:
"""Return a component that renders the first matching route"""
def __hash__(self) -> int:
el = self.element
key = el["key"] if is_vdom(el) and "key" in el else getattr(el, "key", id(el))
return hash((self.path, key, self.routes))


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


class Router(Protocol[R]):
def __call__(self, *routes: R) -> ComponentType:
"""Return a component that renders the first matching route"""


class RouteCompiler(Protocol[R]):
def __call__(self, route: R) -> RouteResolver:
"""Compile a route into a resolver that can be matched against a path"""
Expand Down
1 change: 0 additions & 1 deletion requirements/pkg-deps.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
idom >=1
typing_extensions
starlette
Loading

0 comments on commit 2f8f853

Please sign in to comment.