Skip to content

Commit

Permalink
Merge pull request ipython#4428 from minrk/tornado-3.1
Browse files Browse the repository at this point in the history
bump minimum tornado version to 3.1.0
  • Loading branch information
takluyver committed Oct 23, 2013
2 parents f89a7a8 + 36e3884 commit e9bd685
Show file tree
Hide file tree
Showing 4 changed files with 23 additions and 192 deletions.
194 changes: 11 additions & 183 deletions IPython/html/base/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,87 +17,28 @@
#-----------------------------------------------------------------------------


import datetime
import email.utils
import functools
import hashlib
import json
import logging
import mimetypes
import os
import stat
import sys
import threading
import traceback

from tornado import web
from tornado import websocket

try:
from tornado.log import app_log
except ImportError:
app_log = logging.getLogger()

from IPython.config import Application
from IPython.external.decorator import decorator
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.
# It is used by BSD to indicate hidden files.
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
#-----------------------------------------------------------------------------
Expand Down Expand Up @@ -359,20 +300,20 @@ def wrapper(self, *args, **kwargs):
class FileFindHandler(web.StaticFileHandler):
"""subclass of StaticFileHandler for serving files from a search path"""

# cache search results, don't search for files more than once
_static_paths = {}
# _lock is needed for tornado < 2.2.0 compat
_lock = threading.Lock() # protects _static_hashes

def initialize(self, path, default_filename=None):
if isinstance(path, basestring):
path = [path]
self.roots = tuple(

self.root = tuple(
os.path.abspath(os.path.expanduser(p)) + os.sep for p in path
)
self.default_filename = default_filename

@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"""
with cls._lock:
if path in cls._static_paths:
Expand All @@ -382,131 +323,18 @@ def locate_file(cls, path, roots):
except IOError:
# empty string should always give exists=False
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
return abspath

def get(self, path, include_body=True):
path = self.parse_url_path(path)

# begin subclass override
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
def validate_absolute_path(self, root, absolute_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

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.
return super(FileFindHandler, self).validate_absolute_path(root, absolute_path)

``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):
"""Simple redirect handler that strips trailing slashes
Expand Down
11 changes: 5 additions & 6 deletions IPython/html/notebookapp.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@
from zmq.eventloop import ioloop
ioloop.install()

# check for tornado 2.1.0
msg = "The IPython Notebook requires tornado >= 2.1.0"
# check for tornado 3.1.0
msg = "The IPython Notebook requires tornado >= 3.1.0"
try:
import tornado
except ImportError:
Expand All @@ -52,7 +52,7 @@
version_info = tornado.version_info
except AttributeError:
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)

from tornado import httpserver
Expand Down Expand Up @@ -542,9 +542,8 @@ def init_logging(self):
# hook up tornado 3's loggers to our app handlers
for name in ('access', 'application', 'general'):
logger = logging.getLogger('tornado.%s' % name)
logger.propagate = False
logger.parent = self.log
logger.setLevel(self.log.level)
logger.handlers = self.log.handlers

def init_webapp(self):
"""initialize tornado webapp and httpserver"""
Expand Down Expand Up @@ -692,8 +691,8 @@ def init_components(self):

@catch_config_error
def initialize(self, argv=None):
self.init_logging()
super(NotebookApp, self).initialize(argv)
self.init_logging()
self.init_kernel_argv()
self.init_configurables()
self.init_components()
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,7 @@ def run(self):
zmq = 'pyzmq>=2.1.11',
doc = 'Sphinx>=0.3',
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']
)
everything = set()
Expand Down
8 changes: 6 additions & 2 deletions setupext/setupext.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,8 +137,12 @@ def check_for_tornado():
print_status('tornado', "no (required for notebook)")
return False
else:
print_status('tornado', tornado.version)
return True
if getattr(tornado, 'version_info', (0,)) < (3,1):
print_status('tornado', "no (have %s, but require >= 3.1.0)" % tornado.version)
return False
else:
print_status('tornado', tornado.version)
return True

def check_for_readline():
from distutils.version import LooseVersion
Expand Down

0 comments on commit e9bd685

Please sign in to comment.