Package: sanic-routing
Sanic RoutingBackgroundBeginning in v21.3, Sanic makes use of this new AST-style router in two use cases:Routing paths; andRouting signals.Therefore, this package comes with aBaseRouterthat needs to be subclassed in order to be used for its specific needs.Most Sanic users should never need to concern themselves with the details here.Basic ExampleA simple implementation:importloggingfromsanic_routingimportBaseRouterlogging.basicConfig(level=logging.DEBUG)classRouter(BaseRouter):defget(self,path,*args,**kwargs):returnself.resolve(path,*args,**kwargs)router=Router()router.add("/<foo>",lambda:...)router.finalize()router.tree.display()logging.info(router.find_route_src)route,handler,params=router.get("/matchme",method="BASE",extra=None)The above snippet usesrouter.tree.display()to show how the router has decided to arrange the routes into a tree. In this simple example:<Node: level=0>
    <Node: part=__dynamic__:str, level=1, groups=[<RouteGroup: path=<foo:str> len=1>], dynamic=True>We can can see the code that the router has generated for us. It is available as a string atrouter.find_route_src.deffind_route(path,method,router,basket,extra):parts=tuple(path[1:].split(router.delimiter))num=len(parts)# node=1 // part=__dynamic__:strifnum==1:# CHECK 1try:basket['__matches__'][0]=str(parts[0])exceptValueError:passelse:# Return 1returnrouter.dynamic_routes[('<__dynamic__:str>',)][0],basketraiseNotFoundFYI: If you are on Python 3.9, you can see a representation of the source after compilation atrouter.find_route_src_compiledWhat's it doing?Therefore, in general implementation requires you to:Define a router with agetmethod;Add one or more routes;Finalize the router (router.finalize()); andCall the router'sgetmethod.NOTE: You can callrouter.finalize(False)if you do not want to compile the source code into executable form. This is useful if you only intend to review the generated output.Every time you callrouter.addyou create one (1) newRouteinstance. Even if that one route is created with multiple methods, it generates a single instance. If youadd()anotherRoutethat has a similar path structure (but, perhaps has differen methods) they will be grouped together into aRouteGroup. It is worth also noting that aRouteGroupis created the first time you calladd(), but subsequent similar routes will reuse the existing grouping instance.When you callfinalize(), it is taking the defined route groups and arranging them into "nodes" in a hierarchical tree. A single node is a path segment. ANodeinstance can have one or moreRouteGroupon it where theNodeis the termination point for that path.Perhaps an example is easier:router.add("/path/to/<foo>",lambda:...)router.add("/path/to/<foo:int>",lambda:...)router.add("/path/to/different/<foo>",lambda:...)router.add("/path/to/different/<foo>",lambda:...,methods=["one","two"])The generatedRouteGroupinstances (3):<RouteGroup: path=path/to/<foo:str> len=1>
<RouteGroup: path=path/to/<foo:int> len=1>
<RouteGroup: path=path/to/different/<foo:str> len=2>The generatedRouteinstances (4):<Route: path=path/to/<foo:str>>
<Route: path=path/to/<foo:int>>
<Route: path=path/to/different/<foo:str>>
<Route: path=path/to/different/<foo:str>>The Node Tree:<Node: level=0>
    <Node: part=path, level=1>
        <Node: part=to, level=2>
            <Node: part=different, level=3>
                <Node: part=__dynamic__:str, level=4, groups=[<RouteGroup: path=path/to/different/<foo:str> len=2>], dynamic=True>
            <Node: part=__dynamic__:int, level=3, groups=[<RouteGroup: path=path/to/<foo:int> len=1>], dynamic=True>
            <Node: part=__dynamic__:str, level=3, groups=[<RouteGroup: path=path/to/<foo:str> len=1>], dynamic=True>And, the generated source code:deffind_route(path,method,router,basket,extra):parts=tuple(path[1:].split(router.delimiter))num=len(parts)# node=1 // part=pathifnum>1:# CHECK 1ifparts[0]=="path":# CHECK 4# node=1.1 // part=toifnum>2:# CHECK 1ifparts[1]=="to":# CHECK 4# node=1.1.1 // part=differentifnum>3:# CHECK 1ifparts[2]=="different":# CHECK 4# node=1.1.1.1 // part=__dynamic__:strifnum==4:# CHECK 1try:basket['__matches__'][3]=str(parts[3])exceptValueError:passelse:ifmethodinfrozenset({'one','two'}):route_idx=0elifmethodinfrozenset({'BASE'}):route_idx=1else:raiseNoMethod# Return 1.1.1.1returnrouter.dynamic_routes[('path','to','different','<__dynamic__:str>')][route_idx],basket# node=1.1.2 // part=__dynamic__:intifnum>=3:# CHECK 1try:basket['__matches__'][2]=int(parts[2])exceptValueError:passelse:ifnum==3:# CHECK 5# Return 1.1.2returnrouter.dynamic_routes[('path','to','<__dynamic__:int>')][0],basket# node=1.1.3 // part=__dynamic__:strifnum>=3:# CHECK 1try:basket['__matches__'][2]=str(parts[2])exceptValueError:passelse:ifnum==3:# CHECK 5# Return 1.1.3returnrouter.dynamic_routes[('path','to','<__dynamic__:str>')][0],basketraiseNotFoundSpecial casesThe above example only shows routes that have a dynamic path segment in them (example:<foo>). But, there are other use cases that are covered differently:fully static paths- These are paths with no parameters (example:/user/login). These are basically matched against a key/value store.regex paths- If a route as a single regular expression match, then the whole route will be matched via regex. In general, this happens inline not too dissimilar than what we see in the above example.special regex paths- The router comes with a specialpathtype (example:<foo:path>) that can match on an expanded delimiter. This is also true for any regex that uses the path delimiter in it. These cannot be matched in the normal course since they are of unknown length.