]> git.ipfire.org Git - thirdparty/tornado.git/commitdiff
Merge remote-tracking branch 'birknilson/static_version_override' into merge
authorBen Darnell <ben@bendarnell.com>
Sun, 19 May 2013 17:11:53 +0000 (13:11 -0400)
committerBen Darnell <ben@bendarnell.com>
Sun, 19 May 2013 17:11:53 +0000 (13:11 -0400)
Conflicts:
tornado/test/web_test.py

1  2 
tornado/test/web_test.py
tornado/web.py

index a4a089833de7666fdc1228bad1e5f4069bfe3397,c5e6ce2d871d4fa7fece970c985723c4d3fea4f0..c732e922bf785be1efe3db5dbd6225ddb791866a
@@@ -712,62 -502,50 +712,60 @@@ class ErrorResponseTest(WebTestCase)
                  url("/write_error", WriteErrorHandler),
                  url("/get_error_html", GetErrorHtmlHandler),
                  url("/failed_write_error", FailedWriteErrorHandler),
 -                ])
 +                ]
  
      def test_default(self):
 -        response = self.fetch("/default")
 -        self.assertEqual(response.code, 500)
 -        self.assertTrue(b("500: Internal Server Error") in response.body)
 +        with ExpectLog(app_log, "Uncaught exception"):
 +            response = self.fetch("/default")
 +            self.assertEqual(response.code, 500)
 +            self.assertTrue(b"500: Internal Server Error" in response.body)
  
 -        response = self.fetch("/default?status=503")
 -        self.assertEqual(response.code, 503)
 -        self.assertTrue(b("503: Service Unavailable") in response.body)
 +            response = self.fetch("/default?status=503")
 +            self.assertEqual(response.code, 503)
 +            self.assertTrue(b"503: Service Unavailable" in response.body)
  
      def test_write_error(self):
 -        response = self.fetch("/write_error")
 -        self.assertEqual(response.code, 500)
 -        self.assertEqual(b("Exception: ZeroDivisionError"), response.body)
 +        with ExpectLog(app_log, "Uncaught exception"):
 +            response = self.fetch("/write_error")
 +            self.assertEqual(response.code, 500)
 +            self.assertEqual(b"Exception: ZeroDivisionError", response.body)
  
 -        response = self.fetch("/write_error?status=503")
 -        self.assertEqual(response.code, 503)
 -        self.assertEqual(b("Status: 503"), response.body)
 +            response = self.fetch("/write_error?status=503")
 +            self.assertEqual(response.code, 503)
 +            self.assertEqual(b"Status: 503", response.body)
  
      def test_get_error_html(self):
 -        response = self.fetch("/get_error_html")
 -        self.assertEqual(response.code, 500)
 -        self.assertEqual(b("Exception: ZeroDivisionError"), response.body)
 +        with ExpectLog(app_log, "Uncaught exception"):
 +            response = self.fetch("/get_error_html")
 +            self.assertEqual(response.code, 500)
 +            self.assertEqual(b"Exception: ZeroDivisionError", response.body)
  
 -        response = self.fetch("/get_error_html?status=503")
 -        self.assertEqual(response.code, 503)
 -        self.assertEqual(b("Status: 503"), response.body)
 +            response = self.fetch("/get_error_html?status=503")
 +            self.assertEqual(response.code, 503)
 +            self.assertEqual(b"Status: 503", response.body)
  
      def test_failed_write_error(self):
 -        response = self.fetch("/failed_write_error")
 -        self.assertEqual(response.code, 500)
 -        self.assertEqual(b(""), response.body)
 +        with ExpectLog(app_log, "Uncaught exception"):
 +            response = self.fetch("/failed_write_error")
 +            self.assertEqual(response.code, 500)
 +            self.assertEqual(b"", response.body)
  
 -class StaticFileTest(AsyncHTTPTestCase, LogTrapTestCase):
 -    def get_app(self):
 +
 +@wsgi_safe
 +class StaticFileTest(WebTestCase):
 +    # The expected MD5 hash of robots.txt, used in tests that call
 +    # StaticFileHandler.get_version
 +    robots_txt_hash = b"f71d20196d4caf35b6a670db8c70b03d"
 +
 +    def get_handlers(self):
          class StaticUrlHandler(RequestHandler):
              def get(self, path):
