Repository: sloria/webtest-aiohttp (language: python)

Issue:
Needs update for aiohttp 2.0
Apparently there are backwards-incompatible changes in aiohttp 2.0.  At the very least, you can no longer import `web_reqgrep ` from `aiohttp` as in done [here](https://github.com/sloria/webtest-aiohttp/blob/master/webtest_aiohttp.py#L35-L37).  


Discussion / hints:
Thanks @patrick-jones . I would certainly review and merge a PR fixing 2.0 compatibility. =)

Fix patch:
diff --git a/.travis.yml b/.travis.yml
index 5dbc1fc..3644782 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -9,7 +9,7 @@ python:
 
 env:
 # Lowest supported version
-- AIOHTTP_VERSION="==1.1.6"
+- AIOHTTP_VERSION="==2.3.8"
 # Latest release
 - AIOHTTP_VERSION=""
 
diff --git a/README.rst b/README.rst
index 17ee4aa..b77fe83 100644
--- a/README.rst
+++ b/README.rst
@@ -12,32 +12,22 @@ webtest-aiohttp
 
 webtest-aiohttp provides integration of WebTest with aiohttp.web applications.
 
-Supports aiohttp>=0.21.0.
-
+Supports aiohttp>=2.3.8.
 
 .. code-block:: python
 
-    import asyncio
-
     from aiohttp import web
     from webtest_aiohttp import TestApp
 
-    loop = asyncio.get_event_loop()
-    asyncio.set_event_loop(loop)
+    app = web.Application()
 
-    app = web.Application(loop=loop)
-
-    @asyncio.coroutine
-    def hello(request):
-        return web.Response(body=json.dumps(
-            {'message': 'Hello world'}
-        ).encode('utf-8'), content_type='application/json')
+    async def hello(request):
+        return web.json_response({'message': 'Hello world'})
 
     app.router.add_route('GET', '/', handler)
 
-
-    def test_hello():
-        client = TestApp(app)
+    def test_hello(loop):
+        client = TestApp(app, loop=loop)
         res = client.get('/')
         assert res.status_code == 200
         assert res.json == {'message': 'Hello world'}
@@ -48,17 +38,46 @@ Installation
 
     pip install webtest-aiohttp
 
+
+**Note: If you are using aiohttp<2.0.0, you will need to install webtest-aiohttp 1.x.**
+
+::
+
+    pip install 'webtest-aiohttp<2.0.0'
+
+
+Usage with pytest
+=================
+
+If you are using pytest and pytest-aiohttp, you can make your tests more
+concise with a fixture.
+
+
+.. code-block:: python
+
+    from aiohttp import web
+    from webtest_aiohttp import TestApp as WebTestApp
+
+    app = web.Application()
+
+    async def hello(request):
+        return web.json_response({'message': 'Hello world'})
+
+    app.router.add_route('GET', '/', handler)
+
+    @pytest.fixture()
+    def testapp(loop):
+        return WebTestApp(app, loop=loop)
+
+    def test_get(testapp):
+        assert testapp.get('/').json == {'message': 'Hello world'}
+
 Project Links
 =============
 
 - PyPI: https://pypi.python.org/pypi/webtest-aiohttp
 - Issues: https://github.com/sloria/webtest-aiohttp/issues
 
-Credits
-=======
-
-This code was adapted from Kirill Klenov's `muffin <https://github.com/klen/muffin>`_ library.
-
 License
 =======
 
diff --git a/dev-requirements.txt b/dev-requirements.txt
index 64847e7..a67d850 100644
--- a/dev-requirements.txt
+++ b/dev-requirements.txt
@@ -1,10 +1,11 @@
 # Build tasks
 invoke
 
-aiohttp
+aiohttp>=2.3.8
 
 # Testing
 pytest
+pytest-aiohttp==0.3.0
 tox>=1.5.0
 
 # Packaging


Test patch:
diff --git a/test_webtest_aiohttp.py b/test_webtest_aiohttp.py
index 97d48d1..6ba65fa 100644
--- a/test_webtest_aiohttp.py
+++ b/test_webtest_aiohttp.py
@@ -4,31 +4,66 @@
 
 import pytest
 from aiohttp import web
+from aiohttp.web import json_response
 
-from webtest_aiohttp import TestApp
-
-@pytest.fixture(scope='session')
-def loop():
-    loop_ = asyncio.get_event_loop()
-    asyncio.set_event_loop(loop_)
-    return loop_
+from webtest_aiohttp import TestApp as WebTestApp
 
 @pytest.fixture()
-def app(loop):
+def app():
     @asyncio.coroutine
     def handler(request):
-        return web.Response(body=json.dumps(
-            {'message': 'Hello world'}
-        ).encode('utf-8'), content_type='application/json', charset='utf8')
-    app_ = web.Application(loop=loop)
+        return json_response({'message': 'Hello world'})
+
+    @asyncio.coroutine
+    def echo_post(request):
+        form_body = yield from request.post()
+        return json_response(dict(form_body))
+
+    @asyncio.coroutine
+    def echo_json(request):
+        json_body = yield from request.json()
+        return json_response(json_body)
+
+    @asyncio.coroutine
+    def echo_headers(request):
+        return json_response(dict(request.headers))
+
+    @asyncio.coroutine
+    def echo_params(request):
+        return json_response(dict(request.query))
 
