Repository: aio-libs/aiohttp

Issue:
Keep a reference to ClientSession in response object
https://github.com/aio-libs/aiohttp/issues/1981#issuecomment-309293373


Discussion:

Resolving patch:
diff --git a/aiohttp/client.py b/aiohttp/client.py
--- a/aiohttp/client.py
+++ b/aiohttp/client.py
@@ -222,7 +222,8 @@ def _request(self, method, url, *,
                         compress=compress, chunked=chunked,
                         expect100=expect100, loop=self._loop,
                         response_class=self._response_class,
-                        proxy=proxy, proxy_auth=proxy_auth, timer=timer)
+                        proxy=proxy, proxy_auth=proxy_auth, timer=timer,
+                        session=self)
 
                     # connection timeout
                     try:
@@ -688,13 +689,6 @@ def __await__(self):
                 self._session.close()
                 raise
 
-    def __del__(self):
-        # in case of "resp = aiohttp.request(...)"
-        # _SessionRequestContextManager get destroyed before resp get processed
-        # and connection has to stay alive during this time
-        # ClientSession.detach just cleans up connector attribute
-        self._session.detach()
-
 
 def request(method, url, *,
             params=None,
diff --git a/aiohttp/client_reqrep.py b/aiohttp/client_reqrep.py
--- a/aiohttp/client_reqrep.py
+++ b/aiohttp/client_reqrep.py
@@ -66,14 +66,14 @@ def __init__(self, method, url, *,
                  chunked=None, expect100=False,
                  loop=None, response_class=None,
                  proxy=None, proxy_auth=None, proxy_from_env=False,
-                 timer=None):
+                 timer=None, session=None):
 
         if loop is None:
             loop = asyncio.get_event_loop()
 
         assert isinstance(url, URL), url
         assert isinstance(proxy, (URL, type(None))), proxy
-
+        self._session = session
         if params:
             q = MultiDict(url.query)
             url2 = url.with_query(params)
@@ -409,7 +409,7 @@ def send(self, conn):
             request_info=self.request_info
         )
 
-        self.response._post_init(self.loop)
+        self.response._post_init(self.loop, self._session)
         return self.response
 
     @asyncio.coroutine
@@ -446,6 +446,7 @@ class ClientResponse(HeadersMixin):
     # post-init stage allows to not change ctor signature
     _loop = None
     _closed = True  # to allow __del__ for non-initialized properly response
+    _session = None
 
     def __init__(self, method, url, *,
                  writer=None, continue100=None, timer=None,
@@ -487,8 +488,9 @@ def _headers(self):
     def request_info(self):
         return self._request_info
 
-    def _post_init(self, loop):
+    def _post_init(self, loop, session):
         self._loop = loop
+        self._session = session  # store a reference to session #1985
         if loop.get_debug():
             self._source_traceback = traceback.extract_stack(sys._getframe(1))
 
@@ -654,6 +656,7 @@ def _cleanup_writer(self):
         if self._writer is not None and not self._writer.done():
             self._writer.cancel()
         self._writer = None
+        self._session = None
 
     def _notify_content(self):
         content = self.content


Test patch:
diff --git a/tests/test_client_proto.py b/tests/test_client_proto.py
--- a/tests/test_client_proto.py
+++ b/tests/test_client_proto.py
@@ -72,7 +72,7 @@ def test_client_protocol_readuntil_eof(loop):
     proto.data_received(b'HTTP/1.1 200 Ok\r\n\r\n')
 
     response = ClientResponse('get', URL('http://def-cl-resp.org'))
-    response._post_init(loop)
+    response._post_init(loop, mock.Mock())
     yield from response.start(conn, read_until_eof=True)
 
     assert not response.content.is_eof()
diff --git a/tests/test_client_request.py b/tests/test_client_request.py
--- a/tests/test_client_request.py
+++ b/tests/test_client_request.py
@@ -1104,7 +1104,7 @@ def send(self, conn):
                                        self.url,
                                        writer=self._writer,
                                        continue100=self._continue)
-            resp._post_init(self.loop)
+            resp._post_init(self.loop, mock.Mock())
             self.response = resp
             nonlocal called
             called = True
diff --git a/tests/test_client_response.py b/tests/test_client_response.py
--- a/tests/test_client_response.py
+++ b/tests/test_client_response.py
@@ -13,13 +13,18 @@
 from aiohttp.client_reqrep import ClientResponse, RequestInfo
 
 
+@pytest.fixture
+def session():
+    return mock.Mock()
+
+
 @asyncio.coroutine
-def test_http_processing_error():
+def test_http_processing_error(session):
     loop = mock.Mock()
     request_info = mock.Mock()
     response = ClientResponse(
         'get', URL('http://del-cl-resp.org'), request_info=request_info)
-    response._post_init(loop)
+    response._post_init(loop, session)
     loop.get_debug = mock.Mock()
     loop.get_debug.return_value = True
 
