Skip to content

Commit

Permalink
Route group (#24)
Browse files Browse the repository at this point in the history
* Revamp routes to be nested inside RouteGroup

* Cleanup some imports

* Pass all Sanic routing tests

* Passing all tests on main repo

* Fix type annotations
  • Loading branch information
ahopkins authored Apr 19, 2021
1 parent 7818338 commit e274b6c
Show file tree
Hide file tree
Showing 8 changed files with 434 additions and 201 deletions.
5 changes: 3 additions & 2 deletions sanic_routing/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from .group import RouteGroup
from .route import Route
from .router import BaseRouter

__version__ = "0.5.2"
__all__ = ("BaseRouter", "Route")
__version__ = "0.6.0"
__all__ = ("BaseRouter", "Route", "RouteGroup")
2 changes: 1 addition & 1 deletion sanic_routing/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ class BadMethod(BaseException):
class NoMethod(BaseException):
def __init__(
self,
message: str,
message: str = "Method does not exist",
method: Optional[str] = None,
allowed_methods: Optional[Set[str]] = None,
):
Expand Down
122 changes: 122 additions & 0 deletions sanic_routing/group.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
from sanic_routing.utils import Immutable

from .exceptions import InvalidUsage, RouteExists


class RouteGroup:
methods_index: Immutable

def __init__(self, *routes) -> None:
if len(set(route.parts for route in routes)) > 1:
raise InvalidUsage("Cannot group routes with differing paths")

if any(routes[-1].strict != route.strict for route in routes):
raise InvalidUsage("Cannot group routes with differing strictness")

route_list = list(routes)
route_list.pop()

self._routes = routes
self.pattern_idx = 0

def __str__(self):
display = (
f"path={self.path or self.router.delimiter} len={len(self.routes)}"
)
return f"<{self.__class__.__name__}: {display}>"

def __iter__(self):
return iter(self.routes)

def __getitem__(self, key):
return self.routes[key]

def finalize(self):
self.methods_index = Immutable(
{
method: route
for route in self._routes
for method in route.methods
}
)

def reset(self):
self.methods_index = dict(self.methods_index)

def merge(self, group, overwrite: bool = False, append: bool = False):
_routes = list(self._routes)
for other_route in group.routes:
for current_route in self:
if (
current_route == other_route
or (
current_route.requirements
and not other_route.requirements
)
or (
not current_route.requirements
and other_route.requirements
)
) and not append:
if not overwrite:
raise RouteExists(
f"Route already registered: {self.raw_path} "
f"[{','.join(self.methods)}]"
)
else:
_routes.append(other_route)
self._routes = tuple(_routes)

@property
def labels(self):
return self[0].labels

@property
def methods(self):
return frozenset(
[method for route in self for method in route.methods]
)

@property
def params(self):
return self[0].params

@property
def parts(self):
return self[0].parts

@property
def path(self):
return self[0].path

@property
def pattern(self):
return self[0].pattern

@property
def raw_path(self):
return self[0].raw_path

@property
def regex(self):
return self[0].regex

@property
def requirements(self):
return [route.requirements for route in self if route.requirements]

@property
def routes(self):
return self._routes

@property
def router(self):
return self[0].router

@property
def strict(self):
return self[0].strict

@property
def unquote(self):
return self[0].unquote
101 changes: 39 additions & 62 deletions sanic_routing/route.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import re
import typing as t
from collections import defaultdict, namedtuple
from collections import namedtuple
from types import SimpleNamespace

from .exceptions import InvalidUsage, ParameterNameConflicts, RouteExists
from .exceptions import InvalidUsage, ParameterNameConflicts
from .patterns import REGEX_TYPES
from .utils import Immutable, parts_to_path, path_to_parts

Expand All @@ -12,7 +12,7 @@
)


class Requirements(dict):
class Requirements(Immutable):
def __hash__(self):
return hash(frozenset(self.items()))

Expand All @@ -22,10 +22,11 @@ class Route:
"_params",
"_raw_path",
"ctx",
"handlers",
"handler",
"labels",
"methods",
"name",
"overloaded",
"params",
"parts",
"path",
Expand All @@ -36,14 +37,16 @@ class Route:
"static",
"strict",
"unquote",
"overloaded",
)

