Source code for django.urls.resolvers

"""This module converts requested URLs to callback view functions.URLResolver is the main class here. Its resolve() method takes a URL (asa string) and returns a ResolverMatch object which provides access to allattributes of the resolved URL match."""importfunctoolsimportinspectimportreimportthreadingfromimportlibimportimport_modulefromurllib.parseimportquotefromdjango.confimportsettingsfromdjango.core.checksimportError,Warningfromdjango.core.checks.urlsimportcheck_resolverfromdjango.core.exceptionsimportImproperlyConfigured,ViewDoesNotExistfromdjango.utils.datastructuresimportMultiValueDictfromdjango.utils.functionalimportcached_propertyfromdjango.utils.httpimportRFC3986_SUBDELIMS,escape_leading_slashesfromdjango.utils.regex_helperimportnormalizefromdjango.utils.translationimportget_languagefrom.convertersimportget_converterfrom.exceptionsimportNoReverseMatch,Resolver404from.utilsimportget_callable

[docs]classResolverMatch:def__init__(self,func,args,kwargs,url_name=None,app_names=None,namespaces=None,route=None):self.func=funcself.args=argsself.kwargs=kwargsself.url_name=url_nameself.route=route# If a URLRegexResolver doesn't have a namespace or app_name, it passes# in an empty value.self.app_names=[xforxinapp_namesifx]ifapp_nameselse[]self.app_name=':'.join(self.app_names)self.namespaces=[xforxinnamespacesifx]ifnamespaceselse[]self.namespace=':'.join(self.namespaces)ifnothasattr(func,'__name__'):# A class-based viewself._func_path=func.__class__.__module__+'.'+func.__class__.__name__else:# A function-based viewself._func_path=func.__module__+'.'+func.__name__view_path=url_nameorself._func_pathself.view_name=':'.join(self.namespaces+[view_path])def__getitem__(self,index):return(self.func,self.args,self.kwargs)[index]def__repr__(self):return"ResolverMatch(func=%s, args=%s, kwargs=%s, url_name=%s, app_names=%s, namespaces=%s, route=%s)"%(self._func_path,self.args,self.kwargs,self.url_name,self.app_names,self.namespaces,self.route,)

