]> arthur.barton.de Git - bup.git/blob - lib/tornado/web.py
Always publish (l)utimes in helpers when available and fix type conversions.
[bup.git] / lib / tornado / web.py
1 #!/usr/bin/env python
2 #
3 # Copyright 2009 Facebook
4 #
5 # Licensed under the Apache License, Version 2.0 (the "License"); you may
6 # not use this file except in compliance with the License. You may obtain
7 # a copy of the License at
8 #
9 #     http://www.apache.org/licenses/LICENSE-2.0
10 #
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13 # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14 # License for the specific language governing permissions and limitations
15 # under the License.
16
17 """The Tornado web framework.
18
19 The Tornado web framework looks a bit like web.py (http://webpy.org/) or
20 Google's webapp (http://code.google.com/appengine/docs/python/tools/webapp/),
21 but with additional tools and optimizations to take advantage of the
22 Tornado non-blocking web server and tools.
23
24 Here is the canonical "Hello, world" example app:
25
26     import tornado.httpserver
27     import tornado.ioloop
28     import tornado.web
29
30     class MainHandler(tornado.web.RequestHandler):
31         def get(self):
32             self.write("Hello, world")
33
34     if __name__ == "__main__":
35         application = tornado.web.Application([
36             (r"/", MainHandler),
37         ])
38         http_server = tornado.httpserver.HTTPServer(application)
39         http_server.listen(8888)
40         tornado.ioloop.IOLoop.instance().start()
41
42 See the Tornado walkthrough on GitHub for more details and a good
43 getting started guide.
44 """
45
46 import base64
47 import binascii
48 import calendar
49 import Cookie
50 import cStringIO
51 import datetime
52 import email.utils
53 import escape
54 import functools
55 import gzip
56 import hashlib
57 import hmac
58 import httplib
59 import locale
60 import logging
61 import mimetypes
62 import os.path
63 import re
64 import stat
65 import sys
66 import template
67 import time
68 import types
69 import urllib
70 import urlparse
71 import uuid
72
73 class RequestHandler(object):
74     """Subclass this class and define get() or post() to make a handler.
75
76     If you want to support more methods than the standard GET/HEAD/POST, you
77     should override the class variable SUPPORTED_METHODS in your
78     RequestHandler class.
79     """
80     SUPPORTED_METHODS = ("GET", "HEAD", "POST", "DELETE", "PUT")
81
82     def __init__(self, application, request, transforms=None):
83         self.application = application
84         self.request = request
85         self._headers_written = False
86         self._finished = False
87         self._auto_finish = True
88         self._transforms = transforms or []
89         self.ui = _O((n, self._ui_method(m)) for n, m in
90                      application.ui_methods.iteritems())
91         self.ui["modules"] = _O((n, self._ui_module(n, m)) for n, m in
92                                 application.ui_modules.iteritems())
93         self.clear()
94         # Check since connection is not available in WSGI
95         if hasattr(self.request, "connection"):
96             self.request.connection.stream.set_close_callback(
97                 self.on_connection_close)
98
99     @property
100     def settings(self):
101         return self.application.settings
102
103     def head(self, *args, **kwargs):
104         raise HTTPError(405)
105
106     def get(self, *args, **kwargs):
107         raise HTTPError(405)
108
109     def post(self, *args, **kwargs):
110         raise HTTPError(405)
111
112     def delete(self, *args, **kwargs):
113         raise HTTPError(405)
114
115     def put(self, *args, **kwargs):
116         raise HTTPError(405)
117
118     def prepare(self):
119         """Called before the actual handler method.
120
121         Useful to override in a handler if you want a common bottleneck for
122         all of your requests.
123         """
124         pass
125
126     def on_connection_close(self):
127         """Called in async handlers if the client closed the connection.
128
129         You may override this to clean up resources associated with
130         long-lived connections.
131
132         Note that the select()-based implementation of IOLoop does not detect
133         closed connections and so this method will not be called until
134         you try (and fail) to produce some output.  The epoll- and kqueue-
135         based implementations should detect closed connections even while
136         the request is idle.
137         """
138         pass
139
140     def clear(self):
141         """Resets all headers and content for this response."""
142         self._headers = {
143             "Server": "TornadoServer/0.1",
144             "Content-Type": "text/html; charset=UTF-8",
145         }
146         if not self.request.supports_http_1_1():
147             if self.request.headers.get("Connection") == "Keep-Alive":
148                 self.set_header("Connection", "Keep-Alive")
149         self._write_buffer = []
150         self._status_code = 200
151
152     def set_status(self, status_code):
153         """Sets the status code for our response."""
154         assert status_code in httplib.responses
155         self._status_code = status_code
156
157     def set_header(self, name, value):
158         """Sets the given response header name and value.
159
160         If a datetime is given, we automatically format it according to the
161         HTTP specification. If the value is not a string, we convert it to
162         a string. All header values are then encoded as UTF-8.
163         """
164         if isinstance(value, datetime.datetime):
165             t = calendar.timegm(value.utctimetuple())
166             value = email.utils.formatdate(t, localtime=False, usegmt=True)
167         elif isinstance(value, int) or isinstance(value, long):
168             value = str(value)
169         else:
170             value = _utf8(value)
171             # If \n is allowed into the header, it is possible to inject
172             # additional headers or split the request. Also cap length to
173             # prevent obviously erroneous values.
174             safe_value = re.sub(r"[\x00-\x1f]", " ", value)[:4000]
175             if safe_value != value:
176                 raise ValueError("Unsafe header value %r", value)
177         self._headers[name] = value
178
179     _ARG_DEFAULT = []
180     def get_argument(self, name, default=_ARG_DEFAULT, strip=True):
181         """Returns the value of the argument with the given name.
182
183         If default is not provided, the argument is considered to be
184         required, and we throw an HTTP 404 exception if it is missing.
185
186         If the argument appears in the url more than once, we return the
187         last value.
188
189         The returned value is always unicode.
190         """
191         args = self.get_arguments(name, strip=strip)
192         if not args:
193             if default is self._ARG_DEFAULT:
194                 raise HTTPError(404, "Missing argument %s" % name)
195             return default
196         return args[-1]
197
198     def get_arguments(self, name, strip=True):
199         """Returns a list of the arguments with the given name.
200
201         If the argument is not present, returns an empty list.
202
203         The returned values are always unicode.
204         """
205         values = self.request.arguments.get(name, [])
206         # Get rid of any weird control chars
207         values = [re.sub(r"[\x00-\x08\x0e-\x1f]", " ", x) for x in values]
208         values = [_unicode(x) for x in values]
209         if strip:
210             values = [x.strip() for x in values]
211         return values
212
213
214     @property
215     def cookies(self):
216         """A dictionary of Cookie.Morsel objects."""
217         if not hasattr(self, "_cookies"):
218             self._cookies = Cookie.BaseCookie()
219             if "Cookie" in self.request.headers:
220                 try:
221                     self._cookies.load(self.request.headers["Cookie"])
222                 except:
223                     self.clear_all_cookies()
224         return self._cookies
225
226     def get_cookie(self, name, default=None):
227         """Gets the value of the cookie with the given name, else default."""
228         if name in self.cookies:
229             return self.cookies[name].value
230         return default
231
232     def set_cookie(self, name, value, domain=None, expires=None, path="/",
233                    expires_days=None, **kwargs):
234         """Sets the given cookie name/value with the given options.
235
236         Additional keyword arguments are set on the Cookie.Morsel
237         directly.
238         See http://docs.python.org/library/cookie.html#morsel-objects
239         for available attributes.
240         """
241         name = _utf8(name)
242         value = _utf8(value)
243         if re.search(r"[\x00-\x20]", name + value):
244             # Don't let us accidentally inject bad stuff
245             raise ValueError("Invalid cookie %r: %r" % (name, value))
246         if not hasattr(self, "_new_cookies"):
247             self._new_cookies = []
248         new_cookie = Cookie.BaseCookie()
249         self._new_cookies.append(new_cookie)
250         new_cookie[name] = value
251         if domain:
252             new_cookie[name]["domain"] = domain
253         if expires_days is not None and not expires:
254             expires = datetime.datetime.utcnow() + datetime.timedelta(
255                 days=expires_days)
256         if expires:
257             timestamp = calendar.timegm(expires.utctimetuple())
258             new_cookie[name]["expires"] = email.utils.formatdate(
259                 timestamp, localtime=False, usegmt=True)
260         if path:
261             new_cookie[name]["path"] = path
262         for k, v in kwargs.iteritems():
263             new_cookie[name][k] = v
264
265     def clear_cookie(self, name, path="/", domain=None):
266         """Deletes the cookie with the given name."""
267         expires = datetime.datetime.utcnow() - datetime.timedelta(days=365)
268         self.set_cookie(name, value="", path=path, expires=expires,
269                         domain=domain)
270
271     def clear_all_cookies(self):
272         """Deletes all the cookies the user sent with this request."""
273         for name in self.cookies.iterkeys():
274             self.clear_cookie(name)
275
276     def set_secure_cookie(self, name, value, expires_days=30, **kwargs):
277         """Signs and timestamps a cookie so it cannot be forged.
278
279         You must specify the 'cookie_secret' setting in your Application
280         to use this method. It should be a long, random sequence of bytes
281         to be used as the HMAC secret for the signature.
282
283         To read a cookie set with this method, use get_secure_cookie().
284         """
285         timestamp = str(int(time.time()))
286         value = base64.b64encode(value)
287         signature = self._cookie_signature(name, value, timestamp)
288         value = "|".join([value, timestamp, signature])
289         self.set_cookie(name, value, expires_days=expires_days, **kwargs)
290
291     def get_secure_cookie(self, name, include_name=True, value=None):
292         """Returns the given signed cookie if it validates, or None.
293
294         In older versions of Tornado (0.1 and 0.2), we did not include the
295         name of the cookie in the cookie signature. To read these old-style
296         cookies, pass include_name=False to this method. Otherwise, all
297         attempts to read old-style cookies will fail (and you may log all
298         your users out whose cookies were written with a previous Tornado
299         version).
300         """
301         if value is None: value = self.get_cookie(name)
302         if not value: return None
303         parts = value.split("|")
304         if len(parts) != 3: return None
305         if include_name:
306             signature = self._cookie_signature(name, parts[0], parts[1])
307         else:
308             signature = self._cookie_signature(parts[0], parts[1])
309         if not _time_independent_equals(parts[2], signature):
310             logging.warning("Invalid cookie signature %r", value)
311             return None
312         timestamp = int(parts[1])
313         if timestamp < time.time() - 31 * 86400:
314             logging.warning("Expired cookie %r", value)
315             return None
316         try:
317             return base64.b64decode(parts[0])
318         except:
319             return None
320
321     def _cookie_signature(self, *parts):
322         self.require_setting("cookie_secret", "secure cookies")
323         hash = hmac.new(self.application.settings["cookie_secret"],
324                         digestmod=hashlib.sha1)
325         for part in parts: hash.update(part)
326         return hash.hexdigest()
327
328     def redirect(self, url, permanent=False):
329         """Sends a redirect to the given (optionally relative) URL."""
330         if self._headers_written:
331             raise Exception("Cannot redirect after headers have been written")
332         self.set_status(301 if permanent else 302)
333         # Remove whitespace
334         url = re.sub(r"[\x00-\x20]+", "", _utf8(url))
335         self.set_header("Location", urlparse.urljoin(self.request.uri, url))
336         self.finish()
337
338     def write(self, chunk):
339         """Writes the given chunk to the output buffer.
340
341         To write the output to the network, use the flush() method below.
342
343         If the given chunk is a dictionary, we write it as JSON and set
344         the Content-Type of the response to be text/javascript.
345         """
346         assert not self._finished
347         if isinstance(chunk, dict):
348             chunk = escape.json_encode(chunk)
349             self.set_header("Content-Type", "text/javascript; charset=UTF-8")
350         chunk = _utf8(chunk)
351         self._write_buffer.append(chunk)
352
353     def render(self, template_name, **kwargs):
354         """Renders the template with the given arguments as the response."""
355         html = self.render_string(template_name, **kwargs)
356
357         # Insert the additional JS and CSS added by the modules on the page
358         js_embed = []
359         js_files = []
360         css_embed = []
361         css_files = []
362         html_heads = []
363         html_bodies = []
364         for module in getattr(self, "_active_modules", {}).itervalues():
365             embed_part = module.embedded_javascript()
366             if embed_part: js_embed.append(_utf8(embed_part))
367             file_part = module.javascript_files()
368             if file_part:
369                 if isinstance(file_part, basestring):
370                     js_files.append(file_part)
371                 else:
372                     js_files.extend(file_part)
373             embed_part = module.embedded_css()
374             if embed_part: css_embed.append(_utf8(embed_part))
375             file_part = module.css_files()
376             if file_part:
377                 if isinstance(file_part, basestring):
378                     css_files.append(file_part)
379                 else:
380                     css_files.extend(file_part)
381             head_part = module.html_head()
382             if head_part: html_heads.append(_utf8(head_part))
383             body_part = module.html_body()
384             if body_part: html_bodies.append(_utf8(body_part))
385         if js_files:
386             # Maintain order of JavaScript files given by modules
387             paths = []
388             unique_paths = set()
389             for path in js_files:
390                 if not path.startswith("/") and not path.startswith("http:"):
391                     path = self.static_url(path)
392                 if path not in unique_paths:
393                     paths.append(path)
394                     unique_paths.add(path)
395             js = ''.join('<script src="' + escape.xhtml_escape(p) +
396                          '" type="text/javascript"></script>'
397                          for p in paths)
398             sloc = html.rindex('</body>')
399             html = html[:sloc] + js + '\n' + html[sloc:]
400         if js_embed:
401             js = '<script type="text/javascript">\n//<![CDATA[\n' + \
402                 '\n'.join(js_embed) + '\n//]]>\n</script>'
403             sloc = html.rindex('</body>')
404             html = html[:sloc] + js + '\n' + html[sloc:]
405         if css_files:
406             paths = set()
407             for path in css_files:
408                 if not path.startswith("/") and not path.startswith("http:"):
409                     paths.add(self.static_url(path))
410                 else:
411                     paths.add(path)
412             css = ''.join('<link href="' + escape.xhtml_escape(p) + '" '
413                           'type="text/css" rel="stylesheet"/>'
414                           for p in paths)
415             hloc = html.index('</head>')
416             html = html[:hloc] + css + '\n' + html[hloc:]
417         if css_embed:
418             css = '<style type="text/css">\n' + '\n'.join(css_embed) + \
419                 '\n</style>'
420             hloc = html.index('</head>')
421             html = html[:hloc] + css + '\n' + html[hloc:]
422         if html_heads:
423             hloc = html.index('</head>')
424             html = html[:hloc] + ''.join(html_heads) + '\n' + html[hloc:]
425         if html_bodies:
426             hloc = html.index('</body>')
427             html = html[:hloc] + ''.join(html_bodies) + '\n' + html[hloc:]
428         self.finish(html)
429
430     def render_string(self, template_name, **kwargs):
431         """Generate the given template with the given arguments.
432
433         We return the generated string. To generate and write a template
434         as a response, use render() above.
435         """
436         # If no template_path is specified, use the path of the calling file
437         template_path = self.get_template_path()
438         if not template_path:
439             frame = sys._getframe(0)
440             web_file = frame.f_code.co_filename
441             while frame.f_code.co_filename == web_file:
442                 frame = frame.f_back
443             template_path = os.path.dirname(frame.f_code.co_filename)
444         if not getattr(RequestHandler, "_templates", None):
445             RequestHandler._templates = {}
446         if template_path not in RequestHandler._templates:
447             loader = self.application.settings.get("template_loader") or\
448               template.Loader(template_path)
449             RequestHandler._templates[template_path] = loader
450         t = RequestHandler._templates[template_path].load(template_name)
451         args = dict(
452             handler=self,
453             request=self.request,
454             current_user=self.current_user,
455             locale=self.locale,
456             _=self.locale.translate,
457             static_url=self.static_url,
458             xsrf_form_html=self.xsrf_form_html,
459             reverse_url=self.application.reverse_url
460         )
461         args.update(self.ui)
462         args.update(kwargs)
463         return t.generate(**args)
464
465     def flush(self, include_footers=False):
466         """Flushes the current output buffer to the nextwork."""
467         if self.application._wsgi:
468             raise Exception("WSGI applications do not support flush()")
469
470         chunk = "".join(self._write_buffer)
471         self._write_buffer = []
472         if not self._headers_written:
473             self._headers_written = True
474             for transform in self._transforms:
475                 self._headers, chunk = transform.transform_first_chunk(
476                     self._headers, chunk, include_footers)
477             headers = self._generate_headers()
478         else:
479             for transform in self._transforms:
480                 chunk = transform.transform_chunk(chunk, include_footers)
481             headers = ""
482
483         # Ignore the chunk and only write the headers for HEAD requests
484         if self.request.method == "HEAD":
485             if headers: self.request.write(headers)
486             return
487
488         if headers or chunk:
489             self.request.write(headers + chunk)
490
491     def finish(self, chunk=None):
492         """Finishes this response, ending the HTTP request."""
493         assert not self._finished
494         if chunk is not None: self.write(chunk)
495
496         # Automatically support ETags and add the Content-Length header if
497         # we have not flushed any content yet.
498         if not self._headers_written:
499             if (self._status_code == 200 and self.request.method == "GET" and
500                 "Etag" not in self._headers):
501                 hasher = hashlib.sha1()
502                 for part in self._write_buffer:
503                     hasher.update(part)
504                 etag = '"%s"' % hasher.hexdigest()
505                 inm = self.request.headers.get("If-None-Match")
506                 if inm and inm.find(etag) != -1:
507                     self._write_buffer = []
508                     self.set_status(304)
509                 else:
510                     self.set_header("Etag", etag)
511             if "Content-Length" not in self._headers:
512                 content_length = sum(len(part) for part in self._write_buffer)
513                 self.set_header("Content-Length", content_length)
514
515         if hasattr(self.request, "connection"):
516             # Now that the request is finished, clear the callback we
517             # set on the IOStream (which would otherwise prevent the
518             # garbage collection of the RequestHandler when there
519             # are keepalive connections)
520             self.request.connection.stream.set_close_callback(None)
521
522         if not self.application._wsgi:
523             self.flush(include_footers=True)
524             self.request.finish()
525             self._log()
526         self._finished = True
527
528     def send_error(self, status_code=500, **kwargs):
529         """Sends the given HTTP error code to the browser.
530
531         We also send the error HTML for the given error code as returned by
532         get_error_html. Override that method if you want custom error pages
533         for your application.
534         """
535         if self._headers_written:
536             logging.error("Cannot send error response after headers written")
537             if not self._finished:
538                 self.finish()
539             return
540         self.clear()
541         self.set_status(status_code)
542         message = self.get_error_html(status_code, **kwargs)
543         self.finish(message)
544
545     def get_error_html(self, status_code, **kwargs):
546         """Override to implement custom error pages.
547
548         If this error was caused by an uncaught exception, the
549         exception object can be found in kwargs e.g. kwargs['exception']
550         """
551         return "<html><title>%(code)d: %(message)s</title>" \
552                "<body>%(code)d: %(message)s</body></html>" % {
553             "code": status_code,
554             "message": httplib.responses[status_code],
555         }
556
557     @property
558     def locale(self):
559         """The local for the current session.
560
561         Determined by either get_user_locale, which you can override to
562         set the locale based on, e.g., a user preference stored in a
563         database, or get_browser_locale, which uses the Accept-Language
564         header.
565         """
566         if not hasattr(self, "_locale"):
567             self._locale = self.get_user_locale()
568             if not self._locale:
569                 self._locale = self.get_browser_locale()
570                 assert self._locale
571         return self._locale
572
573     def get_user_locale(self):
574         """Override to determine the locale from the authenticated user.
575
576         If None is returned, we use the Accept-Language header.
577         """
578         return None
579
580     def get_browser_locale(self, default="en_US"):
581         """Determines the user's locale from Accept-Language header.
582
583         See http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.4
584         """
585         if "Accept-Language" in self.request.headers:
586             languages = self.request.headers["Accept-Language"].split(",")
587             locales = []
588             for language in languages:
589                 parts = language.strip().split(";")
590                 if len(parts) > 1 and parts[1].startswith("q="):
591                     try:
592                         score = float(parts[1][2:])
593                     except (ValueError, TypeError):
594                         score = 0.0
595                 else:
596                     score = 1.0
597                 locales.append((parts[0], score))
598             if locales:
599                 locales.sort(key=lambda (l, s): s, reverse=True)
600                 codes = [l[0] for l in locales]
601                 return locale.get(*codes)
602         return locale.get(default)
603
604     @property
605     def current_user(self):
606         """The authenticated user for this request.
607
608         Determined by either get_current_user, which you can override to
609         set the user based on, e.g., a cookie. If that method is not
610         overridden, this method always returns None.
611
612         We lazy-load the current user the first time this method is called
613         and cache the result after that.
614         """
615         if not hasattr(self, "_current_user"):
616             self._current_user = self.get_current_user()
617         return self._current_user
618
619     def get_current_user(self):
620         """Override to determine the current user from, e.g., a cookie."""
621         return None
622
623     def get_login_url(self):
624         """Override to customize the login URL based on the request.
625
626         By default, we use the 'login_url' application setting.
627         """
628         self.require_setting("login_url", "@tornado.web.authenticated")
629         return self.application.settings["login_url"]
630
631     def get_template_path(self):
632         """Override to customize template path for each handler.
633
634         By default, we use the 'template_path' application setting.
635         Return None to load templates relative to the calling file.
636         """
637         return self.application.settings.get("template_path")
638
639     @property
640     def xsrf_token(self):
641         """The XSRF-prevention token for the current user/session.
642
643         To prevent cross-site request forgery, we set an '_xsrf' cookie
644         and include the same '_xsrf' value as an argument with all POST
645         requests. If the two do not match, we reject the form submission
646         as a potential forgery.
647
648         See http://en.wikipedia.org/wiki/Cross-site_request_forgery
649         """
650         if not hasattr(self, "_xsrf_token"):
651             token = self.get_cookie("_xsrf")
652             if not token:
653                 token = binascii.b2a_hex(uuid.uuid4().bytes)
654                 expires_days = 30 if self.current_user else None
655                 self.set_cookie("_xsrf", token, expires_days=expires_days)
656             self._xsrf_token = token
657         return self._xsrf_token
658
659     def check_xsrf_cookie(self):
660         """Verifies that the '_xsrf' cookie matches the '_xsrf' argument.
661
662         To prevent cross-site request forgery, we set an '_xsrf' cookie
663         and include the same '_xsrf' value as an argument with all POST
664         requests. If the two do not match, we reject the form submission
665         as a potential forgery.
666
667         See http://en.wikipedia.org/wiki/Cross-site_request_forgery
668         """
669         if self.request.headers.get("X-Requested-With") == "XMLHttpRequest":
670             return
671         token = self.get_argument("_xsrf", None)
672         if not token:
673             raise HTTPError(403, "'_xsrf' argument missing from POST")
674         if self.xsrf_token != token:
675             raise HTTPError(403, "XSRF cookie does not match POST argument")
676
677     def xsrf_form_html(self):
678         """An HTML <input/> element to be included with all POST forms.
679
680         It defines the _xsrf input value, which we check on all POST
681         requests to prevent cross-site request forgery. If you have set
682         the 'xsrf_cookies' application setting, you must include this
683         HTML within all of your HTML forms.
684
685         See check_xsrf_cookie() above for more information.
686         """
687         return '<input type="hidden" name="_xsrf" value="' + \
688             escape.xhtml_escape(self.xsrf_token) + '"/>'
689
690     def static_url(self, path):
691         """Returns a static URL for the given relative static file path.
692
693         This method requires you set the 'static_path' setting in your
694         application (which specifies the root directory of your static
695         files).
696
697         We append ?v=<signature> to the returned URL, which makes our
698         static file handler set an infinite expiration header on the
699         returned content. The signature is based on the content of the
700         file.
701
702         If this handler has a "include_host" attribute, we include the
703         full host for every static URL, including the "http://". Set
704         this attribute for handlers whose output needs non-relative static
705         path names.
706         """
707         self.require_setting("static_path", "static_url")
708         if not hasattr(RequestHandler, "_static_hashes"):
709             RequestHandler._static_hashes = {}
710         hashes = RequestHandler._static_hashes
711         if path not in hashes:
712             try:
713                 f = open(os.path.join(
714                     self.application.settings["static_path"], path))
715                 hashes[path] = hashlib.md5(f.read()).hexdigest()
716                 f.close()
717             except:
718                 logging.error("Could not open static file %r", path)
719                 hashes[path] = None
720         base = self.request.protocol + "://" + self.request.host \
721             if getattr(self, "include_host", False) else ""
722         static_url_prefix = self.settings.get('static_url_prefix', '/static/')
723         if hashes.get(path):
724             return base + static_url_prefix + path + "?v=" + hashes[path][:5]
725         else:
726             return base + static_url_prefix + path
727
728     def async_callback(self, callback, *args, **kwargs):
729         """Wrap callbacks with this if they are used on asynchronous requests.
730
731         Catches exceptions and properly finishes the request.
732         """
733         if callback is None:
734             return None
735         if args or kwargs:
736             callback = functools.partial(callback, *args, **kwargs)
737         def wrapper(*args, **kwargs):
738             try:
739                 return callback(*args, **kwargs)
740             except Exception, e:
741                 if self._headers_written:
742                     logging.error("Exception after headers written",
743                                   exc_info=True)
744                 else:
745                     self._handle_request_exception(e)
746         return wrapper
747
748     def require_setting(self, name, feature="this feature"):
749         """Raises an exception if the given app setting is not defined."""
750         if not self.application.settings.get(name):
751             raise Exception("You must define the '%s' setting in your "
752                             "application to use %s" % (name, feature))
753
754     def reverse_url(self, name, *args):
755         return self.application.reverse_url(name, *args)
756
757     def _execute(self, transforms, *args, **kwargs):
758         """Executes this request with the given output transforms."""
759         self._transforms = transforms
760         try:
761             if self.request.method not in self.SUPPORTED_METHODS:
762                 raise HTTPError(405)
763             # If XSRF cookies are turned on, reject form submissions without
764             # the proper cookie
765             if self.request.method == "POST" and \
766                self.application.settings.get("xsrf_cookies"):
767                 self.check_xsrf_cookie()
768             self.prepare()
769             if not self._finished:
770                 getattr(self, self.request.method.lower())(*args, **kwargs)
771                 if self._auto_finish and not self._finished:
772                     self.finish()
773         except Exception, e:
774             self._handle_request_exception(e)
775
776     def _generate_headers(self):
777         lines = [self.request.version + " " + str(self._status_code) + " " +
778                  httplib.responses[self._status_code]]
779         lines.extend(["%s: %s" % (n, v) for n, v in self._headers.iteritems()])
780         for cookie_dict in getattr(self, "_new_cookies", []):
781             for cookie in cookie_dict.values():
782                 lines.append("Set-Cookie: " + cookie.OutputString(None))
783         return "\r\n".join(lines) + "\r\n\r\n"
784
785     def _log(self):
786         if self._status_code < 400:
787             log_method = logging.info
788         elif self._status_code < 500:
789             log_method = logging.warning
790         else:
791             log_method = logging.error
792         request_time = 1000.0 * self.request.request_time()
793         log_method("%d %s %.2fms", self._status_code,
794                    self._request_summary(), request_time)
795
796     def _request_summary(self):
797         return self.request.method + " " + self.request.uri + " (" + \
798             self.request.remote_ip + ")"
799
800     def _handle_request_exception(self, e):
801         if isinstance(e, HTTPError):
802             if e.log_message:
803                 format = "%d %s: " + e.log_message
804                 args = [e.status_code, self._request_summary()] + list(e.args)
805                 logging.warning(format, *args)
806             if e.status_code not in httplib.responses:
807                 logging.error("Bad HTTP status code: %d", e.status_code)
808                 self.send_error(500, exception=e)
809             else:
810                 self.send_error(e.status_code, exception=e)
811         else:
812             logging.error("Uncaught exception %s\n%r", self._request_summary(),
813                           self.request, exc_info=e)
814             self.send_error(500, exception=e)
815
816     def _ui_module(self, name, module):
817         def render(*args, **kwargs):
818             if not hasattr(self, "_active_modules"):
819                 self._active_modules = {}
820             if name not in self._active_modules:
821                 self._active_modules[name] = module(self)
822             rendered = self._active_modules[name].render(*args, **kwargs)
823             return rendered
824         return render
825
826     def _ui_method(self, method):
827         return lambda *args, **kwargs: method(self, *args, **kwargs)
828
829
830 def asynchronous(method):
831     """Wrap request handler methods with this if they are asynchronous.
832
833     If this decorator is given, the response is not finished when the
834     method returns. It is up to the request handler to call self.finish()
835     to finish the HTTP request. Without this decorator, the request is
836     automatically finished when the get() or post() method returns.
837
838        class MyRequestHandler(web.RequestHandler):
839            @web.asynchronous
840            def get(self):
841               http = httpclient.AsyncHTTPClient()
842               http.fetch("http://friendfeed.com/", self._on_download)
843
844            def _on_download(self, response):
845               self.write("Downloaded!")
846               self.finish()
847
848     """
849     @functools.wraps(method)
850     def wrapper(self, *args, **kwargs):
851         if self.application._wsgi:
852             raise Exception("@asynchronous is not supported for WSGI apps")
853         self._auto_finish = False
854         return method(self, *args, **kwargs)
855     return wrapper
856
857
858 def removeslash(method):
859     """Use this decorator to remove trailing slashes from the request path.
860
861     For example, a request to '/foo/' would redirect to '/foo' with this
862     decorator. Your request handler mapping should use a regular expression
863     like r'/foo/*' in conjunction with using the decorator.
864     """
865     @functools.wraps(method)
866     def wrapper(self, *args, **kwargs):
867         if self.request.path.endswith("/"):
868             if self.request.method == "GET":
869                 uri = self.request.path.rstrip("/")
870                 if self.request.query: uri += "?" + self.request.query
871                 self.redirect(uri)
872                 return
873             raise HTTPError(404)
874         return method(self, *args, **kwargs)
875     return wrapper
876
877
878 def addslash(method):
879     """Use this decorator to add a missing trailing slash to the request path.
880
881     For example, a request to '/foo' would redirect to '/foo/' with this
882     decorator. Your request handler mapping should use a regular expression
883     like r'/foo/?' in conjunction with using the decorator.
884     """
885     @functools.wraps(method)
886     def wrapper(self, *args, **kwargs):
887         if not self.request.path.endswith("/"):
888             if self.request.method == "GET":
889                 uri = self.request.path + "/"
890                 if self.request.query: uri += "?" + self.request.query
891                 self.redirect(uri)
892                 return
893             raise HTTPError(404)
894         return method(self, *args, **kwargs)
895     return wrapper
896
897
898 class Application(object):
899     """A collection of request handlers that make up a web application.
900
901     Instances of this class are callable and can be passed directly to
902     HTTPServer to serve the application:
903
904         application = web.Application([
905             (r"/", MainPageHandler),
906         ])
907         http_server = httpserver.HTTPServer(application)
908         http_server.listen(8080)
909         ioloop.IOLoop.instance().start()
910
911     The constructor for this class takes in a list of URLSpec objects
912     or (regexp, request_class) tuples. When we receive requests, we
913     iterate over the list in order and instantiate an instance of the
914     first request class whose regexp matches the request path.
915
916     Each tuple can contain an optional third element, which should be a
917     dictionary if it is present. That dictionary is passed as keyword
918     arguments to the contructor of the handler. This pattern is used
919     for the StaticFileHandler below:
920
921         application = web.Application([
922             (r"/static/(.*)", web.StaticFileHandler, {"path": "/var/www"}),
923         ])
924
925     We support virtual hosts with the add_handlers method, which takes in
926     a host regular expression as the first argument:
927
928         application.add_handlers(r"www\.myhost\.com", [
929             (r"/article/([0-9]+)", ArticleHandler),
930         ])
931
932     You can serve static files by sending the static_path setting as a
933     keyword argument. We will serve those files from the /static/ URI
934     (this is configurable with the static_url_prefix setting),
935     and we will serve /favicon.ico and /robots.txt from the same directory.
936     """
937     def __init__(self, handlers=None, default_host="", transforms=None,
938                  wsgi=False, **settings):
939         if transforms is None:
940             self.transforms = []
941             if settings.get("gzip"):
942                 self.transforms.append(GZipContentEncoding)
943             self.transforms.append(ChunkedTransferEncoding)
944         else:
945             self.transforms = transforms
946         self.handlers = []
947         self.named_handlers = {}
948         self.default_host = default_host
949         self.settings = settings
950         self.ui_modules = {}
951         self.ui_methods = {}
952         self._wsgi = wsgi
953         self._load_ui_modules(settings.get("ui_modules", {}))
954         self._load_ui_methods(settings.get("ui_methods", {}))
955         if self.settings.get("static_path"):
956             path = self.settings["static_path"]
957             handlers = list(handlers or [])
958             static_url_prefix = settings.get("static_url_prefix",
959                                              "/static/")
960             handlers = [
961                 (re.escape(static_url_prefix) + r"(.*)", StaticFileHandler,
962                  dict(path=path)),
963                 (r"/(favicon\.ico)", StaticFileHandler, dict(path=path)),
964                 (r"/(robots\.txt)", StaticFileHandler, dict(path=path)),
965             ] + handlers
966         if handlers: self.add_handlers(".*$", handlers)
967
968         # Automatically reload modified modules
969         if self.settings.get("debug") and not wsgi:
970             import autoreload
971             autoreload.start()
972
973     def add_handlers(self, host_pattern, host_handlers):
974         """Appends the given handlers to our handler list."""
975         if not host_pattern.endswith("$"):
976             host_pattern += "$"
977         handlers = []
978         # The handlers with the wildcard host_pattern are a special
979         # case - they're added in the constructor but should have lower
980         # precedence than the more-precise handlers added later.
981         # If a wildcard handler group exists, it should always be last
982         # in the list, so insert new groups just before it.
983         if self.handlers and self.handlers[-1][0].pattern == '.*$':
984             self.handlers.insert(-1, (re.compile(host_pattern), handlers))
985         else:
986             self.handlers.append((re.compile(host_pattern), handlers))
987
988         for spec in host_handlers:
989             if type(spec) is type(()):
990                 assert len(spec) in (2, 3)
991                 pattern = spec[0]
992                 handler = spec[1]
993                 if len(spec) == 3:
994                     kwargs = spec[2]
995                 else:
996                     kwargs = {}
997                 spec = URLSpec(pattern, handler, kwargs)
998             handlers.append(spec)
999             if spec.name:
1000                 if spec.name in self.named_handlers:
1001                     logging.warning(
1002                         "Multiple handlers named %s; replacing previous value",
1003                         spec.name)
1004                 self.named_handlers[spec.name] = spec
1005
1006     def add_transform(self, transform_class):
1007         """Adds the given OutputTransform to our transform list."""
1008         self.transforms.append(transform_class)
1009
1010     def _get_host_handlers(self, request):
1011         host = request.host.lower().split(':')[0]
1012         for pattern, handlers in self.handlers:
1013             if pattern.match(host):
1014                 return handlers
1015         # Look for default host if not behind load balancer (for debugging)
1016         if "X-Real-Ip" not in request.headers:
1017             for pattern, handlers in self.handlers:
1018                 if pattern.match(self.default_host):
1019                     return handlers
1020         return None
1021
1022     def _load_ui_methods(self, methods):
1023         if type(methods) is types.ModuleType:
1024             self._load_ui_methods(dict((n, getattr(methods, n))
1025                                        for n in dir(methods)))
1026         elif isinstance(methods, list):
1027             for m in methods: self._load_ui_methods(m)
1028         else:
1029             for name, fn in methods.iteritems():
1030                 if not name.startswith("_") and hasattr(fn, "__call__") \
1031                    and name[0].lower() == name[0]:
1032                     self.ui_methods[name] = fn
1033
1034     def _load_ui_modules(self, modules):
1035         if type(modules) is types.ModuleType:
1036             self._load_ui_modules(dict((n, getattr(modules, n))
1037                                        for n in dir(modules)))
1038         elif isinstance(modules, list):
1039             for m in modules: self._load_ui_modules(m)
1040         else:
1041             assert isinstance(modules, dict)
1042             for name, cls in modules.iteritems():
1043                 try:
1044                     if issubclass(cls, UIModule):
1045                         self.ui_modules[name] = cls
1046                 except TypeError:
1047                     pass
1048
1049     def __call__(self, request):
1050         """Called by HTTPServer to execute the request."""
1051         transforms = [t(request) for t in self.transforms]
1052         handler = None
1053         args = []
1054         kwargs = {}
1055         handlers = self._get_host_handlers(request)
1056         if not handlers:
1057             handler = RedirectHandler(
1058                 request, "http://" + self.default_host + "/")
1059         else:
1060             for spec in handlers:
1061                 match = spec.regex.match(request.path)
1062                 if match:
1063                     handler = spec.handler_class(self, request, **spec.kwargs)
1064                     # Pass matched groups to the handler.  Since
1065                     # match.groups() includes both named and unnamed groups,
1066                     # we want to use either groups or groupdict but not both.
1067                     kwargs = dict((k, urllib.unquote(v))
1068                                   for (k, v) in match.groupdict().iteritems())
1069                     if kwargs:
1070                         args = []
1071                     else:
1072                         args = [urllib.unquote(s) for s in match.groups()]
1073                     break
1074             if not handler:
1075                 handler = ErrorHandler(self, request, 404)
1076
1077         # In debug mode, re-compile templates and reload static files on every
1078         # request so you don't need to restart to see changes
1079         if self.settings.get("debug"):
1080             if getattr(RequestHandler, "_templates", None):
1081               map(lambda loader: loader.reset(),
1082                   RequestHandler._templates.values())
1083             RequestHandler._static_hashes = {}
1084
1085         handler._execute(transforms, *args, **kwargs)
1086         return handler
1087
1088     def reverse_url(self, name, *args):
1089         """Returns a URL path for handler named `name`
1090
1091         The handler must be added to the application as a named URLSpec
1092         """
1093         if name in self.named_handlers:
1094             return self.named_handlers[name].reverse(*args)
1095         raise KeyError("%s not found in named urls" % name)
1096
1097
1098 class HTTPError(Exception):
1099     """An exception that will turn into an HTTP error response."""
1100     def __init__(self, status_code, log_message=None, *args):
1101         self.status_code = status_code
1102         self.log_message = log_message
1103         self.args = args
1104
1105     def __str__(self):
1106         message = "HTTP %d: %s" % (
1107             self.status_code, httplib.responses[self.status_code])
1108         if self.log_message:
1109             return message + " (" + (self.log_message % self.args) + ")"
1110         else:
1111             return message
1112
1113
1114 class ErrorHandler(RequestHandler):
1115     """Generates an error response with status_code for all requests."""
1116     def __init__(self, application, request, status_code):
1117         RequestHandler.__init__(self, application, request)
1118         self.set_status(status_code)
1119
1120     def prepare(self):
1121         raise HTTPError(self._status_code)
1122
1123
1124 class RedirectHandler(RequestHandler):
1125     """Redirects the client to the given URL for all GET requests.
1126
1127     You should provide the keyword argument "url" to the handler, e.g.:
1128
1129         application = web.Application([
1130             (r"/oldpath", web.RedirectHandler, {"url": "/newpath"}),
1131         ])
1132     """
1133     def __init__(self, application, request, url, permanent=True):
1134         RequestHandler.__init__(self, application, request)
1135         self._url = url
1136         self._permanent = permanent
1137
1138     def get(self):
1139         self.redirect(self._url, permanent=self._permanent)
1140
1141
1142 class StaticFileHandler(RequestHandler):
1143     """A simple handler that can serve static content from a directory.
1144
1145     To map a path to this handler for a static data directory /var/www,
1146     you would add a line to your application like:
1147
1148         application = web.Application([
1149             (r"/static/(.*)", web.StaticFileHandler, {"path": "/var/www"}),
1150         ])
1151
1152     The local root directory of the content should be passed as the "path"
1153     argument to the handler.
1154
1155     To support aggressive browser caching, if the argument "v" is given
1156     with the path, we set an infinite HTTP expiration header. So, if you
1157     want browsers to cache a file indefinitely, send them to, e.g.,
1158     /static/images/myimage.png?v=xxx.
1159     """
1160     def __init__(self, application, request, path):
1161         RequestHandler.__init__(self, application, request)
1162         self.root = os.path.abspath(path) + os.path.sep
1163
1164     def head(self, path):
1165         self.get(path, include_body=False)
1166
1167     def get(self, path, include_body=True):
1168         abspath = os.path.abspath(os.path.join(self.root, path))
1169         if not abspath.startswith(self.root):
1170             raise HTTPError(403, "%s is not in root static directory", path)
1171         if not os.path.exists(abspath):
1172             raise HTTPError(404)
1173         if not os.path.isfile(abspath):
1174             raise HTTPError(403, "%s is not a file", path)
1175
1176         stat_result = os.stat(abspath)
1177         modified = datetime.datetime.fromtimestamp(stat_result[stat.ST_MTIME])
1178
1179         self.set_header("Last-Modified", modified)
1180         if "v" in self.request.arguments:
1181             self.set_header("Expires", datetime.datetime.utcnow() + \
1182                                        datetime.timedelta(days=365*10))
1183             self.set_header("Cache-Control", "max-age=" + str(86400*365*10))
1184         else:
1185             self.set_header("Cache-Control", "public")
1186         mime_type, encoding = mimetypes.guess_type(abspath)
1187         if mime_type:
1188             self.set_header("Content-Type", mime_type)
1189
1190         self.set_extra_headers(path)
1191
1192         # Check the If-Modified-Since, and don't send the result if the
1193         # content has not been modified
1194         ims_value = self.request.headers.get("If-Modified-Since")
1195         if ims_value is not None:
1196             date_tuple = email.utils.parsedate(ims_value)
1197             if_since = datetime.datetime.fromtimestamp(time.mktime(date_tuple))
1198             if if_since >= modified:
1199                 self.set_status(304)
1200                 return
1201
1202         if not include_body:
1203             return
1204         self.set_header("Content-Length", stat_result[stat.ST_SIZE])
1205         file = open(abspath, "rb")
1206         try:
1207             self.write(file.read())
1208         finally:
1209             file.close()
1210
1211     def set_extra_headers(self, path):
1212       """For subclass to add extra headers to the response"""
1213       pass
1214
1215
1216 class FallbackHandler(RequestHandler):
1217     """A RequestHandler that wraps another HTTP server callback.
1218
1219     The fallback is a callable object that accepts an HTTPRequest,
1220     such as an Application or tornado.wsgi.WSGIContainer.  This is most
1221     useful to use both tornado RequestHandlers and WSGI in the same server.
1222     Typical usage:
1223         wsgi_app = tornado.wsgi.WSGIContainer(
1224             django.core.handlers.wsgi.WSGIHandler())
1225         application = tornado.web.Application([
1226             (r"/foo", FooHandler),
1227             (r".*", FallbackHandler, dict(fallback=wsgi_app),
1228         ])
1229     """
1230     def __init__(self, app, request, fallback):
1231         RequestHandler.__init__(self, app, request)
1232         self.fallback = fallback
1233
1234     def prepare(self):
1235         self.fallback(self.request)
1236         self._finished = True
1237
1238
1239 class OutputTransform(object):
1240     """A transform modifies the result of an HTTP request (e.g., GZip encoding)
1241
1242     A new transform instance is created for every request. See the
1243     ChunkedTransferEncoding example below if you want to implement a
1244     new Transform.
1245     """
1246     def __init__(self, request):
1247         pass
1248
1249     def transform_first_chunk(self, headers, chunk, finishing):
1250         return headers, chunk
1251
1252     def transform_chunk(self, chunk, finishing):
1253         return chunk
1254
1255
1256 class GZipContentEncoding(OutputTransform):
1257     """Applies the gzip content encoding to the response.
1258
1259     See http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.11
1260     """
1261     CONTENT_TYPES = set([
1262         "text/plain", "text/html", "text/css", "text/xml",
1263         "application/x-javascript", "application/xml", "application/atom+xml",
1264         "text/javascript", "application/json", "application/xhtml+xml"])
1265     MIN_LENGTH = 5
1266
1267     def __init__(self, request):
1268         self._gzipping = request.supports_http_1_1() and \
1269             "gzip" in request.headers.get("Accept-Encoding", "")
1270
1271     def transform_first_chunk(self, headers, chunk, finishing):
1272         if self._gzipping:
1273             ctype = headers.get("Content-Type", "").split(";")[0]
1274             self._gzipping = (ctype in self.CONTENT_TYPES) and \
1275                 (not finishing or len(chunk) >= self.MIN_LENGTH) and \
1276                 (finishing or "Content-Length" not in headers) and \
1277                 ("Content-Encoding" not in headers)
1278         if self._gzipping:
1279             headers["Content-Encoding"] = "gzip"
1280             self._gzip_value = cStringIO.StringIO()
1281             self._gzip_file = gzip.GzipFile(mode="w", fileobj=self._gzip_value)
1282             self._gzip_pos = 0
1283             chunk = self.transform_chunk(chunk, finishing)
1284             if "Content-Length" in headers:
1285                 headers["Content-Length"] = str(len(chunk))
1286         return headers, chunk
1287
1288     def transform_chunk(self, chunk, finishing):
1289         if self._gzipping:
1290             self._gzip_file.write(chunk)
1291             if finishing:
1292                 self._gzip_file.close()
1293             else:
1294                 self._gzip_file.flush()
1295             chunk = self._gzip_value.getvalue()
1296             if self._gzip_pos > 0:
1297                 chunk = chunk[self._gzip_pos:]
1298             self._gzip_pos += len(chunk)
1299         return chunk
1300
1301
1302 class ChunkedTransferEncoding(OutputTransform):
1303     """Applies the chunked transfer encoding to the response.
1304
1305     See http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.6.1
1306     """
1307     def __init__(self, request):
1308         self._chunking = request.supports_http_1_1()
1309
1310     def transform_first_chunk(self, headers, chunk, finishing):
1311         if self._chunking:
1312             # No need to chunk the output if a Content-Length is specified
1313             if "Content-Length" in headers or "Transfer-Encoding" in headers:
1314                 self._chunking = False
1315             else:
1316                 headers["Transfer-Encoding"] = "chunked"
1317                 chunk = self.transform_chunk(chunk, finishing)
1318         return headers, chunk
1319
1320     def transform_chunk(self, block, finishing):
1321         if self._chunking:
1322             # Don't write out empty chunks because that means END-OF-STREAM
1323             # with chunked encoding
1324             if block:
1325                 block = ("%x" % len(block)) + "\r\n" + block + "\r\n"
1326             if finishing:
1327                 block += "0\r\n\r\n"
1328         return block
1329
1330
1331 def authenticated(method):
1332     """Decorate methods with this to require that the user be logged in."""
1333     @functools.wraps(method)
1334     def wrapper(self, *args, **kwargs):
1335         if not self.current_user:
1336             if self.request.method == "GET":
1337                 url = self.get_login_url()
1338                 if "?" not in url:
1339                     url += "?" + urllib.urlencode(dict(next=self.request.uri))
1340                 self.redirect(url)
1341                 return
1342             raise HTTPError(403)
1343         return method(self, *args, **kwargs)
1344     return wrapper
1345
1346
1347 class UIModule(object):
1348     """A UI re-usable, modular unit on a page.
1349
1350     UI modules often execute additional queries, and they can include
1351     additional CSS and JavaScript that will be included in the output
1352     page, which is automatically inserted on page render.
1353     """
1354     def __init__(self, handler):
1355         self.handler = handler
1356         self.request = handler.request
1357         self.ui = handler.ui
1358         self.current_user = handler.current_user
1359         self.locale = handler.locale
1360
1361     def render(self, *args, **kwargs):
1362         raise NotImplementedError()
1363
1364     def embedded_javascript(self):
1365         """Returns a JavaScript string that will be embedded in the page."""
1366         return None
1367
1368     def javascript_files(self):
1369         """Returns a list of JavaScript files required by this module."""
1370         return None
1371
1372     def embedded_css(self):
1373         """Returns a CSS string that will be embedded in the page."""
1374         return None
1375
1376     def css_files(self):
1377         """Returns a list of CSS files required by this module."""
1378         return None
1379
1380     def html_head(self):
1381         """Returns a CSS string that will be put in the <head/> element"""
1382         return None
1383
1384     def html_body(self):
1385         """Returns an HTML string that will be put in the <body/> element"""
1386         return None
1387
1388     def render_string(self, path, **kwargs):
1389         return self.handler.render_string(path, **kwargs)
1390
1391 class URLSpec(object):
1392     """Specifies mappings between URLs and handlers."""
1393     def __init__(self, pattern, handler_class, kwargs={}, name=None):
1394         """Creates a URLSpec.
1395
1396         Parameters:
1397         pattern: Regular expression to be matched.  Any groups in the regex
1398             will be passed in to the handler's get/post/etc methods as
1399             arguments.
1400         handler_class: RequestHandler subclass to be invoked.
1401         kwargs (optional): A dictionary of additional arguments to be passed
1402             to the handler's constructor.
1403         name (optional): A name for this handler.  Used by
1404             Application.reverse_url.
1405         """
1406         if not pattern.endswith('$'):
1407             pattern += '$'
1408         self.regex = re.compile(pattern)
1409         self.handler_class = handler_class
1410         self.kwargs = kwargs
1411         self.name = name
1412         self._path, self._group_count = self._find_groups()
1413
1414     def _find_groups(self):
1415         """Returns a tuple (reverse string, group count) for a url.
1416
1417         For example: Given the url pattern /([0-9]{4})/([a-z-]+)/, this method
1418         would return ('/%s/%s/', 2).
1419         """
1420         pattern = self.regex.pattern
1421         if pattern.startswith('^'):
1422             pattern = pattern[1:]
1423         if pattern.endswith('$'):
1424             pattern = pattern[:-1]
1425
1426         if self.regex.groups != pattern.count('('):
1427             # The pattern is too complicated for our simplistic matching,
1428             # so we can't support reversing it.
1429             return (None, None)
1430
1431         pieces = []
1432         for fragment in pattern.split('('):
1433             if ')' in fragment:
1434                 paren_loc = fragment.index(')')
1435                 if paren_loc >= 0:
1436                     pieces.append('%s' + fragment[paren_loc + 1:])
1437             else:
1438                 pieces.append(fragment)
1439
1440         return (''.join(pieces), self.regex.groups)
1441
1442     def reverse(self, *args):
1443         assert self._path is not None, \
1444             "Cannot reverse url regex " + self.regex.pattern
1445         assert len(args) == self._group_count, "required number of arguments "\
1446             "not found"
1447         if not len(args):
1448             return self._path
1449         return self._path % tuple([str(a) for a in args])
1450
1451 url = URLSpec
1452
1453 def _utf8(s):
1454     if isinstance(s, unicode):
1455         return s.encode("utf-8")
1456     assert isinstance(s, str)
1457     return s
1458
1459
1460 def _unicode(s):
1461     if isinstance(s, str):
1462         try:
1463             return s.decode("utf-8")
1464         except UnicodeDecodeError:
1465             raise HTTPError(400, "Non-utf8 argument")
1466     assert isinstance(s, unicode)
1467     return s
1468
1469
1470 def _time_independent_equals(a, b):
1471     if len(a) != len(b):
1472         return False
1473     result = 0
1474     for x, y in zip(a, b):
1475         result |= ord(x) ^ ord(y)
1476     return result == 0
1477
1478
1479 class _O(dict):
1480     """Makes a dictionary behave like an object."""
1481     def __getattr__(self, name):
1482         try:
1483             return self[name]
1484         except KeyError:
1485             raise AttributeError(name)
1486
1487     def __setattr__(self, name, value):
1488         self[name] = value