-                 self.write(self.static_url(path))
+                 with_v = int(self.get_argument('include_version', 1))
+                 self.write(self.static_url(path, include_version=with_v))
  
-         class AbsoluteStaticUrlHandler(RequestHandler):
+         class AbsoluteStaticUrlHandler(StaticUrlHandler):
              include_host = True
  
-             def get(self, path):
-                 self.write(self.static_url(path))
          class OverrideStaticUrlHandler(RequestHandler):
              def get(self, path):
                  do_include = bool(self.get_argument("include_host"))
  
      def test_absolute_static_url(self):
          response = self.fetch("/abs_static_url/robots.txt")
 -        self.assertEqual(response.body,
 -                         utf8(self.get_url("/") + "static/robots.txt?v=f71d2"))
 +        self.assertEqual(response.body, (
 +            utf8(self.get_url("/")) +
 +            b"static/robots.txt?v=" +
 +            self.robots_txt_hash
 +        ))
  
 -        self.assertEqual(response.body, b("/static/robots.txt"))
+     def test_relative_version_exclusion(self):
+         response = self.fetch("/static_url/robots.txt?include_version=0")
++        self.assertEqual(response.body, b"/static/robots.txt")
+     def test_absolute_version_exclusion(self):
+         response = self.fetch("/abs_static_url/robots.txt?include_version=0")
+         self.assertEqual(response.body,
+                          utf8(self.get_url("/") + "static/robots.txt"))
      def test_include_host_override(self):
          self._trigger_include_host_check(False)
          self._trigger_include_host_check(True)
          response = self.fetch(path % int(include_host))
          self.assertEqual(response.body, utf8(str(True)))
  
 -class CustomStaticFileTest(AsyncHTTPTestCase, LogTrapTestCase):
 -    def get_app(self):
 -        class MyStaticFileHandler(StaticFileHandler):
 -            def get(self, path):
 -                path = self.parse_url_path(path)
 -                assert path == "foo.txt"
 -                self.write("bar")
 +    def test_static_304_if_modified_since(self):
 +        response1 = self.fetch("/static/robots.txt")
 +        response2 = self.fetch("/static/robots.txt", headers={
 +            'If-Modified-Since': response1.headers['Last-Modified']})
 +        self.assertEqual(response2.code, 304)
 +        self.assertTrue('Content-Length' not in response2.headers)
 +        self.assertTrue('Last-Modified' not in response2.headers)
 +
 +    def test_static_304_if_none_match(self):
 +        response1 = self.fetch("/static/robots.txt")
 +        response2 = self.fetch("/static/robots.txt", headers={
 +            'If-None-Match': response1.headers['Etag']})
 +        self.assertEqual(response2.code, 304)
 +
 +    def test_static_if_modified_since_pre_epoch(self):
 +        # On windows, the functions that work with time_t do not accept
 +        # negative values, and at least one client (processing.js) seems
 +        # to use if-modified-since 1/1/1960 as a cache-busting technique.
 +        response = self.fetch("/static/robots.txt", headers={
 +                'If-Modified-Since': 'Fri, 01 Jan 1960 00:00:00 GMT'})
 +        self.assertEqual(response.code, 200)
 +
 +    def test_static_if_modified_since_time_zone(self):
 +        # Instead of the value from Last-Modified, make requests with times
 +        # chosen just before and after the known modification time
 +        # of the file to ensure that the right time zone is being used
 +        # when parsing If-Modified-Since.
 +        stat = os.stat(os.path.join(os.path.dirname(__file__),
 +                                    'static/robots.txt'))
 +
 +        response = self.fetch('/static/robots.txt', headers={
 +                'If-Modified-Since': format_timestamp(stat.st_mtime - 1)})
 +        self.assertEqual(response.code, 200)
 +        response = self.fetch('/static/robots.txt', headers={
 +                'If-Modified-Since': format_timestamp(stat.st_mtime + 1)})
 +        self.assertEqual(response.code, 304)
 +
 +    def test_static_etag(self):
 +        response = self.fetch('/static/robots.txt')
 +        self.assertEqual(utf8(response.headers.get("Etag")),
 +                         b'"' + self.robots_txt_hash + b'"')
 +
 +    def test_static_with_range(self):
 +        response = self.fetch('/static/robots.txt', headers={
 +                'Range': 'bytes=0-9'})
 +        self.assertEqual(response.code, 206)
 +        self.assertEqual(response.body, b"User-agent")
 +        self.assertEqual(utf8(response.headers.get("Etag")),
 +                         b'"' + self.robots_txt_hash + b'"')
 +        self.assertEqual(response.headers.get("Content-Length"), "10")
 +        self.assertEqual(response.headers.get("Content-Range"),
 +                         "0-9/26")
 +
 +    def test_static_with_range_end_edge(self):
 +        response = self.fetch('/static/robots.txt', headers={
 +                'Range': 'bytes=22-'})
 +        self.assertEqual(response.body, b": /\n")
 +        self.assertEqual(response.headers.get("Content-Length"), "4")
 +        self.assertEqual(response.headers.get("Content-Range"),
 +                         "22-25/26")
 +
 +    def test_static_with_range_neg_end(self):
 +        response = self.fetch('/static/robots.txt', headers={
 +                'Range': 'bytes=-4'})
 +        self.assertEqual(response.body, b": /\n")
 +        self.assertEqual(response.headers.get("Content-Length"), "4")
 +        self.assertEqual(response.headers.get("Content-Range"),
 +                         "22-25/26")
 +
 +    def test_static_invalid_range(self):
 +        response = self.fetch('/static/robots.txt', headers={
 +                'Range': 'asdf'})
 +        self.assertEqual(response.code, 416)
 +
 +    def test_static_head(self):
 +        response = self.fetch('/static/robots.txt', method='HEAD')
 +        self.assertEqual(response.code, 200)
 +        # No body was returned, but we did get the right content length.
 +        self.assertEqual(response.body, b'')
 +        self.assertEqual(response.headers['Content-Length'], '26')
 +        self.assertEqual(utf8(response.headers['Etag']),
 +                         b'"' + self.robots_txt_hash + b'"')
 +
 +    def test_static_head_range(self):
 +        response = self.fetch('/static/robots.txt', method='HEAD',
 +                              headers={'Range': 'bytes=1-4'})
 +        self.assertEqual(response.code, 206)
 +        self.assertEqual(response.body, b'')
 +        self.assertEqual(response.headers['Content-Length'], '4')
 +        self.assertEqual(utf8(response.headers['Etag']),
 +                         b'"' + self.robots_txt_hash + b'"')
 +
 +    def test_static_range_if_none_match(self):
 +        response = self.fetch('/static/robots.txt', headers={
 +            'Range': 'bytes=1-4',
 +            'If-None-Match': b'"' + self.robots_txt_hash + b'"'})
 +        self.assertEqual(response.code, 304)
 +        self.assertEqual(response.body, b'')
 +        self.assertTrue('Content-Length' not in response.headers)
 +        self.assertEqual(utf8(response.headers['Etag']),
 +                         b'"' + self.robots_txt_hash + b'"')
 +
 +    def test_static_404(self):
 +        response = self.fetch('/static/blarg')
 +        self.assertEqual(response.code, 404)
 +
 +
 +@wsgi_safe
 +class StaticDefaultFilenameTest(WebTestCase):
 +    def get_app_kwargs(self):
 +        return dict(static_path=os.path.join(os.path.dirname(__file__),
 +                                             'static'),
 +                    static_handler_args=dict(default_filename='index.html'))
 +
 +    def get_handlers(self):
 +        return []
 +
 +    def test_static_default_filename(self):
 +        response = self.fetch('/static/dir/', follow_redirects=False)
 +        self.assertEqual(response.code, 200)
 +        self.assertEqual(b'this is the index\n', response.body)
 +
 +    def test_static_default_redirect(self):
 +        response = self.fetch('/static/dir', follow_redirects=False)
 +        self.assertEqual(response.code, 301)
 +        self.assertTrue(response.headers['Location'].endswith('/static/dir/'))
 +
 +
  
 +@wsgi_safe
 +class CustomStaticFileTest(WebTestCase):
 +    def get_handlers(self):
 +        class MyStaticFileHandler(StaticFileHandler):
              @classmethod
