Skip to content

Instantly share code, notes, and snippets.

@knbk
Last active August 29, 2015 14:19
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save knbk/96999abaab4ad4e5f8f9 to your computer and use it in GitHub Desktop.
Save knbk/96999abaab4ad4e5f8f9 to your computer and use it in GitHub Desktop.
URL dispatcher API proposal

Overview

Git branch: https://github.com/knbk/django/tree/url_dispatcher

Example url config: https://gist.github.com/knbk/e2f41fb1f61afb61bf90

The URL class

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

The Constraint class

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 class

The Resolvers, along with the Views, 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

The View class implements the same interface as Resolver: resolve() and search(). All leaf nodes in the Resolver graph must be Views. 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))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment