Repository: kevin1024/vcrpy

Issue:
Doesn't work correctly with Biopython on Python3
I am using `vcrpy` version `2.0.1` with `biopython` version `1.72` when Biopython tries to iterate over the VCRHttpResponse `'VCRHTTPResponse' object has no attribute 'fp'` exception is thrown.

  **Note: works fine in Python2 and in Python3 too without VCR**

Stack Trace:
```
Traceback (most recent call last):
  File "/home/addgene/addgene-core/src/django/addgene/common/tests/regression/efetch.py", line 149, in test_efetch_vcr
    articles = list(Medline.parse(handle))
  File "/srv/addgene-py3/lib/python3.5/site-packages/Bio/Medline/__init__.py", line 129, in parse
    for line in handle:
  File "/usr/lib/python3.5/http/client.py", line 470, in readinto
    if self.fp is None:
AttributeError: 'VCRHTTPResponse' object has no attribute 'fp'
```

If I add `self.fp = None` in the constructor of `VCRHTTPResponse` class, `fp` error is not thrown.

 Even after fixing the `fp` attribute issue, Iterating over the response results in an empty list.

Regression Test which I wrote for it.
```
from __future__ import unicode_literals

import io
from Bio import Medline

from addgene.test.django import AddgeneTestCase

from vcr.stubs import VCRHTTPResponse

from Bio._py3k import _binary_to_string_handle, _bytes_to_string


class TestVcrResponseForBiopython(AddgeneTestCase):

    def setUp(self):
        self.recorded_response = {
            "status": {
                "message": "OK",
                "code": 200
            },
            "headers": {
                "content-length": ["0"],
                "server": ["gunicorn/18.0"],
                "connection": ["Close"],
                "access-control-allow-credentials": ["true"],
                "date": ["Fri, 24 Oct 2014 18:35:37 GMT"],
                "access-control-allow-origin": ["*"],
                "content-type": ["text/html; charset=utf-8"],
            },
            "body": {
                "string":b"\nPMID- 19416910\nOWN - NLM\n"
            }
        }
        self.vcr_response = VCRHTTPResponse(self.recorded_response)

    def test_efetch_vcr(self):
        handle = _binary_to_string_handle(self.vcr_response)
        articles = list(Medline.parse(handle))
```
`_binary_to_string_handle` is used by the `Bio.Entrez.efetch` method which makes the actual http request (recorded via VCR) and passes the output of `_binary_to_string_handle(VCRHttpResponse)` to the `Medline.parse` method.  

code of `_binary_to_string_handle`:
```
    def _binary_to_string_handle(handle):
        """
        params: handle: VCRHTTPResponse

        Treat a binary (bytes) handle like a text (unicode) handle (PRIVATE).
        """
        try:
            # If this is a network handle from urllib,
            # the HTTP headers may tell us the encoding.
            encoding = handle.headers.get_content_charset()
        except AttributeError:
            encoding = None
        if encoding is None:
            # The W3C recommendation is:
            # When no explicit charset parameter is provided by the sender,
            # media subtypes of the "text" type are defined to have a default
            # charset value of "ISO-8859-1" when received via HTTP.
            # "ISO-8859-1" is also known as 'latin-1'
            # See the following for more detail:
            # https://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7.1
            encoding = 'latin-1'
        wrapped = io.TextIOWrapper(io.BufferedReader(handle), encoding=encoding)
        try:
            # If wrapping an online handle, this is nice to have:
            wrapped.url = handle.url
        except AttributeError:
            pass
        return wrapped
```

Code of Medline.parse:
```
def parse(handle):
    """
    handle: <_io.TextIOWrapper encoding='utf-8'>
    
    Read Medline records one by one from the handle.

    The handle is either is a Medline file, a file-like object, or a list
    of lines describing one or more Medline records.

    Typical usage::

        from Bio import Medline
        with open("mymedlinefile") as handle:
            records = Medline.parse(handle)
            for record in records:
                print(record['TI'])

    """
    # TODO - Turn that into a working doctest
    # These keys point to string values
    textkeys = ("ID", "PMID", "SO", "RF", "NI", "JC", "TA", "IS", "CY", "TT",
                "CA", "IP", "VI", "DP", "YR", "PG", "LID", "DA", "LR", "OWN",
                "STAT", "DCOM", "PUBM", "DEP", "PL", "JID", "SB", "PMC",
                "EDAT", "MHDA", "PST", "AB", "AD", "EA", "TI", "JT")
    handle = iter(handle)

    key = ""
    record = Record()
    for line in handle:
        line = line.rstrip()
        if line[:6] == "      ":  # continuation line
            if key == "MH":
                # Multi-line MESH term, want to append to last entry in list
                record[key][-1] += line[5:]  # including space using line[5:]
            else:
                record[key].append(line[6:])
        elif line:
            key = line[:4].rstrip()
            if key not in record:
                record[key] = []
            record[key].append(line[6:])
        elif record:
            # Join each list of strings into one string.
            for key in record:
                if key in textkeys:
                    record[key] = " ".join(record[key])
            yield record
            record = Record()
    if record:  # catch last one
        for key in record:
            if key in textkeys:
                record[key] = " ".join(record[key])
        yield record
``` 
In this `for line in handle:` has 0 iterations, and `handle` is `io.TextIOWrapper(io.BufferedReader(handle), encoding=encoding)` and it returns an empty list. For normal HttpResponse handle it works as expected and returns the list of articles. 

