From 07c78be0b4723deb62acebab50888cc59b1e4eb2 Mon Sep 17 00:00:00 2001 From: Facundo Batista Date: Fri, 23 Mar 2007 18:54:07 +0000 Subject: [PATCH] Added a 'create_connect()' function to socket.py, which creates a connection with an optional timeout, and modified httplib.py to use this function in HTTPConnection. Applies patch 1676823. --- Doc/lib/libhttplib.tex | 14 ++++++- Doc/lib/libsocket.tex | 9 +++++ Lib/httplib.py | 23 ++--------- Lib/socket.py | 29 ++++++++++++++ Lib/test/test_httplib.py | 42 +++++++++++++++++++- Lib/test/test_socket.py | 85 +++++++++++++++++++++++++++++++++++++++- Misc/NEWS | 4 ++ 7 files changed, 181 insertions(+), 25 deletions(-) diff --git a/Doc/lib/libhttplib.tex b/Doc/lib/libhttplib.tex index 557ee3d0c25..67371a4bb7c 100644 --- a/Doc/lib/libhttplib.tex +++ b/Doc/lib/libhttplib.tex @@ -26,18 +26,28 @@ that use HTTP and HTTPS. The module provides the following classes: -\begin{classdesc}{HTTPConnection}{host\optional{, port}} +\begin{classdesc}{HTTPConnection}{host\optional{, port\optional{, + strict\optional{, timeout}}}} An \class{HTTPConnection} instance represents one transaction with an HTTP server. It should be instantiated passing it a host and optional port number. If no port number is passed, the port is extracted from the host string if it has the form \code{\var{host}:\var{port}}, else the default HTTP port (80) is -used. For example, the following calls all create instances that connect to +used. +When True the optional parameter \var{strict} +causes \code{BadStatusLine} to be raised if the status line can't be parsed +as a valid HTTP/1.0 or 1.1 status line. If the optional \var{timeout} +parameter is given, connection attempts will timeout after that many +seconds (if no timeout is passed, or is passed as None, the global default +timeout setting is used). + +For example, the following calls all create instances that connect to the server at the same host and port: \begin{verbatim} >>> h1 = httplib.HTTPConnection('www.cwi.nl') >>> h2 = httplib.HTTPConnection('www.cwi.nl:80') >>> h3 = httplib.HTTPConnection('www.cwi.nl', 80) +>>> h3 = httplib.HTTPConnection('www.cwi.nl', 80, timeout=10) \end{verbatim} \versionadded{2.0} \end{classdesc} diff --git a/Doc/lib/libsocket.tex b/Doc/lib/libsocket.tex index e0ce0a54d86..c63b52b20bc 100644 --- a/Doc/lib/libsocket.tex +++ b/Doc/lib/libsocket.tex @@ -170,6 +170,15 @@ supported on this platform. \versionadded{2.3} \end{datadesc} +\begin{funcdesc}{create_connection}{address\optional{, timeout}} +Connects to the \var{address} received (as usual, a pair host/port), with +an optional timeout for the connection. Specially useful for higher-level +protocols, it is not normally used directly from application-level code. +Passing the optional \var{timeout} parameter will set the timeout on the +socket instance (if not present, or passed as None, the global default +timeout setting is used). +\end{funcdesc} + \begin{funcdesc}{getaddrinfo}{host, port\optional{, family\optional{, socktype\optional{, proto\optional{, flags}}}}} diff --git a/Lib/httplib.py b/Lib/httplib.py index 213d80c2b43..008f0a497c7 100644 --- a/Lib/httplib.py +++ b/Lib/httplib.py @@ -625,7 +625,8 @@ class HTTPConnection: debuglevel = 0 strict = 0 - def __init__(self, host, port=None, strict=None): + def __init__(self, host, port=None, strict=None, timeout=None): + self.timeout = timeout self.sock = None self._buffer = [] self.__response = None @@ -658,25 +659,7 @@ class HTTPConnection: def connect(self): """Connect to the host and port specified in __init__.""" - msg = "getaddrinfo returns an empty list" - for res in socket.getaddrinfo(self.host, self.port, 0, - socket.SOCK_STREAM): - af, socktype, proto, canonname, sa = res - try: - self.sock = socket.socket(af, socktype, proto) - if self.debuglevel > 0: - print "connect: (%s, %s)" % (self.host, self.port) - self.sock.connect(sa) - except socket.error, msg: - if self.debuglevel > 0: - print 'connect fail:', (self.host, self.port) - if self.sock: - self.sock.close() - self.sock = None - continue - break - if not self.sock: - raise socket.error, msg + self.sock = socket.create_connection((self.host,self.port), self.timeout) def close(self): """Close the connection to the HTTP server.""" diff --git a/Lib/socket.py b/Lib/socket.py index 0082e7656e2..c03e884aba9 100644 --- a/Lib/socket.py +++ b/Lib/socket.py @@ -24,6 +24,7 @@ inet_ntoa() -- convert 32-bit packed format IP to string (123.45.67.89) ssl() -- secure socket layer support (only available if configured) socket.getdefaulttimeout() -- get the default timeout value socket.setdefaulttimeout() -- set the default timeout value +create_connection() -- connects to an address, with an optional timeout [*] not available on all platforms! @@ -412,3 +413,31 @@ class _fileobject(object): if not line: raise StopIteration return line + + +def create_connection(address, timeout=None): + """Connect to address (host, port) with an optional timeout. + + Provides access to socketobject timeout for higher-level + protocols. Passing a timeout will set the timeout on the + socket instance (if not present, or passed as None, the + default global timeout setting will be used). + """ + + msg = "getaddrinfo returns an empty list" + host, port = address + for res in getaddrinfo(host, port, 0, SOCK_STREAM): + af, socktype, proto, canonname, sa = res + sock = None + try: + sock = socket(af, socktype, proto) + if timeout is not None: + sock.settimeout(timeout) + sock.connect(sa) + return sock + + except error, msg: + if sock is not None: + sock.close() + + raise error, msg diff --git a/Lib/test/test_httplib.py b/Lib/test/test_httplib.py index 90a4e550093..a39a3eb5016 100644 --- a/Lib/test/test_httplib.py +++ b/Lib/test/test_httplib.py @@ -1,6 +1,7 @@ import httplib import StringIO import sys +import socket from unittest import TestCase @@ -149,8 +150,47 @@ class OfflineTest(TestCase): def test_responses(self): self.assertEquals(httplib.responses[httplib.NOT_FOUND], "Not Found") +PORT = 50003 +HOST = "localhost" + +class TimeoutTest(TestCase): + + def setUp(self): + self.serv = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.serv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + global PORT + PORT = test_support.bind_port(self.serv, HOST, PORT) + self.serv.listen(1) + + def tearDown(self): + self.serv.close() + self.serv = None + + def testTimeoutAttribute(self): + '''This will prove that the timeout gets through + HTTPConnection and into the socket. + ''' + # default + httpConn = httplib.HTTPConnection(HOST, PORT) + httpConn.connect() + self.assertTrue(httpConn.sock.gettimeout() is None) + + # a value + httpConn = httplib.HTTPConnection(HOST, PORT, timeout=10) + httpConn.connect() + self.assertEqual(httpConn.sock.gettimeout(), 10) + + # None, having other default + previous = socket.getdefaulttimeout() + socket.setdefaulttimeout(10) + httpConn = httplib.HTTPConnection(HOST, PORT, timeout=None) + httpConn.connect() + socket.setdefaulttimeout(previous) + self.assertEqual(httpConn.sock.gettimeout(), 10) + + def test_main(verbose=None): - test_support.run_unittest(HeaderTests, OfflineTest, BasicTest) + test_support.run_unittest(HeaderTests, OfflineTest, BasicTest, TimeoutTest) if __name__ == '__main__': test_main() diff --git a/Lib/test/test_socket.py b/Lib/test/test_socket.py index c56c3b71d7a..a7ebe044960 100644 --- a/Lib/test/test_socket.py +++ b/Lib/test/test_socket.py @@ -75,7 +75,7 @@ class ThreadableTest: Note, the server setup function cannot call any blocking functions that rely on the client thread during setup, - unless serverExplicityReady() is called just before + unless serverExplicitReady() is called just before the blocking call (such as in setting up a client/server connection and performing the accept() in setUp(). """ @@ -810,6 +810,85 @@ class SmallBufferedFileObjectClassTestCase(FileObjectClassTestCase): bufsize = 2 # Exercise the buffering code +class NetworkConnectionTest(object): + """Prove network connection.""" + def clientSetUp(self): + self.cli = socket.create_connection((HOST, PORT)) + self.serv_conn = self.cli + +class BasicTCPTest2(NetworkConnectionTest, BasicTCPTest): + """Tests that NetworkConnection does not break existing TCP functionality. + """ + +class NetworkConnectionAttributesTest(unittest.TestCase): + + def setUp(self): + self.serv = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.serv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + global PORT + PORT = test_support.bind_port(self.serv, HOST, PORT) + self.serv.listen(1) + + def tearDown(self): + if self.serv: + self.serv.close() + self.serv = None + + def testWithoutServer(self): + self.tearDown() + self.failUnlessRaises(socket.error, lambda: socket.create_connection((HOST, PORT))) + + def testTimeoutAttribute(self): + # default + sock = socket.create_connection((HOST, PORT)) + self.assertTrue(sock.gettimeout() is None) + + # a value, named + sock = socket.create_connection((HOST, PORT), timeout=10) + self.assertEqual(sock.gettimeout(), 10) + + # a value, just the value + sock = socket.create_connection((HOST, PORT), 10) + self.assertEqual(sock.gettimeout(), 10) + + # None, having other default + previous = socket.getdefaulttimeout() + socket.setdefaulttimeout(10) + sock = socket.create_connection((HOST, PORT), timeout=None) + socket.setdefaulttimeout(previous) + self.assertEqual(sock.gettimeout(), 10) + + def testFamily(self): + sock = socket.create_connection((HOST, PORT), timeout=10) + self.assertEqual(sock.family, 2) + + +def threadedServer(delay): + serv = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + serv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + global PORT + PORT = test_support.bind_port(serv, HOST, PORT) + serv.listen(1) + conn, addr = serv.accept() + time.sleep(delay) + conn.send("done!") + conn.close() + +class NetworkConnectionBehaviourTest(unittest.TestCase): + def testInsideTimeout(self): + threading.Thread(target=threadedServer, args=(3,)).start() + time.sleep(.1) + sock = socket.create_connection((HOST, PORT)) + data = sock.recv(5) + self.assertEqual(data, "done!") + + def testOutsideTimeout(self): + threading.Thread(target=threadedServer, args=(3,)).start() + time.sleep(.1) + sock = socket.create_connection((HOST, PORT), timeout=1) + self.failUnlessRaises(socket.timeout, lambda: sock.recv(5)) + + class Urllib2FileobjectTest(unittest.TestCase): # urllib2.HTTPHandler has "borrowed" socket._fileobject, and requires that @@ -977,7 +1056,7 @@ class BufferIOTest(SocketConnectedTest): def test_main(): tests = [GeneralModuleTests, BasicTCPTest, TCPCloserTest, TCPTimeoutTest, - TestExceptions, BufferIOTest] + TestExceptions, BufferIOTest, BasicTCPTest2] if sys.platform != 'mac': tests.extend([ BasicUDPTest, UDPTimeoutTest ]) @@ -988,6 +1067,8 @@ def test_main(): LineBufferedFileObjectClassTestCase, SmallBufferedFileObjectClassTestCase, Urllib2FileobjectTest, + NetworkConnectionAttributesTest, + NetworkConnectionBehaviourTest, ]) if hasattr(socket, "socketpair"): tests.append(BasicSocketPairTest) diff --git a/Misc/NEWS b/Misc/NEWS index 3489240c8a0..dd8f51ef77a 100644 --- a/Misc/NEWS +++ b/Misc/NEWS @@ -199,6 +199,10 @@ Core and builtins Library ------- +- Patch #1676823: Added create_connection() to socket.py, which may be + called with a timeout, and use it from httplib (whose HTTPConnection + now accepts an optional timeout). + - Bug #978833: Revert r50844, as it broke _socketobject.dup. - Bug #1675967: re patterns pickled with Python 2.4 and earlier can