from __future__ import annotations from typing import FrozenSet, List, Optional, Sequence, Tuple from sanic_routing.route import Requirements, Route from sanic_routing.utils import Immutable from .exceptions import InvalidUsage, RouteExists class RouteGroup: methods_index: Immutable passthru_properties = ( "labels", "params", "parts", "path", "pattern", "raw_path", "regex", "router", "segments", "strict", "unquote", "uri", ) #: The _reconstructed_ path after the Route has been normalized. #: Does not contain preceding ``/`` (see also #: :py:attr:`uri`) path: str #: A regex version of the :py:attr:`~sanic_routing.route.Route.path` pattern: Optional[str] #: Whether the route requires regular expression evaluation regex: bool #: The raw version of the path exploded (see also #: :py:attr:`segments`) parts: Tuple[str, ...] #: Same as :py:attr:`parts` except #: generalized so that any dynamic parts do not #: include param keys since they have no impact on routing. segments: Tuple[str, ...] #: Whether the route should be matched with strict evaluation strict: bool #: Whether the route should be unquoted after matching if (for example) it #: is suspected to contain non-URL friendly characters unquote: bool #: Since :py:attr:`path` does NOT #: include a preceding '/', this adds it back. uri: str 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 __repr__(self) -> str: return str(self) def __iter__(self): return iter(self.routes) def __getitem__(self, key): return self.routes[key] def __getattr__(self, key): # There are a number of properties that all of the routes in the group # share in common. We pass thrm through to make them available # on the RouteGroup, and then cache them so that they are permanent. if key in self.passthru_properties: value = getattr(self[0], key) setattr(self, key, value) return value raise AttributeError(f"RouteGroup has no '{key}' attribute") def finalize(self): self.methods_index = Immutable( { method: route for route in self._routes for method in route.methods } ) def prioritize_routes(self) -> None: """ Sorts the routes in the group by priority """ self._routes = tuple( sorted(self._routes, key=lambda route: route.priority) ) def reset(self): self.methods_index = dict(self.methods_index) def merge( self, group: RouteGroup, overwrite: bool = False, append: bool = False ) -> None: """ The purpose of merge is to group routes with the same path, but declarared individually. In other words to group these: .. code-block:: python @app.get("/path/to") def handler1(...): ... @app.post("/path/to") def handler2(...): ... The other main purpose is to look for conflicts and raise ``RouteExists`` A duplicate route is when: 1. They have the same path and any overlapping methods; AND 2. If they have requirements, they are the same :param group: Incoming route group :type group: RouteGroup :param overwrite: whether to allow an otherwise duplicate route group to overwrite the existing, if ``True`` will not raise exception on duplicates, defaults to False :type overwrite: bool, optional :param append: whether to allow an otherwise duplicate route group to append its routes to the existing route group, defaults to False :type append: bool, optional :raises RouteExists: Raised when there is a duplicate """ _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) _routes.sort( key=lambda route: route.priority, reverse=True ) self._routes = tuple(_routes) @property def depth(self) -> int: """ The number of parts in :py:attr:`parts` """ return len(self[0].parts) @property def dynamic_path(self) -> bool: return any( (param.label == "path") or ("/" in param.label) for param in self.params.values() ) @property def methods(self) -> FrozenSet[str]: """""" return frozenset( [method for route in self for method in route.methods] ) @property def routes(self) -> Sequence[Route]: return self._routes @property def requirements(self) -> List[Requirements]: return [route.requirements for route in self if route.requirements]