@@ -34,10 +39,10 @@ def test_http_processing_error():
     assert info.value.request_info is request_info
 
 
-def test_del():
+def test_del(session):
     loop = mock.Mock()
     response = ClientResponse('get', URL('http://del-cl-resp.org'))
-    response._post_init(loop)
+    response._post_init(loop, session)
     loop.get_debug = mock.Mock()
     loop.get_debug.return_value = True
 
@@ -53,9 +58,9 @@ def test_del():
     connection.release.assert_called_with()
 
 
-def test_close(loop):
+def test_close(loop, session):
     response = ClientResponse('get', URL('http://def-cl-resp.org'))
-    response._post_init(loop)
+    response._post_init(loop, session)
     response._closed = False
     response._connection = mock.Mock()
     response.close()
@@ -64,25 +69,25 @@ def test_close(loop):
     response.close()
 
 
-def test_wait_for_100_1(loop):
+def test_wait_for_100_1(loop, session):
     response = ClientResponse(
         'get', URL('http://python.org'), continue100=object())
-    response._post_init(loop)
+    response._post_init(loop, session)
     assert response._continue is not None
     response.close()
 
 
-def test_wait_for_100_2(loop):
+def test_wait_for_100_2(loop, session):
     response = ClientResponse(
         'get', URL('http://python.org'))
-    response._post_init(loop)
+    response._post_init(loop, session)
     assert response._continue is None
     response.close()
 
 
-def test_repr(loop):
+def test_repr(loop, session):
     response = ClientResponse('get', URL('http://def-cl-resp.org'))
-    response._post_init(loop)
+    response._post_init(loop, session)
     response.status = 200
     response.reason = 'Ok'
     assert '<ClientResponse(http://def-cl-resp.org) [200 Ok]>'\
@@ -109,9 +114,9 @@ def test_url_obj_deprecated():
 
 
 @asyncio.coroutine
-def test_read_and_release_connection(loop):
+def test_read_and_release_connection(loop, session):
     response = ClientResponse('get', URL('http://def-cl-resp.org'))
-    response._post_init(loop)
+    response._post_init(loop, session)
 
     def side_effect(*args, **kwargs):
         fut = helpers.create_future(loop)
@@ -126,9 +131,9 @@ def side_effect(*args, **kwargs):
 
 
 @asyncio.coroutine
-def test_read_and_release_connection_with_error(loop):
+def test_read_and_release_connection_with_error(loop, session):
     response = ClientResponse('get', URL('http://def-cl-resp.org'))
-    response._post_init(loop)
+    response._post_init(loop, session)
     content = response.content = mock.Mock()
     content.read.return_value = helpers.create_future(loop)
     content.read.return_value.set_exception(ValueError)
@@ -139,9 +144,9 @@ def test_read_and_release_connection_with_error(loop):
 
 
 @asyncio.coroutine
-def test_release(loop):
+def test_release(loop, session):
     response = ClientResponse('get', URL('http://def-cl-resp.org'))
-    response._post_init(loop)
+    response._post_init(loop, session)
     fut = helpers.create_future(loop)
     fut.set_result(b'')
     content = response.content = mock.Mock()
@@ -152,13 +157,13 @@ def test_release(loop):
 
 
 @asyncio.coroutine
-def test_release_on_del(loop):
+def test_release_on_del(loop, session):
     connection = mock.Mock()
     connection.protocol.upgraded = False
 
     def run(conn):
         response = ClientResponse('get', URL('http://def-cl-resp.org'))
-        response._post_init(loop)
+        response._post_init(loop, session)
         response._closed = False
         response._connection = conn
 
@@ -168,9 +173,9 @@ def run(conn):
 
 
 @asyncio.coroutine
-def test_response_eof(loop):
+def test_response_eof(loop, session):
     response = ClientResponse('get', URL('http://def-cl-resp.org'))
-    response._post_init(loop)
+    response._post_init(loop, session)
     response._closed = False
     conn = response._connection = mock.Mock()
     conn.protocol.upgraded = False
@@ -181,9 +186,9 @@ def test_response_eof(loop):
 
 
 @asyncio.coroutine
-def test_response_eof_upgraded(loop):
+def test_response_eof_upgraded(loop, session):
     response = ClientResponse('get', URL('http://def-cl-resp.org'))
-    response._post_init(loop)
+    response._post_init(loop, session)
 
     conn = response._connection = mock.Mock()
     conn.protocol.upgraded = True
@@ -194,9 +199,9 @@ def test_response_eof_upgraded(loop):
 
 
 @asyncio.coroutine
-def test_response_eof_after_connection_detach(loop):
+def test_response_eof_after_connection_detach(loop, session):
     response = ClientResponse('get', URL('http://def-cl-resp.org'))
