Repository: urllib3/urllib3

Issue:
Support `io` framework for HTTPResponse
This suggestion is motivated by the following issue that pops up in `requests` (see kennethreitz/requests#1364 - they redirected me here):

```
>>> import io, requests
>>> r = requests.get('http://www.google.com',stream=True)
>>> io.BufferedReader(r.raw).read()
AttributeError: 'HTTPResponse' object has no attribute 'readable'
```

The base problem here is that `requests.packages.urllib3.response.HTTPResponse`  object (that's what `r.raw` is) does not respect the `io` API.  The following _does_ work, however:

```
>>> r = requests.get('http://www.google.com',stream=True)
>>> r.raw.readable=lambda:True
>>> r.raw.closed=False
>>> io.BufferedReader(r.raw).read()
```

Suggesting that the main thing that is neeeded is adding a `readable` method and a `closed` attribute to `HTTPResponse`.  (I think `flush` and `close` are probably also necessary, and maybe also `writable` and `seekable`).

Is there a reason this is not possible, or would it be fine to just add the relevant methods to `HTTPResponse`?



Discussion:
I don't see any reason why not. :) You're welcome to. Please include tests accordingly.


Resolving patch:
diff --git a/urllib3/response.py b/urllib3/response.py
--- a/urllib3/response.py
+++ b/urllib3/response.py
@@ -7,6 +7,7 @@
 
 import logging
 import zlib
+import io
 
 from .exceptions import DecodeError
 from .packages.six import string_types as basestring, binary_type
@@ -48,7 +49,7 @@ def _get_decoder(mode):
     return DeflateDecoder()
 
 
-class HTTPResponse(object):
+class HTTPResponse(io.IOBase):
     """
     HTTP Response container.
 
@@ -239,3 +240,35 @@ def getheaders(self):
 
     def getheader(self, name, default=None):
         return self.headers.get(name, default)
+
+    # Overrides from io.IOBase
+    def close(self):
+        if not self.closed:
+            self._fp.close()
+
+    @property
+    def closed(self):
+        if self._fp is None:
+            return True
+        elif hasattr(self._fp, 'closed'):
+            return self._fp.closed
+        elif hasattr(self._fp, 'isclosed'):  # Python 2
+            return self._fp.isclosed()
+        else:
+            return True
+
+    def fileno(self):
+        if self._fp is None:
+            raise IOError("HTTPResponse has no file to get a fileno from")
+        elif hasattr(self._fp, "fileno"):
+            return self._fp.fileno()
+        else:
+            raise IOError("The file-like object  this HTTPResponse is wrapped "
+                          "around has no file descriptor")
+
+    def flush(self):
+        if self._fp is not None and hasattr(self._fp, 'flush'):
+            return self._fp.flush()
+
+    def readable(self):
+        return True


Test patch:
diff --git a/test/test_response.py b/test/test_response.py
--- a/test/test_response.py
+++ b/test/test_response.py
@@ -1,6 +1,6 @@
 import unittest
 
-from io import BytesIO
+from io import BytesIO, BufferedReader
 
 from urllib3.response import HTTPResponse
 from urllib3.exceptions import DecodeError
@@ -112,5 +112,53 @@ def test_chunked_decoding_gzip(self):
         self.assertEqual(r.read(1), b'f')
         self.assertEqual(r.read(2), b'oo')
 
+    def test_io(self):
+        import socket
+        try:
+            from http.client import HTTPResponse as OldHTTPResponse
+        except:
+            from httplib import HTTPResponse as OldHTTPResponse
+
+        fp = BytesIO(b'foo')
+        resp = HTTPResponse(fp, preload_content=False)
+
+        self.assertEqual(resp.closed, False)
+        self.assertEqual(resp.readable(), True)
+        self.assertEqual(resp.writable(), False)
+        self.assertRaises(IOError, resp.fileno)
+
+        resp.close()
+        self.assertEqual(resp.closed, True)
+
+        # Try closing with an `httplib.HTTPResponse`, because it has an
+        # `isclosed` method.
+        hlr = OldHTTPResponse(socket.socket())
+        resp2 = HTTPResponse(hlr, preload_content=False)
+        self.assertEqual(resp2.closed, False)
+        resp2.close()
+        self.assertEqual(resp2.closed, True)
+
+        #also try when only data is present.
+        resp3 = HTTPResponse('foodata')
+        self.assertRaises(IOError, resp3.fileno)
+
+        resp3._fp = 2
+        # A corner case where _fp is present but doesn't have `closed`,
+        # `isclosed`, or `fileno`.  Unlikely, but possible.
+        self.assertEqual(resp3.closed, True)
+        self.assertRaises(IOError, resp3.fileno)
+
+    def test_io_bufferedreader(self):
+        fp = BytesIO(b'foo')
+        resp = HTTPResponse(fp, preload_content=False)
+        br = BufferedReader(resp)
+
+        self.assertEqual(br.read(), b'foo')
+
+        br.close()
+        self.assertEqual(resp.closed, True)
+
+
+
 if __name__ == '__main__':
     unittest.main()