Repository: tornadoweb/tornado
Pull request #1528 (2015-09-28 01:11:39)

Misc updates


Patch:
diff --git a/appveyor.yml b/appveyor.yml
deleted file mode 100644
index 98837bb6..00000000
--- a/appveyor.yml
+++ /dev/null
@@ -1,49 +0,0 @@
-# Appveyor is Windows CI: https://ci.appveyor.com/project/bdarnell/tornado
-environment:
-  # We only build with 3.5 because it works out of the box, while other
-  # versions require lots of machinery.
-  matrix:
-    - PYTHON: "C:\\Python35"
-      PYTHON_VERSION: "3.5.x"
-      PYTHON_ARCH: "32"
-
-    - PYTHON: "C:\\Python35-x64"
-      PYTHON_VERSION: "3.5.x"
-      PYTHON_ARCH: "64"
-
-install:
-  # Make sure the right python version is first on the PATH.
-  - "SET PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%"
-
-  # Check that we have the expected version and architecture for Python
-  - "python --version"
-  - "python -c \"import struct; print(struct.calcsize('P') * 8)\""
-
-  # Upgrade to the latest version of pip to avoid it displaying warnings
-  # about it being out of date.
-  - "pip install --disable-pip-version-check --user --upgrade pip"
-
-  - "pip install tox wheel"
-
-build: false  # Not a C# project, build stuff at the test step instead.
-
-test_script:
-  # Build the compiled extension and run the project tests.
-  # This is a bit of a hack that won't scale when py36 is out,
-  # but for now it lets us avoid duplication with .travis.yml and tox.ini.
-  # Running "py35-full" would be nice but it's failing on installing
-  # dependencies with no useful logs.
-  - "tox -e py35"
-
-after_test:
-  # If tests are successful, create binary packages for the project.
-  - "python setup.py bdist_wheel"
-  - ps: "ls dist"
-
-artifacts:
-  # Archive the generated packages in the ci.appveyor.com build report.
-  - path: dist\*
-
-#on_success:
-#  - TODO: upload the content of dist/*.whl to a public wheelhouse
-#
diff --git a/tornado/curl_httpclient.py b/tornado/curl_httpclient.py
index 9c948168..ae6f114a 100644
--- a/tornado/curl_httpclient.py
+++ b/tornado/curl_httpclient.py
@@ -454,8 +454,7 @@ class CurlAsyncHTTPClient(AsyncHTTPClient):
         if header_callback is not None:
             self.io_loop.add_callback(header_callback, header_line)
         # header_line as returned by curl includes the end-of-line characters.
-        # whitespace at the start should be preserved to allow multi-line headers
-        header_line = header_line.rstrip()
+        header_line = header_line.strip()
         if header_line.startswith("HTTP/"):
             headers.clear()
             try:
diff --git a/tornado/httpclient.py b/tornado/httpclient.py
index c2e68623..9179227b 100644
--- a/tornado/httpclient.py
+++ b/tornado/httpclient.py
@@ -603,9 +603,12 @@ class HTTPError(Exception):
     """
     def __init__(self, code, message=None, response=None):
         self.code = code
-        message = message or httputil.responses.get(code, "Unknown")
+        self.message = message or httputil.responses.get(code, "Unknown")
         self.response = response
-        Exception.__init__(self, "HTTP %d: %s" % (self.code, message))
+        super(HTTPError, self).__init__(code, message, response)
+
+    def __str__(self):
+        return "HTTP %d: %s" % (self.code, self.message)
 
 
 class _RequestProxy(object):
diff --git a/tornado/ioloop.py b/tornado/ioloop.py
index a49e3ca9..195475ba 100644
--- a/tornado/ioloop.py
+++ b/tornado/ioloop.py
@@ -914,7 +914,7 @@ class PollIOLoop(IOLoop):
             # with other threads, or waking logic will induce a race.
             with self._callback_lock:
                 if self._closing:
-                    raise RuntimeError("IOLoop is closing")
+                    return
                 list_empty = not self._callbacks
                 self._callbacks.append(functools.partial(
                     stack_context.wrap(callback), *args, **kwargs))
@@ -927,7 +927,7 @@ class PollIOLoop(IOLoop):
                     self._waker.wake()
         else:
             if self._closing:
-                raise RuntimeError("IOLoop is closing")
+                return
             # If we're on the IOLoop's thread, we don't need the lock,
             # since we don't need to wake anyone, just add the
             # callback. Blindly insert into self._callbacks. This is
diff --git a/tornado/iostream.py b/tornado/iostream.py
index 1e677104..0bad0acd 100644
--- a/tornado/iostream.py
+++ b/tornado/iostream.py
@@ -725,18 +725,22 @@ class BaseIOStream(object):
         to read (i.e. the read returns EWOULDBLOCK or equivalent).  On
         error closes the socket and raises an exception.
         """
-        try:
-            chunk = self.read_from_fd()
-        except (socket.error, IOError, OSError) as e:
-            # ssl.SSLError is a subclass of socket.error
-            if self._is_connreset(e):
-                # Treat ECONNRESET as a connection close rather than
-                # an error to minimize log spam  (the exception will
-                # be available on self.error for apps that care).
+        while True:
+            try:
+                chunk = self.read_from_fd()
+            except (socket.error, IOError, OSError) as e:
+                if errno_from_exception(e) == errno.EINTR:
+                    continue
+                # ssl.SSLError is a subclass of socket.error
+                if self._is_connreset(e):
+                    # Treat ECONNRESET as a connection close rather than
+                    # an error to minimize log spam  (the exception will
+                    # be available on self.error for apps that care).
+                    self.close(exc_info=True)
+                    return
                 self.close(exc_info=True)
-                return
-            self.close(exc_info=True)
-            raise
+                raise
+            break
         if chunk is None:
             return 0
         self._read_buffer.append(chunk)
diff --git a/tornado/web.py b/tornado/web.py
index dd0df5ef..f6ae767b 100644
--- a/tornado/web.py
+++ b/tornado/web.py
@@ -1473,7 +1473,7 @@ class RequestHandler(object):
         if isinstance(e, Finish):
             # Not an error; just finish the request without logging.
             if not self._finished:
-                self.finish()
+                self.finish(*e.args)
             return
         try:
             self.log_exception(*sys.exc_info())
@@ -2063,7 +2063,7 @@ class HTTPError(Exception):
         determined automatically from ``status_code``, but can be used
         to use a non-standard numeric code.
     """
-    def __init__(self, status_code, log_message=None, *args, **kwargs):
+    def __init__(self, status_code=500, log_message=None, *args, **kwargs):
         self.status_code = status_code
         self.log_message = log_message
         self.args = args
@@ -2084,10 +2084,14 @@ class HTTPError(Exception):
 class Finish(Exception):
     """An exception that ends the request without producing an error response.
 
-    When `Finish` is raised in a `RequestHandler`, the request will end
-    (calling `RequestHandler.finish` if it hasn't already been called),
-    but the outgoing response will not be modified and the error-handling
-    methods (including `RequestHandler.write_error`) will not be called.
+    When `Finish` is raised in a `RequestHandler`, the request will
+    end (calling `RequestHandler.finish` if it hasn't already been
+    called), but the error-handling methods (including
+    `RequestHandler.write_error`) will not be called.
+
+    If `Finish()` was created with no arguments, the pending response
+    will be sent as-is. If `Finish()` was given an argument, that
+    argument will be passed to `RequestHandler.finish()`.
 
     This can be a more convenient way to implement custom error pages
     than overriding ``write_error`` (especially in library code)::
@@ -2096,6 +2100,10 @@ class Finish(Exception):
             self.set_status(401)
             self.set_header('WWW-Authenticate', 'Basic realm="something"')
             raise Finish()
+
+    .. versionchanged:: 4.3
+       Arguments passed to ``Finish()`` will be passed on to
+       `RequestHandler.finish`.
     """
     pass
 
diff --git a/tornado/websocket.py b/tornado/websocket.py
index 58262cc3..3b8b9a1f 100644
--- a/tornado/websocket.py
+++ b/tornado/websocket.py
@@ -208,12 +208,15 @@ class WebSocketHandler(tornado.web.RequestHandler):
         .. versionchanged:: 3.2
            `WebSocketClosedError` was added (previously a closed connection
            would raise an `AttributeError`)
+
+        .. versionchanged:: 4.3
+           Returns a `.Future` which can be used for flow control.
         """
         if self.ws_connection is None:
             raise WebSocketClosedError()
         if isinstance(message, dict):
             message = tornado.escape.json_encode(message)
-        self.ws_connection.write_message(message, binary=binary)
+        return self.ws_connection.write_message(message, binary=binary)
 
     def select_subprotocol(self, subprotocols):
         """Invoked when a new WebSocket requests specific subprotocols.
@@ -671,7 +674,7 @@ class WebSocketProtocol13(WebSocketProtocol):
         frame += data
         self._wire_bytes_out += len(frame)
         try:
-            self.stream.write(frame)
+            return self.stream.write(frame)
         except StreamClosedError:
             self._abort()
 
@@ -688,7 +691,7 @@ class WebSocketProtocol13(WebSocketProtocol):
         if self._compressor:
             message = self._compressor.compress(message)
             flags |= self.RSV1
-        self._write_frame(True, opcode, message, flags=flags)
+        return self._write_frame(True, opcode, message, flags=flags)
 
     def write_ping(self, data):
         """Send ping frame."""
@@ -970,7 +973,7 @@ class WebSocketClientConnection(simple_httpclient._HTTPConnection):
 
     def write_message(self, message, binary=False):
         """Sends a message to the WebSocket server."""
-        self.protocol.write_message(message, binary)
+        return self.protocol.write_message(message, binary)
 
     def read_message(self, callback=None):
         """Reads a message from the WebSocket server.


Test patch:
diff --git a/tornado/test/httpclient_test.py b/tornado/test/httpclient_test.py
index 0d6e6eed..c5738f27 100644
--- a/tornado/test/httpclient_test.py
+++ b/tornado/test/httpclient_test.py
@@ -5,6 +5,7 @@ from __future__ import absolute_import, division, print_function, with_statement
 import base64
 import binascii
 from contextlib import closing
+import copy
 import functools
 import sys
 import threading
@@ -371,30 +372,6 @@ Transfer-Encoding: chunked
                     "response=%r, value=%r, container=%r" %
                     (resp.body, value, container))
 