+    app_ = web.Application()
     app_.router.add_route('GET', '/', handler)
+    app_.router.add_route('POST', '/echo_post', echo_post)
+    app_.router.add_route('POST', '/echo_json', echo_json)
+    app_.router.add_route('GET', '/echo_headers', echo_headers)
+    app_.router.add_route('GET', '/echo_params', echo_params)
     return app_
 
-def test_testapp(app):
-    testapp = TestApp(app)
-    res = testapp.get('/')
+
+@pytest.fixture()
+def wt(app, loop):
+    return WebTestApp(app, loop=loop)
+
+def test_get(wt):
+    res = wt.get('/')
     assert res.status_code == 200
     expected = {'message': 'Hello world'}
     assert res.json == expected
     assert res.text == json.dumps(expected)
+
+def test_post_form(wt):
+    res = wt.post('/echo_post', {'name': 'Steve'})
+    assert res.json == {'name': 'Steve'}
+
+def test_post_json(wt):
+    res = wt.post_json('/echo_json', {'name': 'Steve'})
+    assert res.json == {'name': 'Steve'}
+
+def test_headers(wt):
+    res = wt.get('/echo_headers', headers={'X-Foo': 'Bar'})
+    assert res.json['X-Foo'] == 'Bar'
+
+def test_params(wt):
+    res = wt.get('/echo_params?foo=bar')
+    assert res.json == {'foo': 'bar'}
diff --git a/webtest_aiohttp.py b/webtest_aiohttp.py
index c943ee4..3fed4d2 100755
--- a/webtest_aiohttp.py
+++ b/webtest_aiohttp.py
@@ -28,204 +28,43 @@ def test_hello():
         assert res.status_code == 200
         assert res.json == {'message': 'Hello world'}
 """
-import io
-
+from aiohttp.test_utils import TestClient, TestServer
 import aiohttp
-from aiohttp import (
-    hdrs, web_reqrep, web_exceptions
-)
-from aiohttp.abc import AbstractMatchInfo
-from aiohttp.helpers import TimeService
-import asyncio
-from aiohttp.server import ServerHttpProtocol
 import webob
 import webtest
 
 __version__ = '1.1.1'
 
-class RequestHandler(ServerHttpProtocol):
-
-    _request = None
-
-    def __init__(self, manager, app, router, time_service, *,
-                 secure_proxy_ssl_header=None, **kwargs):
-        super().__init__(**kwargs)
-
-        self._manager = manager
-        self._app = app
-        self._router = router
-        self._secure_proxy_ssl_header = secure_proxy_ssl_header
-        self._time_service = time_service
-
-    def __repr__(self):
-        if self._request is None:
-            meth = 'none'
-            path = 'none'
-        else:
-            meth = self._request.method
-            path = self._request.rel_url.raw_path
-        return "<{} {}:{} {}>".format(
-            self.__class__.__name__, meth, path,
-            'connected' if self.transport is not None else 'disconnected')
-
-    def connection_made(self, transport):
-        super().connection_made(transport)
-
-        self._manager.connection_made(self, transport)
-
-    def connection_lost(self, exc):
-        self._manager.connection_lost(self, exc)
-
-        super().connection_lost(exc)
-
-    @asyncio.coroutine
-    def handle_request(self, message, payload):
-        self._manager._requests_count += 1
-        if self.access_log:
-            now = self._loop.time()
-
-        try:
-            request = web_reqrep.Request(
-                message, payload,
-                self.transport, self.reader, self.writer,
-                self._time_service, task=None,
-                secure_proxy_ssl_header=self._secure_proxy_ssl_header)
-        except TypeError:  # no task argument in older versions of aiohttp
-            request = web_reqrep.Request(
-                message, payload,
-                self.transport, self.reader, self.writer,
-                self._time_service,
-                secure_proxy_ssl_header=self._secure_proxy_ssl_header)
-        self._request = request
-        try:
-            match_info = yield from self._router.resolve(request)
-            assert isinstance(match_info, AbstractMatchInfo), match_info
-            match_info.add_app(self._app)
-            match_info.freeze()
-
-            resp = None
-            request._match_info = match_info
-            expect = request.headers.get(hdrs.EXPECT)
-            if expect:
-                resp = (
-                    yield from match_info.expect_handler(request))
-
-            if resp is None:
-                handler = match_info.handler
-                for app in match_info.apps:
-                    for factory in reversed(app.middlewares):
-                        handler = yield from factory(app, handler)
-                resp = yield from handler(request)
-
-            assert isinstance(resp, web_reqrep.StreamResponse), \
-                ("Handler {!r} should return response instance, "
-                 "got {!r} [middlewares {!r}]").format(
-                     match_info.handler, type(resp), self._middlewares)
-        except web_exceptions.HTTPException as exc:
-            resp = exc
-
-        resp_msg = yield from resp.prepare(request)
-        yield from resp.write_eof()
-
-        # notify server about keep-alive
-        self.keep_alive(resp.keep_alive)
-
-        # Restore default state.
-        # Should be no-op if server code didn't touch these attributes.
-        self.writer.set_tcp_cork(False)
-        self.writer.set_tcp_nodelay(True)
-
-        # log access
-        if self.access_log:
-            self.log_access(message, None, resp_msg, self._loop.time() - now)
-
-        # for repr
-        self._request = None
-
-
-class RequestHandlerFactory:
-
-    def __init__(self, app, router, *,
-                 handler=RequestHandler, loop=None,
-                 secure_proxy_ssl_header=None, **kwargs):
-        self._app = app
-        self._router = router
-        self._handler = handler
-        self._loop = loop
-        self._connections = {}
-        self._secure_proxy_ssl_header = secure_proxy_ssl_header
-        self._kwargs = kwargs
-        self._kwargs.setdefault('logger', app.logger)
-        self._requests_count = 0
-        self._time_service = TimeService(self._loop)
-
-    @property
-    def requests_count(self):
-        """Number of processed requests."""
-        return self._requests_count
-
-    @property
-    def secure_proxy_ssl_header(self):
-        return self._secure_proxy_ssl_header
-
-    @property
-    def connections(self):
-        return list(self._connections.keys())
-
-    def connection_made(self, handler, transport):
-        self._connections[handler] = transport
-
-    def connection_lost(self, handler, exc=None):
-        if handler in self._connections:
-            del self._connections[handler]
-
-    @asyncio.coroutine
-    def finish_connections(self, timeout=None):
-        coros = [conn.shutdown(timeout) for conn in self._connections]
-        yield from asyncio.gather(*coros, loop=self._loop)
-        self._connections.clear()
-        self._time_service.stop()
-
-    def __call__(self):
-        return self._handler(
-            self, self._app, self._router, self._time_service, loop=self._loop,
-            secure_proxy_ssl_header=self._secure_proxy_ssl_header,
-            **self._kwargs)
-
-# from https://github.com/klen/muffin/blob/develop/muffin/pytest.py
-def WSGIHandler(app):
+def WSGIHandler(app, loop):
     """Return a wsgi application given an `aiohttp.web.Application`."""
-    loop = app.loop
-    assert loop is not None, 'Application must have an event loop'
 
     def handle(environ, start_response):
-
+        server = TestServer(app, loop=loop)
+        # aiohttp.test_utils.TestClient will do the heavy lifting
+        client = TestClient(server, loop=loop)
+        loop.run_until_complete(client.start_server())
         req = webob.Request(environ)
-        vers = aiohttp.HttpVersion10 if req.http_version == 'HTTP/1.0' else aiohttp.HttpVersion11
-        message = aiohttp.RawRequestMessage(
-            req.method, req.path_qs, vers, aiohttp.CIMultiDict(req.headers), False, False, False)
-        payload = aiohttp.StreamReader(loop=loop)
-        payload.feed_data(req.body)
-        payload.feed_eof()
-        factory = RequestHandlerFactory(
-            app, app.router, loop=loop, keep_alive_on=False)
-        handler = factory()
-        handler.transport = io.BytesIO()
-        handler.transport.is_closing = lambda: False
-        handler.transport._conn_lost = 0
-        handler.transport.get_extra_info = lambda s: {
-            'peername': ('127.0.0.1', 80)
-        }.get(s)
-        handler.writer = aiohttp.parsers.StreamWriter(
-            handler.transport, handler, handler.reader, handler._loop)
-        coro = handler.handle_request(message, payload)
-        if loop.is_running():
-            raise RuntimeError('Client cannot start during another coroutine is running.')
-
-        loop.run_until_complete(coro)
-        handler.transport.seek(9)
-        res = webob.Response.from_file(handler.transport)
+        coro = client.request(
+            data=req.body,
+            params=req.params,
+            method=req.method,
+            path=req.path,
+            headers=dict(req.headers)
+        )
+        result = loop.run_until_complete(coro)
+        body = loop.run_until_complete(result.read())
+        raw_headers = list(result.raw_headers)
+        headerlist = [(name.decode('latin1'), value.decode('latin1'))
+                      for name, value in raw_headers]
+        res = webob.Response(
+            body=body,
+            status=result.status,
+            content_type=result.content_type,
+            headerlist=headerlist,
+            charset=result.charset
+        )
         start_response(res.status, res.headerlist)
+        loop.run_until_complete(client.close())
         return res.app_iter
     return handle
 
@@ -236,5 +75,11 @@ class TestApp(webtest.TestApp):
     def __init__(self, app, *args, **kwargs):
         self.aiohttp_app = None
         if isinstance(app, aiohttp.web.Application):
-            self.aiohttp_app = app = WSGIHandler(app)
+            if 'loop' in kwargs:
+                loop = kwargs.pop('loop')
+            elif app.loop:
+                loop = app.loop
+            else:
+                raise ValueError('Must provide a loop to TestApp')
+            self.aiohttp_app = app = WSGIHandler(app, loop)
         super().__init__(app, *args, **kwargs)