Merge pull request #4428 from minrk/tornado-3.1

bump minimum tornado version to 3.1.0
This commit is contained in:
Thomas Kluyver 2013-10-23 14:36:25 -07:00
commit daf15735bd
3 changed files with 17 additions and 190 deletions

View File

@ -17,21 +17,15 @@ Authors:
#----------------------------------------------------------------------------- #-----------------------------------------------------------------------------
import datetime
import email.utils
import functools import functools
import hashlib
import json import json
import logging import logging
import mimetypes
import os import os
import stat import stat
import sys import sys
import threading
import traceback import traceback
from tornado import web from tornado import web
from tornado import websocket
try: try:
from tornado.log import app_log from tornado.log import app_log
@ -39,65 +33,12 @@ except ImportError:
app_log = logging.getLogger() app_log = logging.getLogger()
from IPython.config import Application from IPython.config import Application
from IPython.external.decorator import decorator
from IPython.utils.path import filefind from IPython.utils.path import filefind
from IPython.utils.jsonutil import date_default
# UF_HIDDEN is a stat flag not defined in the stat module. # UF_HIDDEN is a stat flag not defined in the stat module.
# It is used by BSD to indicate hidden files. # It is used by BSD to indicate hidden files.
UF_HIDDEN = getattr(stat, 'UF_HIDDEN', 32768) UF_HIDDEN = getattr(stat, 'UF_HIDDEN', 32768)
#-----------------------------------------------------------------------------
# Monkeypatch for Tornado <= 2.1.1 - Remove when no longer necessary!
#-----------------------------------------------------------------------------
# Google Chrome, as of release 16, changed its websocket protocol number. The
# parts tornado cares about haven't really changed, so it's OK to continue
# accepting Chrome connections, but as of Tornado 2.1.1 (the currently released
# version as of Oct 30/2011) the version check fails, see the issue report:
# https://github.com/facebook/tornado/issues/385
# This issue has been fixed in Tornado post 2.1.1:
# https://github.com/facebook/tornado/commit/84d7b458f956727c3b0d6710
# Here we manually apply the same patch as above so that users of IPython can
# continue to work with an officially released Tornado. We make the
# monkeypatch version check as narrow as possible to limit its effects; once
# Tornado 2.1.1 is no longer found in the wild we'll delete this code.
import tornado
if tornado.version_info <= (2,1,1):
def _execute(self, transforms, *args, **kwargs):
from tornado.websocket import WebSocketProtocol8, WebSocketProtocol76
self.open_args = args
self.open_kwargs = kwargs
# The difference between version 8 and 13 is that in 8 the
# client sends a "Sec-Websocket-Origin" header and in 13 it's
# simply "Origin".
if self.request.headers.get("Sec-WebSocket-Version") in ("7", "8", "13"):
self.ws_connection = WebSocketProtocol8(self)
self.ws_connection.accept_connection()
elif self.request.headers.get("Sec-WebSocket-Version"):
self.stream.write(tornado.escape.utf8(
"HTTP/1.1 426 Upgrade Required\r\n"
"Sec-WebSocket-Version: 8\r\n\r\n"))
self.stream.close()
else:
self.ws_connection = WebSocketProtocol76(self)
self.ws_connection.accept_connection()
websocket.WebSocketHandler._execute = _execute
del _execute
#----------------------------------------------------------------------------- #-----------------------------------------------------------------------------
# Top-level handlers # Top-level handlers
#----------------------------------------------------------------------------- #-----------------------------------------------------------------------------
@ -359,20 +300,20 @@ HTTPError = web.HTTPError
class FileFindHandler(web.StaticFileHandler): class FileFindHandler(web.StaticFileHandler):
"""subclass of StaticFileHandler for serving files from a search path""" """subclass of StaticFileHandler for serving files from a search path"""
# cache search results, don't search for files more than once
_static_paths = {} _static_paths = {}
# _lock is needed for tornado < 2.2.0 compat
_lock = threading.Lock() # protects _static_hashes
def initialize(self, path, default_filename=None): def initialize(self, path, default_filename=None):
if isinstance(path, basestring): if isinstance(path, basestring):
path = [path] path = [path]
self.roots = tuple(
self.root = tuple(
os.path.abspath(os.path.expanduser(p)) + os.sep for p in path os.path.abspath(os.path.expanduser(p)) + os.sep for p in path
) )
self.default_filename = default_filename self.default_filename = default_filename
@classmethod @classmethod
def locate_file(cls, path, roots): def get_absolute_path(cls, roots, path):
"""locate a file to serve on our static file search path""" """locate a file to serve on our static file search path"""
with cls._lock: with cls._lock:
if path in cls._static_paths: if path in cls._static_paths:
@ -383,130 +324,17 @@ class FileFindHandler(web.StaticFileHandler):
# empty string should always give exists=False # empty string should always give exists=False
return '' return ''
# os.path.abspath strips a trailing /
# it needs to be temporarily added back for requests to root/
if not (abspath + os.sep).startswith(roots):
raise HTTPError(403, "%s is not in root static directory", path)
cls._static_paths[path] = abspath cls._static_paths[path] = abspath
return abspath return abspath
def get(self, path, include_body=True): def validate_absolute_path(self, root, absolute_path):
path = self.parse_url_path(path) """check if the file should be served (raises 404, 403, etc.)"""
for root in self.root:
if (absolute_path + os.sep).startswith(root):
break
# begin subclass override return super(FileFindHandler, self).validate_absolute_path(root, absolute_path)
abspath = self.locate_file(path, self.roots)
# end subclass override
if os.path.isdir(abspath) and self.default_filename is not None:
# need to look at the request.path here for when path is empty
# but there is some prefix to the path that was already
# trimmed by the routing
if not self.request.path.endswith("/"):
self.redirect(self.request.path + "/")
return
abspath = os.path.join(abspath, self.default_filename)
if not os.path.exists(abspath):
raise HTTPError(404)
if not os.path.isfile(abspath):
raise HTTPError(403, "%s is not a file", path)
stat_result = os.stat(abspath)
modified = datetime.datetime.utcfromtimestamp(stat_result[stat.ST_MTIME])
self.set_header("Last-Modified", modified)
mime_type, encoding = mimetypes.guess_type(abspath)
if mime_type:
self.set_header("Content-Type", mime_type)
cache_time = self.get_cache_time(path, modified, mime_type)
if cache_time > 0:
self.set_header("Expires", datetime.datetime.utcnow() + \
datetime.timedelta(seconds=cache_time))
self.set_header("Cache-Control", "max-age=" + str(cache_time))
else:
self.set_header("Cache-Control", "public")
self.set_extra_headers(path)
# Check the If-Modified-Since, and don't send the result if the
# content has not been modified
ims_value = self.request.headers.get("If-Modified-Since")
if ims_value is not None:
date_tuple = email.utils.parsedate(ims_value)
if_since = datetime.datetime(*date_tuple[:6])
if if_since >= modified:
self.set_status(304)
return
with open(abspath, "rb") as file:
data = file.read()
hasher = hashlib.sha1()
hasher.update(data)
self.set_header("Etag", '"%s"' % hasher.hexdigest())
if include_body:
self.write(data)
else:
assert self.request.method == "HEAD"
self.set_header("Content-Length", len(data))
@classmethod
def get_version(cls, settings, path):
"""Generate the version string to be used in static URLs.
This method may be overridden in subclasses (but note that it
is a class method rather than a static method). The default
implementation uses a hash of the file's contents.
``settings`` is the `Application.settings` dictionary and ``path``
is the relative location of the requested asset on the filesystem.
The returned value should be a string, or ``None`` if no version
could be determined.
"""
# begin subclass override:
static_paths = settings['static_path']
if isinstance(static_paths, basestring):
static_paths = [static_paths]
roots = tuple(
os.path.abspath(os.path.expanduser(p)) + os.sep for p in static_paths
)
try:
abs_path = filefind(path, roots)
except IOError:
app_log.error("Could not find static file %r", path)
return None
# end subclass override
with cls._lock:
hashes = cls._static_hashes
if abs_path not in hashes:
try:
f = open(abs_path, "rb")
hashes[abs_path] = hashlib.md5(f.read()).hexdigest()
f.close()
except Exception:
app_log.error("Could not open static file %r", path)
hashes[abs_path] = None
hsh = hashes.get(abs_path)
if hsh:
return hsh[:5]
return None
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``.
"""
if os.sep != "/":
url_path = url_path.replace("/", os.sep)
return url_path
class TrailingSlashHandler(web.RequestHandler): class TrailingSlashHandler(web.RequestHandler):
"""Simple redirect handler that strips trailing slashes """Simple redirect handler that strips trailing slashes

View File

@ -42,8 +42,8 @@ from jinja2 import Environment, FileSystemLoader
from zmq.eventloop import ioloop from zmq.eventloop import ioloop
ioloop.install() ioloop.install()
# check for tornado 2.1.0 # check for tornado 3.1.0
msg = "The IPython Notebook requires tornado >= 2.1.0" msg = "The IPython Notebook requires tornado >= 3.1.0"
try: try:
import tornado import tornado
except ImportError: except ImportError:
@ -52,7 +52,7 @@ try:
version_info = tornado.version_info version_info = tornado.version_info
except AttributeError: except AttributeError:
raise ImportError(msg + ", but you have < 1.1.0") raise ImportError(msg + ", but you have < 1.1.0")
if version_info < (2,1,0): if version_info < (3,1,0):
raise ImportError(msg + ", but you have %s" % tornado.version) raise ImportError(msg + ", but you have %s" % tornado.version)
from tornado import httpserver from tornado import httpserver
@ -542,9 +542,8 @@ class NotebookApp(BaseIPythonApplication):
# hook up tornado 3's loggers to our app handlers # hook up tornado 3's loggers to our app handlers
for name in ('access', 'application', 'general'): for name in ('access', 'application', 'general'):
logger = logging.getLogger('tornado.%s' % name) logger = logging.getLogger('tornado.%s' % name)
logger.propagate = False logger.parent = self.log
logger.setLevel(self.log.level) logger.setLevel(self.log.level)
logger.handlers = self.log.handlers
def init_webapp(self): def init_webapp(self):
"""initialize tornado webapp and httpserver""" """initialize tornado webapp and httpserver"""
@ -692,8 +691,8 @@ class NotebookApp(BaseIPythonApplication):
@catch_config_error @catch_config_error
def initialize(self, argv=None): def initialize(self, argv=None):
self.init_logging()
super(NotebookApp, self).initialize(argv) super(NotebookApp, self).initialize(argv)
self.init_logging()
self.init_kernel_argv() self.init_kernel_argv()
self.init_configurables() self.init_configurables()
self.init_components() self.init_components()

View File

@ -273,7 +273,7 @@ if 'setuptools' in sys.modules:
zmq = 'pyzmq>=2.1.11', zmq = 'pyzmq>=2.1.11',
doc = 'Sphinx>=0.3', doc = 'Sphinx>=0.3',
test = 'nose>=0.10.1', test = 'nose>=0.10.1',
notebook = ['tornado>=2.0', 'pyzmq>=2.1.11', 'jinja2'], notebook = ['tornado>=3.1', 'pyzmq>=2.1.11', 'jinja2'],
nbconvert = ['pygments', 'jinja2', 'Sphinx>=0.3'] nbconvert = ['pygments', 'jinja2', 'Sphinx>=0.3']
) )
everything = set() everything = set()