]> git.ipfire.org Git - thirdparty/tornado.git/commitdiff
Add Application.reverse_url, which can generate a url to a handler by name.
authorBen Darnell <bdarnell@beaker.local>
Thu, 10 Dec 2009 00:44:19 +0000 (16:44 -0800)
committerBen Darnell <bdarnell@beaker.local>
Thu, 10 Dec 2009 00:44:19 +0000 (16:44 -0800)
This minimizes repetition of url patterns for links and redirects.

Internally, handler mappings are now maintained as a list of URLSpec objects
instead of tuples.  The Application constructor and add_handlers method
will accept either URLSpecs or (pattern, handler, [kwargs]) tuples.

Inspired by similar functionality in django, and based on
http://github.com/apgwoz/tornado/commit/0b57fbcf992647b6674ad9e433bffffac900f83a
by apgwoz.

tornado/web.py

index dccb3297a5b7833a427299213ec813facf39c45b..44fef1cb4398c93da21bef117d3549f413c81c68 100644 (file)
@@ -395,6 +395,7 @@ class RequestHandler(object):
             _=self.locale.translate,
             static_url=self.static_url,
             xsrf_form_html=self.xsrf_form_html,
+            reverse_url=self.application.reverse_url
         )
         args.update(self.ui)
         args.update(kwargs)
@@ -822,10 +823,10 @@ class Application(object):
         http_server.listen(8080)
         ioloop.IOLoop.instance().start()
 
-    The constructor for this class takes in a list of (regexp, request_class)
-    tuples. When we receive requests, we iterate over the list in order and
-    instantiate an instance of the first request class whose regexp matches
-    the request path.
+    The constructor for this class takes in a list of URLSpec objects
+    or (regexp, request_class) tuples. When we receive requests, we
+    iterate over the list in order and instantiate an instance of the
+    first request class whose regexp matches the request path.
 
     Each tuple can contain an optional third element, which should be a
     dictionary if it is present. That dictionary is passed as keyword
@@ -857,6 +858,7 @@ class Application(object):
         else:
             self.transforms = transforms
         self.handlers = []
+        self.named_handlers = {}
         self.default_host = default_host
         self.settings = settings
         self.ui_modules = {}
@@ -886,17 +888,19 @@ class Application(object):
         handlers = []
         self.handlers.append((re.compile(host_pattern), handlers))
 
-        for handler_tuple in host_handlers:
-            assert len(handler_tuple) in (2, 3)
-            pattern = handler_tuple[0]
-            handler = handler_tuple[1]
-            if len(handler_tuple) == 3:
-                kwargs = handler_tuple[2]
-            else:
-                kwargs = {}
-            if not pattern.endswith("$"):
-                pattern += "$"
-            handlers.append((re.compile(pattern), handler, kwargs))
+        for spec in host_handlers:
+            if type(spec) is type(()):
+                assert len(spec) in (2, 3)
+                pattern = spec[0]
+                handler = spec[1]
+                if len(spec) == 3:
+                    kwargs = spec[2]
+                else:
+                    kwargs = {}
+                spec = URLSpec(pattern, handler, kwargs)
+            handlers.append(spec)
+            if spec.name:
+                self.named_handlers[handler.name] = spec
 
     def add_transform(self, transform_class):
         """Adds the given OutputTransform to our transform list."""
@@ -951,10 +955,10 @@ class Application(object):
             handler = RedirectHandler(
                 request, "http://" + self.default_host + "/")
         else:
-            for pattern, handler_class, kwargs in handlers:
-                match = pattern.match(request.path)
+            for spec in handlers:
+                match = spec.regex.match(request.path)
                 if match:
-                    handler = handler_class(self, request, **kwargs)
+                    handler = spec.handler_class(self, request, **spec.kwargs)
                     args = match.groups()
                     break
             if not handler:
@@ -969,6 +973,14 @@ class Application(object):
         handler._execute(transforms, *args)
         return handler
 
+    def reverse_url(self, name, *args):
+        """Returns a URL path for handler named `name`
+
+        The handler must be added to the application as a named URLSpec
+        """
+        if name in self.named_handlers:
+            return self.named_handlers[name].reverse(*args)
+        raise KeyError("%s not found in named urls" % name)
 
 class HTTPError(Exception):
     """An exception that will turn into an HTTP error response."""
@@ -1252,6 +1264,67 @@ class UIModule(object):
     def render_string(self, path, **kwargs):
         return self.handler.render_string(path, **kwargs)
 
+class URLSpec(object):
+    """Specifies mappings between URLs and handlers."""
+    def __init__(self, pattern, handler_class, kwargs=None, name=None):
+        """Creates a URLSpec.
+
+        Parameters:
+        pattern: Regular expression to be matched.  Any groups in the regex
+            will be passed in to the handler's get/post/etc methods as 
+            arguments.
+        handler_class: RequestHandler subclass to be invoked.
+        kwargs (optional): A dictionary of additional arguments to be passed
+            to the handler's constructor.
+        name (optional): A name for this handler.  Used by 
+            Application.reverse_url.
+        """
+        if not pattern.endswith('$'):
+            pattern += '$'
+        self.regex = re.compile(pattern)
+        self.handler_class = handler_class
+        self.kwargs = kwargs
+        self.name = name
+        self._path, self._group_count = self._find_groups()
+
+    def _find_groups(self):
+        """Returns a tuple (reverse string, group count) for a url.
+
+        For example: Given the url pattern /([0-9]{4})/([a-z-]+)/, this method
+        would return ('/%s/%s/', 2).
+        """
+        pattern = self.regex.pattern
+        if pattern.startswith('^'):
+            pattern = pattern[1:]
+        if pattern.endswith('$'):
+            pattern = pattern[:-1]
+
+        if self.regex.groups != pattern.count('('):
+            # The pattern is too complicated for our simplistic matching,
+            # so we can't support reversing it.
+            return (None, None)
+
+        pieces = []
+        for fragment in pattern.split('('):
+            if ')' in fragment:
+                paren_loc = fragment.index(')')
+                if paren_loc >= 0:
+                    pieces.append('%s' + fragment[paren_loc + 1:])
+            else:
+                pieces.append(fragment)
+
+        return (''.join(pieces), self.regex.groups)
+
+    def reverse(self, *args):
+        assert self._path is not None, \
+            "Cannot reverse url regex " + self.regex.pattern
+        assert len(args) == self._group_count, "required number of arguments "\
+            "not found"
+        if not len(args):
+            return self._path
+        return self._path % tuple([str(a) for a in args])
+
+url = URLSpec
 
 def _utf8(s):
     if isinstance(s, unicode):