-    def test_multi_line_headers(self):
-        # Multi-line http headers are rare but rfc-allowed
-        # http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2
-        sock, port = bind_unused_port()
-        with closing(sock):
-            def write_response(stream, request_data):
-                stream.write(b"""\
-HTTP/1.1 200 OK
-X-XSS-Protection: 1;
-\tmode=block
-
-""".replace(b"\n", b"\r\n"), callback=stream.close)
-
-            def accept_callback(conn, address):
-                stream = IOStream(conn, io_loop=self.io_loop)
-                stream.read_until(b"\r\n\r\n",
-                                  functools.partial(write_response, stream))
-            netutil.add_accept_handler(sock, accept_callback, self.io_loop)
-            self.http_client.fetch("http://127.0.0.1:%d/" % port, self.stop)
-            resp = self.wait()
-            resp.rethrow()
-            self.assertEqual(resp.headers['X-XSS-Protection'], "1; mode=block")
-            self.io_loop.remove_handler(sock.fileno())
-
     def test_304_with_content_length(self):
         # According to the spec 304 responses SHOULD NOT include
         # Content-Length or other entity headers, but some servers do it
@@ -629,3 +606,15 @@ class HTTPRequestTestCase(unittest.TestCase):
         request = HTTPRequest('http://example.com', if_modified_since=http_date)
         self.assertEqual(request.headers,
                          {'If-Modified-Since': format_timestamp(http_date)})
+
+
+class HTTPErrorTestCase(unittest.TestCase):
+    def test_copy(self):
+        e = HTTPError(403)
+        e2 = copy.copy(e)
+        self.assertIsNot(e, e2)
+        self.assertEqual(e.code, e2.code)
+
+    def test_str(self):
+        e = HTTPError(403)
+        self.assertEqual(str(e), "HTTP 403: Forbidden")
diff --git a/tornado/test/web_test.py b/tornado/test/web_test.py
index 558c9ae8..5ddd68de 100644
--- a/tornado/test/web_test.py
+++ b/tornado/test/web_test.py
@@ -15,6 +15,7 @@ from tornado.web import RequestHandler, authenticated, Application, asynchronous
 
 import binascii
 import contextlib
+import copy
 import datetime
 import email.utils
 import itertools
@@ -1204,7 +1205,6 @@ class StaticFileTest(WebTestCase):
         # is probably a packaging error).
         self.assertEqual(response.code, 403)
 
-    @unittest.skipIf(os.name != 'posix', 'non-posix OS')
     def test_root_static_path(self):
         # Sometimes people set the StaticFileHandler's path to '/'
         # to disable Tornado's path validation (in conjunction with
@@ -2544,15 +2544,19 @@ class FinishExceptionTest(SimpleHandlerTestCase):
         def get(self):
             self.set_status(401)
             self.set_header('WWW-Authenticate', 'Basic realm="something"')
-            self.write('authentication required')
-            raise Finish()
+            if self.get_argument('finish_value', ''):
+                raise Finish('authentication required')
+            else:
+                self.write('authentication required')
+                raise Finish()
 
     def test_finish_exception(self):
-        response = self.fetch('/')
-        self.assertEqual(response.code, 401)
-        self.assertEqual('Basic realm="something"',
-                         response.headers.get('WWW-Authenticate'))
-        self.assertEqual(b'authentication required', response.body)
+        for url in ['/', '/?finish_value=1']:
+            response = self.fetch(url)
+            self.assertEqual(response.code, 401)
+            self.assertEqual('Basic realm="something"',
+                             response.headers.get('WWW-Authenticate'))
+            self.assertEqual(b'authentication required', response.body)
 
 
 @wsgi_safe
@@ -2671,3 +2675,12 @@ class RequestSummaryTest(SimpleHandlerTestCase):
     def test_missing_remote_ip(self):
         resp = self.fetch("/")
         self.assertEqual(resp.body, b"GET / (None)")
+
+
+class HTTPErrorTest(unittest.TestCase):
+    def test_copy(self):
+        e = HTTPError(403, reason="Go away")
+        e2 = copy.copy(e)
+        self.assertIsNot(e, e2)
+        self.assertEqual(e.status_code, e2.status_code)
+        self.assertEqual(e.reason, e2.reason)
diff --git a/tornado/test/websocket_test.py b/tornado/test/websocket_test.py
index 23a4324c..7b47214d 100644
--- a/tornado/test/websocket_test.py
+++ b/tornado/test/websocket_test.py
@@ -130,7 +130,7 @@ class WebSocketTest(WebSocketBaseTestCase):
     @gen_test
     def test_websocket_gen(self):
         ws = yield self.ws_connect('/echo')
-        ws.write_message('hello')
+        yield ws.write_message('hello')
         response = yield ws.read_message()
         self.assertEqual(response, 'hello')
         yield self.close(ws)