-    response._post_init(loop)
+    response._post_init(loop, session)
     response._closed = False
     conn = response._connection = mock.Mock()
     conn.protocol = None
@@ -207,9 +212,9 @@ def test_response_eof_after_connection_detach(loop):
 
 
 @asyncio.coroutine
-def test_text(loop):
+def test_text(loop, session):
     response = ClientResponse('get', URL('http://def-cl-resp.org'))
-    response._post_init(loop)
+    response._post_init(loop, session)
 
     def side_effect(*args, **kwargs):
         fut = helpers.create_future(loop)
@@ -227,9 +232,9 @@ def side_effect(*args, **kwargs):
 
 
 @asyncio.coroutine
-def test_text_bad_encoding(loop):
+def test_text_bad_encoding(loop, session):
     response = ClientResponse('get', URL('http://def-cl-resp.org'))
-    response._post_init(loop)
+    response._post_init(loop, session)
 
     def side_effect(*args, **kwargs):
         fut = helpers.create_future(loop)
@@ -250,9 +255,9 @@ def side_effect(*args, **kwargs):
 
 
 @asyncio.coroutine
-def test_text_custom_encoding(loop):
+def test_text_custom_encoding(loop, session):
     response = ClientResponse('get', URL('http://def-cl-resp.org'))
-    response._post_init(loop)
+    response._post_init(loop, session)
 
     def side_effect(*args, **kwargs):
         fut = helpers.create_future(loop)
@@ -272,9 +277,9 @@ def side_effect(*args, **kwargs):
 
 
 @asyncio.coroutine
-def test_text_detect_encoding(loop):
+def test_text_detect_encoding(loop, session):
     response = ClientResponse('get', URL('http://def-cl-resp.org'))
-    response._post_init(loop)
+    response._post_init(loop, session)
 
     def side_effect(*args, **kwargs):
         fut = helpers.create_future(loop)
@@ -292,9 +297,9 @@ def side_effect(*args, **kwargs):
 
 
 @asyncio.coroutine
-def test_text_after_read(loop):
+def test_text_after_read(loop, session):
     response = ClientResponse('get', URL('http://def-cl-resp.org'))
-    response._post_init(loop)
+    response._post_init(loop, session)
 
     def side_effect(*args, **kwargs):
         fut = helpers.create_future(loop)
@@ -312,9 +317,9 @@ def side_effect(*args, **kwargs):
 
 
 @asyncio.coroutine
-def test_json(loop):
+def test_json(loop, session):
     response = ClientResponse('get', URL('http://def-cl-resp.org'))
-    response._post_init(loop)
+    response._post_init(loop, session)
 
     def side_effect(*args, **kwargs):
         fut = helpers.create_future(loop)
@@ -332,9 +337,9 @@ def side_effect(*args, **kwargs):
 
 
 @asyncio.coroutine
-def test_json_custom_loader(loop):
+def test_json_custom_loader(loop, session):
     response = ClientResponse('get', URL('http://def-cl-resp.org'))
-    response._post_init(loop)
+    response._post_init(loop, session)
     response.headers = {
         'Content-Type': 'application/json;charset=cp1251'}
     response._content = b'data'
@@ -347,9 +352,9 @@ def custom(content):
 
 
 @asyncio.coroutine
-def test_json_no_content(loop):
+def test_json_no_content(loop, session):
     response = ClientResponse('get', URL('http://def-cl-resp.org'))
-    response._post_init(loop)
+    response._post_init(loop, session)
     response.headers = {
         'Content-Type': 'data/octet-stream'}
     response._content = b''
@@ -364,9 +369,9 @@ def test_json_no_content(loop):
 
 
 @asyncio.coroutine
-def test_json_override_encoding(loop):
+def test_json_override_encoding(loop, session):
     response = ClientResponse('get', URL('http://def-cl-resp.org'))
-    response._post_init(loop)
+    response._post_init(loop, session)
 
     def side_effect(*args, **kwargs):
         fut = helpers.create_future(loop)
@@ -386,19 +391,19 @@ def side_effect(*args, **kwargs):
 
 
 @pytest.mark.xfail
-def test_override_flow_control(loop):
+def test_override_flow_control(loop, session):
     class MyResponse(ClientResponse):
         flow_control_class = aiohttp.StreamReader
     response = MyResponse('get', URL('http://my-cl-resp.org'))
-    response._post_init(loop)
+    response._post_init(loop, session)
     response._connection = mock.Mock()
     assert isinstance(response.content, aiohttp.StreamReader)
     response.close()
 
 
-def test_get_encoding_unknown(loop):
+def test_get_encoding_unknown(loop, session):
     response = ClientResponse('get', URL('http://def-cl-resp.org'))
-    response._post_init(loop)
+    response._post_init(loop, session)
 
     response.headers = {'Content-Type': 'application/json'}
     with mock.patch('aiohttp.client_reqrep.chardet') as m_chardet: