Skip to content

Commit e274b6c

Browse files
authored
Route group (#24)
* Revamp routes to be nested inside RouteGroup * Cleanup some imports * Pass all Sanic routing tests * Passing all tests on main repo * Fix type annotations
1 parent 7818338 commit e274b6c

8 files changed

Lines changed: 434 additions & 201 deletions

File tree

sanic_routing/__init__.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1+
from .group import RouteGroup
12
from .route import Route
23
from .router import BaseRouter
34

4-
__version__ = "0.5.2"
5-
__all__ = ("BaseRouter", "Route")
5+
__version__ = "0.6.0"
6+
__all__ = ("BaseRouter", "Route", "RouteGroup")

sanic_routing/exceptions.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ class BadMethod(BaseException):
2222
class NoMethod(BaseException):
2323
def __init__(
2424
self,
25-
message: str,
25+
message: str = "Method does not exist",
2626
method: Optional[str] = None,
2727
allowed_methods: Optional[Set[str]] = None,
2828
):

sanic_routing/group.py

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
from sanic_routing.utils import Immutable
2+
3+
from .exceptions import InvalidUsage, RouteExists
4+
5+
6+
class RouteGroup:
7+
methods_index: Immutable
8+
9+
def __init__(self, *routes) -> None:
10+
if len(set(route.parts for route in routes)) > 1:
11+
raise InvalidUsage("Cannot group routes with differing paths")
12+
13+
if any(routes[-1].strict != route.strict for route in routes):
14+
raise InvalidUsage("Cannot group routes with differing strictness")
15+
16+
route_list = list(routes)
17+
route_list.pop()
18+
19+
self._routes = routes
20+
self.pattern_idx = 0
21+
22+
def __str__(self):
23+
display = (
24+
f"path={self.path or self.router.delimiter} len={len(self.routes)}"
25+
)
26+
return f"<{self.__class__.__name__}: {display}>"
27+
28+
def __iter__(self):
29+
return iter(self.routes)
30+
31+
def __getitem__(self, key):
32+
return self.routes[key]
33+
34+
def finalize(self):
35+
self.methods_index = Immutable(
36+
{
37+
method: route
38+
for route in self._routes
39+
for method in route.methods
40+
}
41+
)
42+
43+
def reset(self):
44+
self.methods_index = dict(self.methods_index)
45+
46+
def merge(self, group, overwrite: bool = False, append: bool = False):
47+
_routes = list(self._routes)
48+
for other_route in group.routes:
49+
for current_route in self:
50+
if (
51+
current_route == other_route
52+
or (
53+
current_route.requirements
54+
and not other_route.requirements
55+
)
56+
or (
57+
not current_route.requirements
58+
and other_route.requirements
59+
)
60+
) and not append:
61+
if not overwrite:
62+
raise RouteExists(
63+
f"Route already registered: {self.raw_path} "
64+
f"[{','.join(self.methods)}]"
65+
)
66+
else:
67+
_routes.append(other_route)
68+
self._routes = tuple(_routes)
69+
70+
@property
71+
def labels(self):
72+
return self[0].labels
73+
74+
@property
75+
def methods(self):
76+
return frozenset(
77+
[method for route in self for method in route.methods]
78+
)
79+
80+
@property
81+
def params(self):
82+
return self[0].params
83+
84+
@property
85+
def parts(self):
86+
return self[0].parts
87+
88+
@property
89+
def path(self):
90+
return self[0].path
91+
92+
@property
93+
def pattern(self):
94+
return self[0].pattern
95+
96+
@property
97+
def raw_path(self):
98+
return self[0].raw_path
99+
100+
@property
101+
def regex(self):
102+
return self[0].regex
103+
104+
@property
105+
def requirements(self):
106+
return [route.requirements for route in self if route.requirements]
107+
108+
@property
109+
def routes(self):
110+
return self._routes
111+
112+
@property
113+
def router(self):
114+
return self[0].router
115+
116+
@property
117+
def strict(self):
118+
return self[0].strict
119+
120+
@property
121+
def unquote(self):
122+
return self[0].unquote

sanic_routing/route.py

Lines changed: 39 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import re
22
import typing as t
3-
from collections import defaultdict, namedtuple
3+
from collections import namedtuple
44
from types import SimpleNamespace
55

6-
from .exceptions import InvalidUsage, ParameterNameConflicts, RouteExists
6+
from .exceptions import InvalidUsage, ParameterNameConflicts
77
from .patterns import REGEX_TYPES
88
from .utils import Immutable, parts_to_path, path_to_parts
99

@@ -12,7 +12,7 @@
1212
)
1313

1414

15-
class Requirements(dict):
15+
class Requirements(Immutable):
1616
def __hash__(self):
1717
return hash(frozenset(self.items()))
1818

@@ -22,10 +22,11 @@ class Route:
2222
"_params",
2323
"_raw_path",
2424
"ctx",
25-
"handlers",
25+
"handler",
2626
"labels",
2727
"methods",
2828
"name",
29+
"overloaded",
2930
"params",
3031
"parts",
3132
"path",
@@ -36,14 +37,16 @@ class Route:
3637
"static",
3738
"strict",
3839
"unquote",
39-
"overloaded",
4040
)
4141

4242
def __init__(
4343
self,
4444
router,
4545
raw_path: str,
4646
name: str,
47+
handler: t.Callable[..., t.Any],
48+
methods: t.Iterable[str],
49+
requirements: t.Dict[str, t.Any] = None,
4750
strict: bool = False,
4851
unquote: bool = False,
4952
static: bool = False,
@@ -52,10 +55,14 @@ def __init__(
5255
):
5356
self.router = router
5457
self.name = name
55-
self.handlers = defaultdict(lambda: defaultdict(list)) # type: ignore
58+
self.handler = handler
59+
self.methods = frozenset(methods)
60+
self.requirements = Requirements(requirements or {})
61+
62+
self.ctx = SimpleNamespace()
63+
5664
self._params: t.Dict[int, ParamInfo] = {}
5765
self._raw_path = raw_path
58-
self.ctx = SimpleNamespace()
5966

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

72-
def __repr__(self):
78+
self._setup_params()
79+
80+
def __str__(self):
7381
display = (
7482
f"name={self.name} path={self.path or self.router.delimiter}"
7583
if self.name and self.name != self.path
7684
else f"path={self.path or self.router.delimiter}"
7785
)
7886
return f"<{self.__class__.__name__}: {display}>"
7987

80-
def get_handler(self, raw_path, method, idx):
81-
method = method or self.router.DEFAULT_METHOD
82-
raw_path = raw_path.lstrip(self.router.delimiter)
83-
try:
84-
return self.handlers[raw_path][method][idx]
85-
except (IndexError, KeyError):
86-
raise self.router.method_handler_exception(
87-
f"Method '{method}' not found on {self}",
88-
method=method,
89-
allowed_methods=self.methods,
88+
def __eq__(self, other) -> bool:
89+
if not isinstance(other, self.__class__):
90+
return False
91+
return bool(
92+
(
93+
self.parts,
94+
self.requirements,
95+
)
96+
== (
97+
other.parts,
98+
other.requirements,
9099
)
100+
and (self.methods & other.methods)
101+
)
91102

92-
def add_handler(
93-
self,
94-
raw_path,
95-
handler,
96-
method,
97-
requirements,
98-
overwrite: bool = False,
99-
):
103+
def _setup_params(self):
100104
key_path = parts_to_path(
101-
path_to_parts(raw_path, self.router.delimiter),
105+
path_to_parts(self.raw_path, self.router.delimiter),
102106
self.router.delimiter,
103107
)
104-
105-
if (
106-
not self.router.stacking
107-
and self.handlers.get(key_path, {}).get(method)
108-
and (
109-
requirements is None
110-
or Requirements(requirements) in self.requirements.values()
111-
)
112-
and not overwrite
113-
):
114-
raise RouteExists(
115-
f"Route already registered: {key_path} [{method}]"
116-
)
117-
118-
idx = len(self.handlers[key_path][method.upper()])
119-
self.handlers[key_path][method.upper()].append(handler)
120-
if requirements is not None:
121-
self.requirements[idx] = Requirements(requirements)
122-
123108
if not self.static:
124109
parts = path_to_parts(key_path, self.router.delimiter)
125110
for idx, part in enumerate(parts):
126-
if "<" in part and len(self.handlers[key_path]) == 1:
111+
if "<" in part:
127112
if ":" in part:
128113
(
129114
name,
@@ -173,17 +158,6 @@ def _finalize_params(self):
173158
sorted(params.items(), key=lambda param: self._sorting(param[1]))
174159
)
175160

176-
def _finalize_methods(self):
177-
self.methods = set()
178-
for handlers in self.handlers.values():
179-
self.methods.update(set(key.upper() for key in handlers.keys()))
180-
181-
def _finalize_handlers(self):
182-
self.handlers = Immutable(self.handlers)
183-
184-
def _reset_handlers(self):
185-
self.handlers = dict(self.handlers)
186-
187161
def _compile_regex(self):
188162
components = []
189163

@@ -225,11 +199,14 @@ def finalize(self):
225199
self._finalize_params()
226200
if self.regex:
227201
self._compile_regex()
228-
self._finalize_methods()
229-
self._finalize_handlers()
202+
self.requirements = Immutable(self.requirements)
230203

231204
def reset(self):
232-
self._reset_handlers()
205+
self.requirements = dict(self.requirements)
206+
207+
@property
208+
def defined_params(self):
209+
return self._params
233210

234211
@property
235212
def raw_path(self):

0 commit comments

Comments
 (0)