Git branch: https://github.com/knbk/django/tree/url_dispatcher
Example url config: https://gist.github.com/knbk/e2f41fb1f61afb61bf90
Holds the state of an URL while resolving/reversing. Applying a constraint extracts and saves the arguments, while stripping the matching part from the path. Unapplying a constraint uses the arguments to reconstruct the partial path and adds it back to the path.
class URL(object):
def apply_constraint(self, constraint):
"""
Apply a constraint to self, extracting and saving captured arguments.
"""
args, kwargs = constraint.match(self)
self.constraints.append((constraint, args, kwargs))
def reconstruct(self):
"""
Reconstruct the path from the saved constraints and arguments.
"""
while self.constraints:
constraint, args, kwargs = self.constraints.pop(0)
constraint.construct(self, *args, **kwargs)
return self
def build_path(self, request=None):
"""
Build an url path. Take into account cross-domain links if `request` is supplied.
"""
url = self.clone().reconstruct()
if request and request.get_host() != url.host:
return "%s%s" % (url.host, url.path)
return url.path
When resolving, a Constraint
searches the url for matches, extracts any arguments and strips the matching part from the url. When reversing, a Constraint
takes a set of arguments, and reconstructs the matching part of the url from those arguments. The partial match is then added back to the url.
With just a request object, and a matching set of constraints, you must be able to fully match and extract all arguments, until the request's path is fully resolved. You must then be able to reconstruct the original URL
or an equivalent matching path.
Likewise, with just a set of constraints and matching arguments, you must be able to construct a fully-qualified path and domain. You must then be able to extract an equivalent set of arguments by applying the original set of constraints.
class Constraint(object):
def match(self, url):
"""
See if url matches, extract arguments, strip matching part from url and return arguments.
"""
def construct(self, url, *args, **kwargs):
"""
Construct partial url path from arguments, add partial path to url. Return unused arguments.
"""
The Resolver
s, along with the View
s, form a graph of possible resolve/reverse paths, with each node containing a set of constraints. The Resolver
handles the nesting and naming of other resolvers and views. When resolving, it is responsible for passing down the URL
, applying the constraints, and find the matching path to a View
. When reversing, it should resolve a fully-qualified url name to a set of constraints that can be reconstructed into an url.
class Resolver(object):
def resolve(self, url):
"""
Apply constraints, then search for matching subresolver.
"""
for constraint in self.constraints:
url.apply_constraint(constraint)
for name, pattern in self.url_patterns:
try:
return pattern.resolve(url)
except Resolver404:
pass
raise Resolver404
def search(self, lookup, current_app=None):
"""
Search for (namespaced) view named `lookup`, yield possible sets of constraints.
"""
for name, pattern in self.url_patterns:
if name is None or name == lookup[0]:
for constraints in pattern.search(lookup[1:], current_app)
yield self.constraints + constraints
The View
class implements the same interface as Resolver
: resolve()
and search()
. All leaf nodes in the Resolver
graph must be View
s. A View
also contains the logic to call a view function, possibly with a set of decorators to apply.
class View(object):
def resolve(self, url):
for constraint in self.constraints:
url.apply_constraint(constraint)
return self, url
def search(self, lookup, current_app=None):
if lookup[0] == self.name:
return [self.constraints]
return []
@property
def decorated_callback(self):
callback = self.callback
for decorator in self.decorators:
callback = decorator(callback)
return callback
def __call__(self, request, *args, **kwargs):
return self.decorated_callback(request, *args, **kwargs.update(self.default_kwargs))