@functools.lru_cache(maxsize=None)defget_resolver(urlconf=None):ifurlconfisNone:urlconf=settings.ROOT_URLCONFreturnURLResolver(RegexPattern(r'^/'),urlconf)@functools.lru_cache(maxsize=None)defget_ns_resolver(ns_pattern,resolver,converters):# Build a namespaced resolver for the given parent URLconf pattern.# This makes it possible to have captured parameters in the parent# URLconf pattern.pattern=RegexPattern(ns_pattern)pattern.converters=dict(converters)ns_resolver=URLResolver(pattern,resolver.url_patterns)returnURLResolver(RegexPattern(r'^/'),[ns_resolver])classLocaleRegexDescriptor:def__init__(self,attr):self.attr=attrdef__get__(self,instance,cls=None):""" Return a compiled regular expression based on the active language. """ifinstanceisNone:returnself# As a performance optimization, if the given regex string is a regular# string (not a lazily-translated string proxy), compile it once and# avoid per-language compilation.pattern=getattr(instance,self.attr)ifisinstance(pattern,str):instance.__dict__['regex']=instance._compile(pattern)returninstance.__dict__['regex']language_code=get_language()iflanguage_codenotininstance._regex_dict:instance._regex_dict[language_code]=instance._compile(str(pattern))returninstance._regex_dict[language_code]classCheckURLMixin:defdescribe(self):""" Format the URL pattern for display in warning messages. """description="'{}'".format(self)ifself.name:description+=" [name='{}']".format(self.name)returndescriptiondef_check_pattern_startswith_slash(self):""" Check that the pattern does not begin with a forward slash. """regex_pattern=self.regex.patternifnotsettings.APPEND_SLASH:# Skip check as it can be useful to start a URL pattern with a slash# when APPEND_SLASH=False.return[]ifregex_pattern.startswith(('/','^/','^\\/'))andnotregex_pattern.endswith('/'):warning=Warning("Your URL pattern {} has a route beginning with a '/'. Remove this ""slash as it is unnecessary. If this pattern is targeted in an ""include(), ensure the include() pattern has a trailing '/'.".format(self.describe()),id="urls.W002",)return[warning]else:return[]classRegexPattern(CheckURLMixin):regex=LocaleRegexDescriptor('_regex')def__init__(self,regex,name=None,is_endpoint=False):self._regex=regexself._regex_dict={}self._is_endpoint=is_endpointself.name=nameself.converters={}defmatch(self,path):match=self.regex.search(path)ifmatch:# If there are any named groups, use those as kwargs, ignoring# non-named groups. Otherwise, pass all non-named arguments as# positional arguments.kwargs=match.groupdict()args=()ifkwargselsematch.groups()returnpath[match.end():],args,kwargsreturnNonedefcheck(self):warnings=[]warnings.extend(self._check_pattern_startswith_slash())ifnotself._is_endpoint:warnings.extend(self._check_include_trailing_dollar())returnwarningsdef_check_include_trailing_dollar(self):regex_pattern=self.regex.patternifregex_pattern.endswith('$')andnotregex_pattern.endswith(r'\$'):return[Warning("Your URL pattern {} uses include with a route ending with a '$'. ""Remove the dollar from the route to avoid problems including ""URLs.".format(self.describe()),id='urls.W001',)]else:return[]def_compile(self,regex):"""Compile and return the given regular expression."""try:returnre.compile(regex)exceptre.errorase:raiseImproperlyConfigured('"%s" is not a valid regular expression: %s'%(regex,e))def__str__(self):returnstr(self._regex)_PATH_PARAMETER_COMPONENT_RE=re.compile(r'<(?:(?P<converter>[^>:]+):)?(?P<parameter>\w+)>')def_route_to_regex(route,is_endpoint=False):""" Convert a path pattern into a regular expression. Return the regular expression and a dictionary mapping the capture names to the converters. For example, 'foo/<int:pk>' returns '^foo\\/(?P<pk>[0-9]+)' and {'pk': <django.urls.converters.IntConverter>}. """original_route=routeparts=['^']converters={}whileTrue:match=_PATH_PARAMETER_COMPONENT_RE.search(route)ifnotmatch:parts.append(re.escape(route))breakparts.append(re.escape(route[:match.start()]))route=route[match.end():]parameter=match.group('parameter')ifnotparameter.isidentifier():raiseImproperlyConfigured("URL route '%s' uses parameter name %r which isn't a valid ""Python identifier."%(original_route,parameter))raw_converter=match.group('converter')ifraw_converterisNone:# If a converter isn't specified, the default is `str`.raw_converter='str'try:converter=get_converter(raw_converter)exceptKeyErrorase:raiseImproperlyConfigured("URL route '%s' uses invalid converter %s."%(original_route,e))converters[parameter]=converterparts.append('(?P<'+parameter+'>'+converter.regex+')')ifis_endpoint:parts.append('$')return''.join(parts),convertersclassRoutePattern(CheckURLMixin):regex=LocaleRegexDescriptor('_route')def__init__(self,route,name=None,is_endpoint=False):self._route=routeself._regex_dict={}self._is_endpoint=is_endpointself.name=nameself.converters=_route_to_regex(str(route),is_endpoint)[1]defmatch(self,path):match=self.regex.search(path)ifmatch:# RoutePattern doesn't allow non-named groups so args are ignored.kwargs=match.groupdict()forkey,valueinkwargs.items():converter=self.converters[key]try:kwargs[key]=converter.to_python(value)exceptValueError:returnNonereturnpath[match.end():],(),kwargsreturnNonedefcheck(self):warnings=self._check_pattern_startswith_slash()route=self._routeif'(?P<'inrouteorroute.startswith('^')orroute.endswith('$'):warnings.append(Warning("Your URL pattern {} has a route that contains '(?P<', begins ""with a '^', or ends with a '$'. This was likely an oversight ""when migrating to django.urls.path().".format(self.describe()),id='2_0.W001',))returnwarningsdef_compile(self,route):returnre.compile(_route_to_regex(route,self._is_endpoint)[0])def__str__(self):returnstr(self._route)classLocalePrefixPattern:def__init__(self,prefix_default_language=True):self.prefix_default_language=prefix_default_languageself.converters={}@propertydefregex(self):# This is only used by reverse() and cached in _reverse_dict.returnre.compile(self.language_prefix)@propertydeflanguage_prefix(self):language_code=get_language()orsettings.LANGUAGE_CODEiflanguage_code==settings.LANGUAGE_CODEandnotself.prefix_default_language:return''else:return'%s/'%language_codedefmatch(self,path):language_prefix=self.language_prefixifpath.startswith(language_prefix):returnpath[len(language_prefix):],(),{}returnNonedefcheck(self):return[]defdescribe(self):return"'{}'".format(self)def__str__(self):returnself.language_prefixclassURLPattern:def__init__(self,pattern,callback,default_args=None,name=None):self.pattern=patternself.callback=callback# the viewself.default_args=default_argsor{}self.name=namedef__repr__(self):return'<%s%s>'%(self.__class__.__name__,self.pattern.describe())defcheck(self):warnings=self._check_pattern_name()warnings.extend(self.pattern.check())returnwarningsdef_check_pattern_name(self):""" Check that the pattern name does not contain a colon. """ifself.pattern.nameisnotNoneand":"inself.pattern.name:warning=Warning("Your URL pattern {} has a name including a ':'. Remove the colon, to ""avoid ambiguous namespace references.".format(self.pattern.describe()),id="urls.W003",)return[warning]else:return[]defresolve(self,path):match=self.pattern.match(path)ifmatch:new_path,args,kwargs=match# Pass any extra_kwargs as **kwargs.kwargs.update(self.default_args)returnResolverMatch(self.callback,args,kwargs,self.pattern.name,route=str(self.pattern))@cached_propertydeflookup_str(self):""" A string that identifies the view (e.g. 'path.to.view_function' or 'path.to.ClassBasedView'). """callback=self.callbackifisinstance(callback,functools.partial):callback=callback.funcifnothasattr(callback,'__name__'):returncallback.__module__+"."+callback.__class__.__name__returncallback.__module__+"."+callback.__qualname__classURLResolver:def__init__(self,pattern,urlconf_name,default_kwargs=None,app_name=None,namespace=None):self.pattern=pattern# urlconf_name is the dotted Python path to the module defining# urlpatterns. It may also be an object with an urlpatterns attribute# or urlpatterns itself.self.urlconf_name=urlconf_nameself.callback=Noneself.default_kwargs=default_kwargsor{}self.namespace=namespaceself.app_name=app_nameself._reverse_dict={}self._namespace_dict={}self._app_dict={}# set of dotted paths to all functions and classes that are used in# urlpatternsself._callback_strs=set()self._populated=Falseself._local=threading.local()self._urlconf_lock=threading.Lock()def__repr__(self):ifisinstance(self.urlconf_name,list)andself.urlconf_name:# Don't bother to output the whole list, it can be hugeurlconf_repr='<%s list>'%self.urlconf_name[0].__class__.__name__else:urlconf_repr=repr(self.urlconf_name)return'<%s%s (%s:%s) %s>'%(self.__class__.__name__,urlconf_repr,self.app_name,self.namespace,self.pattern.describe(),)defcheck(self):messages=[]forpatterninself.url_patterns:messages.extend(check_resolver(pattern))messages.extend(self._check_custom_error_handlers())returnmessagesorself.pattern.check()def_check_custom_error_handlers(self):messages=[]# All handlers take (request, exception) arguments except handler500# which takes (request).forstatus_code,num_parametersin[(400,2),(403,2),(404,2),(500,1)]:try:handler,param_dict=self.resolve_error_handler(status_code)except(ImportError,ViewDoesNotExist)ase:path=getattr(self.urlconf_module,'handler%s'%status_code)msg=("The custom handler{status_code} view '{path}' could not be imported.").format(status_code=status_code,path=path)messages.append(Error(msg,hint=str(e),id='urls.E008'))continuesignature=inspect.signature(handler)args=[None]*num_parameterstry:signature.bind(*args)exceptTypeError:msg=("The custom handler{status_code} view '{path}' does not ""take the correct number of arguments ({args}).").format(status_code=status_code,path=handler.__module__+'.'+handler.__qualname__,args='request, exception'ifnum_parameters==2else'request',)messages.append(Error(msg,id='urls.E007'))returnmessagesdef_populate(self):# Short-circuit if called recursively in this thread to prevent# infinite recursion. Concurrent threads may call this at the same# time and will need to continue, so set 'populating' on a# thread-local variable.ifgetattr(self._local,'populating',False):returntry:self._local.populating=Truelookups=MultiValueDict()namespaces={}apps={}language_code=get_language()forurl_patterninreversed(self.url_patterns):p_pattern=url_pattern.pattern.regex.patternifp_pattern.startswith('^'):p_pattern=p_pattern[1:]ifisinstance(url_pattern,URLPattern):self._callback_strs.add(url_pattern.lookup_str)bits=normalize(url_pattern.pattern.regex.pattern)lookups.appendlist(url_pattern.callback,(bits,p_pattern,url_pattern.default_args,url_pattern.pattern.converters))ifurl_pattern.nameisnotNone:lookups.appendlist(url_pattern.name,(bits,p_pattern,url_pattern.default_args,url_pattern.pattern.converters))else:# url_pattern is a URLResolver.url_pattern._populate()ifurl_pattern.app_name:apps.setdefault(url_pattern.app_name,[]).append(url_pattern.namespace)namespaces[url_pattern.namespace]=(p_pattern,url_pattern)else:fornameinurl_pattern.reverse_dict:formatches,pat,defaults,convertersinurl_pattern.reverse_dict.getlist(name):new_matches=normalize(p_pattern+pat)lookups.appendlist(name,(new_matches,p_pattern+pat,{**defaults,**url_pattern.default_kwargs},{**self.pattern.converters,**url_pattern.pattern.converters,**converters}))fornamespace,(prefix,sub_pattern)inurl_pattern.namespace_dict.items():current_converters=url_pattern.pattern.converterssub_pattern.pattern.converters.update(current_converters)namespaces[namespace]=(p_pattern+prefix,sub_pattern)forapp_name,namespace_listinurl_pattern.app_dict.items():apps.setdefault(app_name,[]).extend(namespace_list)self._callback_strs.update(url_pattern._callback_strs)self._namespace_dict[language_code]=namespacesself._app_dict[language_code]=appsself._reverse_dict[language_code]=lookupsself._populated=Truefinally:self._local.populating=False@propertydefreverse_dict(self):language_code=get_language()iflanguage_codenotinself._reverse_dict:self._populate()returnself._reverse_dict[language_code]@propertydefnamespace_dict(self):language_code=get_language()iflanguage_codenotinself._namespace_dict:self._populate()returnself._namespace_dict[language_code]@propertydefapp_dict(self):language_code=get_language()iflanguage_codenotinself._app_dict:self._populate()returnself._app_dict[language_code]@staticmethoddef_join_route(route1,route2):"""Join two routes, without the starting ^ in the second route."""ifnotroute1:returnroute2ifroute2.startswith('^'):route2=route2[1:]returnroute1+route2def_is_callback(self,name):ifnotself._populated:self._populate()returnnameinself._callback_strsdefresolve(self,path):path=str(path)# path may be a reverse_lazy objecttried=[]match=self.pattern.match(path)ifmatch:new_path,args,kwargs=matchforpatterninself.url_patterns:try:sub_match=pattern.resolve(new_path)exceptResolver404ase:sub_tried=e.args[0].get('tried')ifsub_triedisnotNone:tried.extend([pattern]+tfortinsub_tried)else:tried.append([pattern])else:ifsub_match:# Merge captured arguments in match with submatchsub_match_dict={**kwargs,**self.default_kwargs}# Update the sub_match_dict with the kwargs from the sub_match.sub_match_dict.update(sub_match.kwargs)# If there are *any* named groups, ignore all non-named groups.# Otherwise, pass all non-named arguments as positional arguments.sub_match_args=sub_match.argsifnotsub_match_dict:sub_match_args=args+sub_match.argscurrent_route=''ifisinstance(pattern,URLPattern)elsestr(pattern.pattern)returnResolverMatch(sub_match.func,sub_match_args,sub_match_dict,sub_match.url_name,[self.app_name]+sub_match.app_names,[self.namespace]+sub_match.namespaces,self._join_route(current_route,sub_match.route),)tried.append([pattern])raiseResolver404({'tried':tried,'path':new_path})raiseResolver404({'path':path})@cached_propertydefurlconf_module(self):# import_module is not thread safe if the module throws an exception# during import, and can return an empty module object in Python < 3.6# (see https://bugs.python.org/issue36284).withself._urlconf_lock:ifisinstance(self.urlconf_name,str):returnimport_module(self.urlconf_name)else:returnself.urlconf_name@cached_propertydefurl_patterns(self):# urlconf_module might be a valid set of patterns, so we default to itpatterns=getattr(self.urlconf_module,"urlpatterns",self.urlconf_module)try:iter(patterns)exceptTypeError:msg=("The included URLconf '{name}' does not appear to have any ""patterns in it. If you see valid patterns in the file then ""the issue is probably caused by a circular import.")raiseImproperlyConfigured(msg.format(name=self.urlconf_name))returnpatternsdefresolve_error_handler(self,view_type):callback=getattr(self.urlconf_module,'handler%s'%view_type,None)ifnotcallback:# No handler specified in file; use lazy import, since# django.conf.urls imports this file.fromdjango.confimporturlscallback=getattr(urls,'handler%s'%view_type)returnget_callable(callback),{}defreverse(self,lookup_view,*args,**kwargs):returnself._reverse_with_prefix(lookup_view,'',*args,**kwargs)def_reverse_with_prefix(self,lookup_view,_prefix,*args,**kwargs):ifargsandkwargs:raiseValueError("Don't mix *args and **kwargs in call to reverse()!")ifnotself._populated:self._populate()possibilities=self.reverse_dict.getlist(lookup_view)forpossibility,pattern,defaults,convertersinpossibilities:forresult,paramsinpossibility:ifargs:iflen(args)!=len(params):continuecandidate_subs=dict(zip(params,args))else:ifset(kwargs).symmetric_difference(params).difference(defaults):continueifany(kwargs.get(k,v)!=vfork,vindefaults.items()):continuecandidate_subs=kwargs# Convert the candidate subs to text using Converter.to_url().text_candidate_subs={}fork,vincandidate_subs.items():ifkinconverters:text_candidate_subs[k]=converters[k].to_url(v)else:text_candidate_subs[k]=str(v)# WSGI provides decoded URLs, without %xx escapes, and the URL# resolver operates on such URLs. First substitute arguments# without quoting to build a decoded URL and look for a match.# Then, if we have a match, redo the substitution with quoted# arguments in order to return a properly encoded URL.candidate_pat=_prefix.replace('%','%%')+resultifre.search('^%s%s'%(re.escape(_prefix),pattern),candidate_pat%text_candidate_subs):# safe characters from `pchar` definition of RFC 3986url=quote(candidate_pat%text_candidate_subs,safe=RFC3986_SUBDELIMS+'/~:@')# Don't allow construction of scheme relative urls.returnescape_leading_slashes(url)# lookup_view can be URL name or callable, but callables are not# friendly in error messages.m=getattr(lookup_view,'__module__',None)n=getattr(lookup_view,'__name__',None)ifmisnotNoneandnisnotNone:lookup_view_s="%s.%s"%(m,n)else:lookup_view_s=lookup_viewpatterns=[patternfor(_,pattern,_,_)inpossibilities]ifpatterns:ifargs:arg_msg="arguments '%s'"%(args,)elifkwargs:arg_msg="keyword arguments '%s'"%(kwargs,)else:arg_msg="no arguments"msg=("Reverse for '%s' with %s not found. %d pattern(s) tried: %s"%(lookup_view_s,arg_msg,len(patterns),patterns))else:msg=("Reverse for '%(view)s' not found. '%(view)s' is not ""a valid view function or pattern name."%{'view':lookup_view_s})raiseNoReverseMatch(msg)