def __init__(
self,
router,
raw_path: str,
name: str,
handler: t.Callable[..., t.Any],
methods: t.Iterable[str],
requirements: t.Dict[str, t.Any] = None,
strict: bool = False,
unquote: bool = False,
static: bool = False,
Expand All @@ -52,10 +55,14 @@ def __init__(
):
self.router = router
self.name = name
self.handlers = defaultdict(lambda: defaultdict(list)) # type: ignore
self.handler = handler
self.methods = frozenset(methods)
self.requirements = Requirements(requirements or {})

self.ctx = SimpleNamespace()

self._params: t.Dict[int, ParamInfo] = {}
self._raw_path = raw_path
self.ctx = SimpleNamespace()

parts = path_to_parts(raw_path, self.router.delimiter)
self.path = parts_to_path(parts, delimiter=self.router.delimiter)
Expand All @@ -66,64 +73,42 @@ def __init__(
self.pattern = None
self.strict: bool = strict
self.unquote: bool = unquote
self.requirements: t.Dict[int, t.Any] = {}
self.labels: t.Optional[t.List[str]] = None

def __repr__(self):
self._setup_params()

def __str__(self):
display = (
f"name={self.name} path={self.path or self.router.delimiter}"
if self.name and self.name != self.path
else f"path={self.path or self.router.delimiter}"
)
return f"<{self.__class__.__name__}: {display}>"

def get_handler(self, raw_path, method, idx):
method = method or self.router.DEFAULT_METHOD
raw_path = raw_path.lstrip(self.router.delimiter)
try:
return self.handlers[raw_path][method][idx]
except (IndexError, KeyError):
raise self.router.method_handler_exception(
f"Method '{method}' not found on {self}",
method=method,
allowed_methods=self.methods,
def __eq__(self, other) -> bool:
if not isinstance(other, self.__class__):
return False
return bool(
(
self.parts,
self.requirements,
)
== (
other.parts,
other.requirements,
)
and (self.methods & other.methods)
)

def add_handler(
self,
raw_path,
handler,
method,
requirements,
overwrite: bool = False,
):
def _setup_params(self):
key_path = parts_to_path(
path_to_parts(raw_path, self.router.delimiter),
path_to_parts(self.raw_path, self.router.delimiter),
self.router.delimiter,
)

if (
not self.router.stacking
and self.handlers.get(key_path, {}).get(method)
and (
requirements is None
or Requirements(requirements) in self.requirements.values()
)
and not overwrite
):
raise RouteExists(
f"Route already registered: {key_path} [{method}]"
)

idx = len(self.handlers[key_path][method.upper()])
self.handlers[key_path][method.upper()].append(handler)
if requirements is not None:
self.requirements[idx] = Requirements(requirements)

if not self.static:
parts = path_to_parts(key_path, self.router.delimiter)
for idx, part in enumerate(parts):
if "<" in part and len(self.handlers[key_path]) == 1:
if "<" in part:
if ":" in part:
(
name,
Expand Down Expand Up @@ -173,17 +158,6 @@ def _finalize_params(self):
sorted(params.items(), key=lambda param: self._sorting(param[1]))
)

def _finalize_methods(self):
self.methods = set()
for handlers in self.handlers.values():
self.methods.update(set(key.upper() for key in handlers.keys()))

def _finalize_handlers(self):
self.handlers = Immutable(self.handlers)

def _reset_handlers(self):
self.handlers = dict(self.handlers)

def _compile_regex(self):
components = []

Expand Down Expand Up @@ -225,11 +199,14 @@ def finalize(self):
self._finalize_params()
if self.regex:
self._compile_regex()
self._finalize_methods()
self._finalize_handlers()
self.requirements = Immutable(self.requirements)

def reset(self):
self._reset_handlers()
self.requirements = dict(self.requirements)

@property
def defined_params(self):
return self._params

@property
def raw_path(self):
Expand Down
Loading

0 comments on commit e274b6c

Please sign in to comment.