My guess is something is wrong in the way VCR defines VCRHTTPResponse which doesn't confine with the expected HTTP response norms of Biopython.

Biopython Docs references:  

https://biopython.org/DIST/docs/api/Bio._py3k-module.html

https://biopython.org/DIST/docs/api/Bio.Medline-module.html

Discussion:
arthurHamon2: @rishab121 can you please rebase your branch on master in order to run the Travis CI ?

Patch:
diff --git a/vcr/stubs/__init__.py b/vcr/stubs/__init__.py
index 05490bb..118bc22 100644
--- a/vcr/stubs/__init__.py
+++ b/vcr/stubs/__init__.py
@@ -60,9 +60,10 @@ def serialize_headers(response):
 
 class VCRHTTPResponse(HTTPResponse):
     """
-    Stub reponse class that gets returned instead of a HTTPResponse
+    Stub response class that gets returned instead of a HTTPResponse
     """
     def __init__(self, recorded_response):
+        self.fp = None
         self.recorded_response = recorded_response
         self.reason = recorded_response['status']['message']
         self.status = self.code = recorded_response['status']['code']
@@ -93,9 +94,30 @@ class VCRHTTPResponse(HTTPResponse):
     def read(self, *args, **kwargs):
         return self._content.read(*args, **kwargs)
 
+    def readall(self):
+        return self._content.readall()
+
+    def readinto(self, *args, **kwargs):
+        return self._content.readinto(*args, **kwargs)
+
     def readline(self, *args, **kwargs):
         return self._content.readline(*args, **kwargs)
 
+    def readlines(self, *args, **kwargs):
+        return self._content.readlines(*args, **kwargs)
+
+    def seekable(self):
+        return self._content.seekable()
+
+    def tell(self):
+        return self._content.tell()
+
+    def isatty(self):
+        return self._content.isatty()
+
+    def seek(self, *args, **kwargs):
+        return self._content.seek(*args, **kwargs)
+
     def close(self):
         self._closed = True
         return True
@@ -121,6 +143,9 @@ class VCRHTTPResponse(HTTPResponse):
         else:
             return default
 
+    def readable(self):
+        return self._content.readable()
+
 
 class VCRConnection(object):
     # A reference to the cassette that's currently being patched in


Test patch:
diff --git a/tests/unit/test_response.py b/tests/unit/test_response.py
index 4ba6dcc..cde3e33 100644
--- a/tests/unit/test_response.py
+++ b/tests/unit/test_response.py
@@ -1,4 +1,9 @@
 # coding: UTF-8
+import io
+import unittest
+
+import six
+
 from vcr.stubs import VCRHTTPResponse
 
 
@@ -66,3 +71,52 @@ def test_response_headers_should_have_correct_values():
 
     assert response.headers.get('content-length') == "10806"
     assert response.headers.get('date') == "Fri, 24 Oct 2014 18:35:37 GMT"
+
+
+@unittest.skipIf(six.PY2, "Regression test for Python3 only")
+def test_response_parses_correctly_and_fp_attribute_error_is_not_thrown():
+    """
+    Regression test for https://github.com/kevin1024/vcrpy/issues/440
+    :return:
+    """
+    recorded_response = {
+        "status": {
+            "message": "OK",
+            "code": 200
+        },
+        "headers": {
+            "content-length": ["0"],
+            "server": ["gunicorn/18.0"],
+            "connection": ["Close"],
+            "access-control-allow-credentials": ["true"],
+            "date": ["Fri, 24 Oct 2014 18:35:37 GMT"],
+            "access-control-allow-origin": ["*"],
+            "content-type": ["text/html; charset=utf-8"],
+        },
+        "body": {
+            "string": b"\nPMID- 19416910\nOWN - NLM\nSTAT- MEDLINE\nDA  - 20090513\nDCOM- "
+                      b"20090622\nLR  - "
+                      b"20141209\nIS  - 1091-6490 (Electronic)\nIS  - 0027-8424 (Linking)\nVI  - "
+                      b"106\nIP  - "
+                      b"19\nDP  - 2009 May 12\nTI  - Genetic dissection of histone deacetylase "
+                      b"requirement in "
+                      b"tumor cells.\nPG  - 7751-5\nLID - 10.1073/pnas.0903139106 [doi]\nAB  - "
+                      b"Histone "
+                      b"deacetylase inhibitors (HDACi) represent a new group of drugs currently\n "
+                      b"     being "
+                      b"tested in a wide variety of clinical applications. They are especially\n  "
+                      b"    effective "
+                      b"in preclinical models of cancer where they show antiproliferative\n      "
+                      b"action in many "
+                      b"different types of cancer cells. Recently, the first HDACi was\n      "
+                      b"approved for the "
+                      b"treatment of cutaneous T cell lymphomas. Most HDACi currently in\n      "
+                      b"clinical "
+
+        }
+    }
+    vcr_response = VCRHTTPResponse(recorded_response)
+    handle = io.TextIOWrapper(io.BufferedReader(vcr_response), encoding='utf-8')
+    handle = iter(handle)
+    articles = [line for line in handle]
+    assert len(articles) > 1