# HG changeset patch # Parent 8d802f93a55a42050ecba90e17dd5778e1709c25 Issue #26721: Change StreamRequestHandler.wfile to BufferedIOBase diff -r 8d802f93a55a Doc/library/http.server.rst --- a/Doc/library/http.server.rst Sat May 14 06:17:43 2016 +0000 +++ b/Doc/library/http.server.rst Sat May 14 07:16:38 2016 +0000 @@ -99,8 +99,8 @@ .. attribute:: rfile - Contains an input stream, positioned at the start of the optional input - data. + An :class:`io.BufferedIOBase` input stream, ready to read from + the start of the optional input data. .. attribute:: wfile @@ -108,6 +108,9 @@ client. Proper adherence to the HTTP protocol must be used when writing to this stream. + .. versionchanged:: 3.6 + This is an :class:`io.BufferedIOBase` stream. + :class:`BaseHTTPRequestHandler` has the following attributes: .. attribute:: server_version diff -r 8d802f93a55a Doc/library/socketserver.rst --- a/Doc/library/socketserver.rst Sat May 14 06:17:43 2016 +0000 +++ b/Doc/library/socketserver.rst Sat May 14 07:16:38 2016 +0000 @@ -407,6 +407,15 @@ read or written, respectively, to get the request data or return data to the client. + The :attr:`rfile` attributes of both classes support the + :class:`io.BufferedIOBase` readable interface, and + :attr:`DatagramRequestHandler.wfile` supports the + :class:`io.BufferedIOBase` writable interface. + + .. versionchanged:: 3.6 + :attr:`StreamRequestHandler.wfile` also supports the + :class:`io.BufferedIOBase` writable interface. + Examples -------- diff -r 8d802f93a55a Doc/whatsnew/3.6.rst --- a/Doc/whatsnew/3.6.rst Sat May 14 06:17:43 2016 +0000 +++ b/Doc/whatsnew/3.6.rst Sat May 14 07:16:38 2016 +0000 @@ -268,6 +268,12 @@ protocol. (Contributed by Aviv Palivoda in :issue:`26404`.) +The :attr:`~socketserver.StreamRequestHandler.wfile` attribute of +:class:`~socketserver.StreamRequestHandler` classes now implements +the :class:`io.BufferedIOBase` writable interface. In particular, +calling :meth:`~io.BufferedIOBase.write` is now guaranteed to send the +data in full. (Contributed by Martin Panter in :issue:`26721`.) + telnetlib --------- diff -r 8d802f93a55a Lib/socketserver.py --- a/Lib/socketserver.py Sat May 14 06:17:43 2016 +0000 +++ b/Lib/socketserver.py Sat May 14 07:16:38 2016 +0000 @@ -132,6 +132,7 @@ import threading except ImportError: import dummy_threading as threading +from io import BytesIO, BufferedIOBase from time import monotonic as time __all__ = ["BaseServer", "TCPServer", "UDPServer", "ForkingUDPServer", @@ -740,7 +741,10 @@ self.connection.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, True) self.rfile = self.connection.makefile('rb', self.rbufsize) - self.wfile = self.connection.makefile('wb', self.wbufsize) + if self.wbufsize == 0: + self.wfile = _SocketWriter(self.connection) + else: + self.wfile = self.connection.makefile('wb', self.wbufsize) def finish(self): if not self.wfile.closed: @@ -753,13 +757,29 @@ self.wfile.close() self.rfile.close() +class _SocketWriter(BufferedIOBase): + """Simple writable BufferedIOBase implementation for a socket + + Does not hold data in a buffer, avoiding any need to call flush().""" + + def __init__(self, sock): + self._sock = sock + + def writable(self): + return True + + def write(self, b): + self._sock.sendall(b) + return len(b) + + def fileno(self): + return self._sock.fileno() class DatagramRequestHandler(BaseRequestHandler): """Define self.rfile and self.wfile for datagram sockets.""" def setup(self): - from io import BytesIO self.packet, self.socket = self.request self.rfile = BytesIO(self.packet) self.wfile = BytesIO() diff -r 8d802f93a55a Lib/test/test_socketserver.py --- a/Lib/test/test_socketserver.py Sat May 14 06:17:43 2016 +0000 +++ b/Lib/test/test_socketserver.py Sat May 14 07:16:38 2016 +0000 @@ -3,6 +3,7 @@ """ import contextlib +import io import os import select import signal @@ -375,6 +376,48 @@ self.active_children.clear() +class SocketWriterTest(unittest.TestCase): + def test_basics(self): + class Handler(socketserver.StreamRequestHandler): + def handle(self): + self.server.wfile = self.wfile + self.server.wfile_fileno = self.wfile.fileno() + self.server.request_fileno = self.request.fileno() + + server = socketserver.TCPServer((HOST, 0), Handler) + self.addCleanup(server.server_close) + s = socket.socket( + server.address_family, socket.SOCK_STREAM, socket.IPPROTO_TCP) + with s: + s.connect(server.server_address) + server.handle_request() + self.assertIsInstance(server.wfile, io.BufferedIOBase) + self.assertEqual(server.wfile_fileno, server.request_fileno) + + @unittest.skipUnless(threading, 'Threading required for this test.') + def test_write(self): + class Handler(socketserver.StreamRequestHandler): + def handle(self): + self.server.sent = self.wfile.write(b'write data\n') + # Should be sent immediately, without requiring flush() + self.server.received = self.rfile.readline() + + server = socketserver.TCPServer((HOST, 0), Handler) + self.addCleanup(server.server_close) + background = threading.Thread(target=server.handle_request) + background.start() + s = socket.socket( + server.address_family, socket.SOCK_STREAM, socket.IPPROTO_TCP) + with s, s.makefile('rb') as reader: + s.connect(server.server_address) + response = reader.readline() + s.sendall(b'client response\n') + background.join() + self.assertEqual(server.sent, len(response)) + self.assertEqual(response, b'write data\n') + self.assertEqual(server.received, b'client response\n') + + class MiscTestCase(unittest.TestCase): def test_all(self): diff -r 8d802f93a55a Lib/test/test_wsgiref.py --- a/Lib/test/test_wsgiref.py Sat May 14 06:17:43 2016 +0000 +++ b/Lib/test/test_wsgiref.py Sat May 14 07:16:38 2016 +0000 @@ -1,4 +1,6 @@ from unittest import mock +from test import support +from test.test_httpservers import NoLogRequestHandler from unittest import TestCase from wsgiref.util import setup_testing_defaults from wsgiref.headers import Headers @@ -7,12 +9,14 @@ from wsgiref.validate import validator from wsgiref.simple_server import WSGIServer, WSGIRequestHandler from wsgiref.simple_server import make_server +from http.client import HTTPConnection from io import StringIO, BytesIO, BufferedReader from socketserver import BaseServer from platform import python_implementation import os import re +import signal import sys import unittest @@ -245,6 +249,47 @@ ], out.splitlines()) + def test_interrupted_write(self): + # BaseHandler._write() and _flush() have to write all data, even if + # it takes multiple send() calls + threading = support.import_module("threading") + SIGUSR1 = support.get_attribute(signal, "SIGUSR1") + + def app(environ, start_response): + start_response("200 OK", []) + return [bytes(support.SOCK_MAX_SIZE)] + + class WsgiHandler(NoLogRequestHandler, WSGIRequestHandler): + pass + + server = make_server(support.HOST, 0, app, handler_class=WsgiHandler) + self.addCleanup(server.server_close) + interrupted = threading.Event() + + def signal_handler(signum, frame): + interrupted.set() + + original = signal.signal(SIGUSR1, signal_handler) + self.addCleanup(signal.signal, SIGUSR1, original) + received = None + + def run_client(): + http = HTTPConnection(*server.server_address) + http.request("GET", "/") + with http.getresponse() as response: + response.read(100) + os.kill(os.getpid(), SIGUSR1) + interrupted.wait() + nonlocal received + received = len(response.read()) + http.close() + + background = threading.Thread(target=run_client) + background.start() + server.handle_request() + background.join() + self.assertEqual(received, support.SOCK_MAX_SIZE - 100) + class UtilityTests(TestCase): diff -r 8d802f93a55a Misc/NEWS --- a/Misc/NEWS Sat May 14 06:17:43 2016 +0000 +++ b/Misc/NEWS Sat May 14 07:16:38 2016 +0000 @@ -277,6 +277,10 @@ Library ------- +- Issue #26721: Change the socketserver.StreamRequestHandler.wfile attribute + to implement BufferedIOBase. In particular, the write() method no longer + does partial writes. + - Issue #26039: zipfile.ZipFile.open() can now be used to write data into a ZIP file, as well as for extracting data. Patch by Thomas Kluyver.