@@ -716,13 +716,20 @@ class PrefixedSubAppResource(PrefixResource):
716716 def __init__ (self , prefix : str , app : "Application" ) -> None :
717717 super ().__init__ (prefix )
718718 self ._app = app
719- for resource in app .router .resources ():
720- resource .add_prefix (prefix )
719+ self ._add_prefix_to_resources (prefix )
721720
722721 def add_prefix (self , prefix : str ) -> None :
723722 super ().add_prefix (prefix )
724- for resource in self ._app .router .resources ():
723+ self ._add_prefix_to_resources (prefix )
724+
725+ def _add_prefix_to_resources (self , prefix : str ) -> None :
726+ router = self ._app .router
727+ for resource in router .resources ():
728+ # Since the canonical path of a resource is about
729+ # to change, we need to unindex it and then reindex
730+ router .unindex_resource (resource )
725731 resource .add_prefix (prefix )
732+ router .index_resource (resource )
726733
727734 def url_for (self , * args : str , ** kwargs : str ) -> URL :
728735 raise RuntimeError (".url_for() is not supported " "by sub-application root" )
@@ -731,11 +738,6 @@ def get_info(self) -> _InfoDict:
731738 return {"app" : self ._app , "prefix" : self ._prefix }
732739
733740 async def resolve (self , request : Request ) -> _Resolve :
734- if (
735- not request .url .raw_path .startswith (self ._prefix2 )
736- and request .url .raw_path != self ._prefix
737- ):
738- return None , set ()
739741 match_info = await self ._app .router .resolve (request )
740742 match_info .add_app (self ._app )
741743 if isinstance (match_info .http_exception , HTTPMethodNotAllowed ):
@@ -974,27 +976,55 @@ def __contains__(self, route: object) -> bool:
974976
975977class UrlDispatcher (AbstractRouter , Mapping [str , AbstractResource ]):
976978 NAME_SPLIT_RE = re .compile (r"[.:-]" )
979+ HTTP_NOT_FOUND = HTTPNotFound ()
977980
978981 def __init__ (self ) -> None :
979982 super ().__init__ ()
980983 self ._resources : List [AbstractResource ] = []
981984 self ._named_resources : Dict [str , AbstractResource ] = {}
985+ self ._resource_index : dict [str , list [AbstractResource ]] = {}
986+ self ._matched_sub_app_resources : List [MatchedSubAppResource ] = []
982987
983988 async def resolve (self , request : Request ) -> UrlMappingMatchInfo :
984- method = request . method
989+ resource_index = self . _resource_index
985990 allowed_methods : Set [str ] = set ()
986991
987- for resource in self ._resources :
992+ # Walk the url parts looking for candidates. We walk the url backwards
993+ # to ensure the most explicit match is found first. If there are multiple
994+ # candidates for a given url part because there are multiple resources
995+ # registered for the same canonical path, we resolve them in a linear
996+ # fashion to ensure registration order is respected.
997+ url_part = request .rel_url .raw_path
998+ while url_part :
999+ for candidate in resource_index .get (url_part , ()):
1000+ match_dict , allowed = await candidate .resolve (request )
1001+ if match_dict is not None :
1002+ return match_dict
1003+ else :
1004+ allowed_methods |= allowed
1005+ if url_part == "/" :
1006+ break
1007+ url_part = url_part .rpartition ("/" )[0 ] or "/"
1008+
1009+ #
1010+ # We didn't find any candidates, so we'll try the matched sub-app
1011+ # resources which we have to walk in a linear fashion because they
1012+ # have regex/wildcard match rules and we cannot index them.
1013+ #
1014+ # For most cases we do not expect there to be many of these since
1015+ # currently they are only added by `add_domain`
1016+ #
1017+ for resource in self ._matched_sub_app_resources :
9881018 match_dict , allowed = await resource .resolve (request )
9891019 if match_dict is not None :
9901020 return match_dict
9911021 else :
9921022 allowed_methods |= allowed
9931023
9941024 if allowed_methods :
995- return MatchInfoError (HTTPMethodNotAllowed (method , allowed_methods ))
996- else :
997- return MatchInfoError (HTTPNotFound () )
1025+ return MatchInfoError (HTTPMethodNotAllowed (request . method , allowed_methods ))
1026+
1027+ return MatchInfoError (self . HTTP_NOT_FOUND )
9981028
9991029 def __iter__ (self ) -> Iterator [str ]:
10001030 return iter (self ._named_resources )
@@ -1050,6 +1080,30 @@ def register_resource(self, resource: AbstractResource) -> None:
10501080 self ._named_resources [name ] = resource
10511081 self ._resources .append (resource )
10521082
1083+ if isinstance (resource , MatchedSubAppResource ):
1084+ # We cannot index match sub-app resources because they have match rules
1085+ self ._matched_sub_app_resources .append (resource )
1086+ else :
1087+ self .index_resource (resource )
1088+
1089+ def _get_resource_index_key (self , resource : AbstractResource ) -> str :
1090+ """Return a key to index the resource in the resource index."""
1091+ # strip at the first { to allow for variables
1092+ return resource .canonical .partition ("{" )[0 ].rstrip ("/" ) or "/"
1093+
1094+ def index_resource (self , resource : AbstractResource ) -> None :
1095+ """Add a resource to the resource index."""
1096+ resource_key = self ._get_resource_index_key (resource )
1097+ # There may be multiple resources for a canonical path
1098+ # so we keep them in a list to ensure that registration
1099+ # order is respected.
1100+ self ._resource_index .setdefault (resource_key , []).append (resource )
1101+
1102+ def unindex_resource (self , resource : AbstractResource ) -> None :
1103+ """Remove a resource from the resource index."""
1104+ resource_key = self ._get_resource_index_key (resource )
1105+ self ._resource_index [resource_key ].remove (resource )
1106+
10531107 def add_resource (self , path : str , * , name : Optional [str ] = None ) -> Resource :
10541108 if path and not path .startswith ("/" ):
10551109 raise ValueError ("path should be started with / or be empty" )
0 commit comments