-             def make_static_url(cls, settings, path):
-                 version = cls.get_version(settings, path)
+             def make_static_url(cls, settings, path, include_version=True):
+                 version_hash = cls.get_version(settings, path)
                  extension_index = path.rindex('.')
                  before_version = path[:extension_index]
                  after_version = path[(extension_index + 1):]
-                 return '/static/%s.%s.%s' % (before_version, version,
 -                return '/static/%s.%s.%s' % (before_version, 42, after_version)
++                return '/static/%s.%s.%s' % (before_version, version_hash,
 +                                             after_version)
  
 -            @classmethod
 -            def parse_url_path(cls, url_path):
 +            def parse_url_path(self, url_path):
                  extension_index = url_path.rindex('.')
                  version_index = url_path.rindex('.', 0, extension_index)
                  return '%s%s' % (url_path[:version_index],
diff --cc tornado/web.py
index 998380753e73a49b72dd0204a31c65507c385b79,e0ebdcb46aedf91a41ad6fc6c9a8f36863fa4a74..765316912660626f2c9c09737621d33aaf4af6a4
@@@ -987,17 -886,18 +987,18 @@@ class RequestHandler(object)
          return '<input type="hidden" name="_xsrf" value="' + \
              escape.xhtml_escape(self.xsrf_token) + '"/>'
  
-     def static_url(self, path, include_host=None):
+     def static_url(self, path, include_host=None, include_version=True):
          """Returns a static URL for the given relative static file path.
  
 -        This method requires you set the 'static_path' setting in your
 +        This method requires you set the ``static_path`` setting in your
          application (which specifies the root directory of your static
          files).
  
 -        We append ?v=<signature> to the returned URL, which makes our
 +        We append ``?v=<signature>`` to the returned URL, which makes our
          static file handler set an infinite expiration header on the
          returned content. The signature is based on the content of the
-         file.
+         file. This behavior can be avoided in case the ``include_version``
+         is set to False, i.e ?v=<signature> is not appended.
  
          By default this method returns URLs relative to the current
          host, but if ``include_host`` is true the URL returned will be
@@@ -1969,26 -1560,21 +1971,34 @@@ class StaticFileHandler(RequestHandler)
          ``settings`` is the `Application.settings` dictionary.  ``path``
          is the static path being requested.  The url returned should be
          relative to the current host.
+         ``include_version`` determines whether the generated URL should
+         include the query string containing the version hash of the
+         file corresponding to the given ``path``.
          """
-         static_url_prefix = settings.get('static_url_prefix', '/static/')
+         url = settings.get('static_url_prefix', '/static/') + path
+         if not include_version:
+             return url
          version_hash = cls.get_version(settings, path)
-         if version_hash:
-             return static_url_prefix + path + "?v=" + version_hash
-         return static_url_prefix + path
+         if not version_hash:
+             return url
+         return '%s?v=%s' % (url, version_hash)
  
 +    def parse_url_path(self, url_path):
 +        """Converts a static URL path into a filesystem path.
 +
 +        ``url_path`` is the path component of the URL with
 +        ``static_url_prefix`` removed.  The return value should be
 +        filesystem path relative to ``static_path``.
 +
 +        This is the inverse of `make_static_url`.
 +        """
 +        if os.path.sep != "/":
 +            url_path = url_path.replace("/", os.path.sep)
 +        return url_path
 +
      @classmethod
      def get_version(cls, settings, path):
          """Generate the version string to be used in static URLs.