Index: Lib/http/client.py =================================================================== --- Lib/http/client.py (revision 86923) +++ Lib/http/client.py (working copy) @@ -73,6 +73,7 @@ import socket from urllib.parse import urlsplit import warnings +import collections __all__ = ["HTTPResponse", "HTTPConnection", "HTTPException", "NotConnected", "UnknownProtocol", @@ -731,7 +732,11 @@ self.__state = _CS_IDLE def send(self, data): - """Send `data' to the server.""" + """Send `data' to the server. + ``data`` can be a string object, a bytes object, an array object, a + file-like object that supports a .read() method, or an iterable object. + """ + if self.sock is None: if self.auto_open: self.connect() @@ -763,9 +768,20 @@ if encode: datablock = datablock.encode("iso-8859-1") self.sock.sendall(datablock) - else: + + + + try: self.sock.sendall(data) + except TypeError: + if isinstance(data, collections.Iterable): + for d in data: self.sock.sendall(d) + else: + raise TypeError("data should be a bytes-like object or " + "an iterable, got %r" % type(it)) + + def _output(self, s): """Add a line of output to the current request buffer. Index: Lib/urllib/request.py =================================================================== --- Lib/urllib/request.py (revision 86923) +++ Lib/urllib/request.py (working copy) @@ -94,6 +94,7 @@ import socket import sys import time +import collections from urllib.error import URLError, HTTPError, ContentTooShortError from urllib.parse import ( @@ -1053,9 +1054,18 @@ 'Content-type', 'application/x-www-form-urlencoded') if not request.has_header('Content-length'): - request.add_unredirected_header( - 'Content-length', '%d' % len(data)) + if hasattr(data, '__len__') and not len(data): + request.add_unredirected_header('Content-length', '0') + elif isinstance(data, collections.Iterable): + try: + m = memoryview(data) + request.add_unredirected_header( + 'Content-length', '%d' % (len(m) * m.itemsize)) + except TypeError: + raise ValueError( + "No Content-Length specified for iterable body") + sel_host = host if request.has_proxy(): scheme, sel = splittype(request.selector) Index: Lib/test/test_httplib.py =================================================================== --- Lib/test/test_httplib.py (revision 86923) +++ Lib/test/test_httplib.py (working copy) @@ -216,20 +216,38 @@ self.assertTrue(sock.data.startswith(expected), '%r != %r' % (sock.data[:len(expected)], expected)) - def test_send(self): + def test_send_bytes(self): expected = b'this is a test this is only a test' conn = client.HTTPConnection('example.com') sock = FakeSocket(None) conn.sock = sock conn.send(expected) self.assertEqual(expected, sock.data) - sock.data = b'' - conn.send(array.array('b', expected)) - self.assertEqual(expected, sock.data) - sock.data = b'' + + def test_send_array(self): + expected = b'this is a test this is only a test' + conn = client.HTTPConnection('example.com') + sock = FakeSocket(None) + conn.sock = sock conn.send(io.BytesIO(expected)) self.assertEqual(expected, sock.data) + + def test_send_iter(self): + expected = b'GET /foo HTTP/1.1\r\nHost: example.com\r\n' \ + b'Accept-Encoding: identity\r\nContent-Length: 11\r\n' \ + b'\r\nonetwothree' + def body(): + yield b"one" + yield b"two" + yield b"three" + + conn = client.HTTPConnection('example.com') + sock = FakeSocket("") + conn.sock = sock + conn.request('GET', '/foo', body(), {'Content-Length': '11'}) + self.assertEquals(sock.data, expected) + def test_chunked(self): chunked_start = ( 'HTTP/1.1 200 OK\r\n' Index: Lib/test/test_urllib2.py =================================================================== --- Lib/test/test_urllib2.py (revision 86923) +++ Lib/test/test_urllib2.py (working copy) @@ -4,6 +4,7 @@ import os import io import socket +import array import urllib.request from urllib.request import Request, OpenerDirector @@ -795,6 +796,12 @@ # check adding of standard headers o.addheaders = [("Spam", "eggs")] + + req = Request("http://example.com/", array.array('I',[1,2,3])) + r = MockResponse(200, "OK", {}, "") + newreq = h.do_request_(req) + self.assertEqual(req.unredirected_hdrs["Content-length"], "12") + for data in "", None: # POST, GET req = Request("http://example.com/", data) r = MockResponse(200, "OK", {}, "") @@ -820,7 +827,22 @@ self.assertEqual(req.unredirected_hdrs["Content-type"], "bar") self.assertEqual(req.unredirected_hdrs["Host"], "baz") self.assertEqual(req.unredirected_hdrs["Spam"], "foo") + + # Check iterable body support + def iterable_body(): + yield "one" + yield "two" + yield "three" + for headers in {}, {"Content-Length": 11}: + req = Request("http://example.com/", iterable_body(), headers) + if not headers: + # Having an iterable body without a Content-Length should + # raise an exception + self.assertRaises(ValueError, h.do_request_, req) + else: + newreq = h.do_request_(req) + def test_http_doubleslash(self): # Checks the presence of any unnecessary double slash in url does not # break anything. Previously, a double slash directly after the host