diff --git a/test_requirements.txt b/test_requirements.txt new file mode 100644 index 0000000..7a38911 --- /dev/null +++ b/test_requirements.txt @@ -0,0 +1 @@ +websockets \ No newline at end of file diff --git a/tests/test_sockjs_integration.py b/tests/test_sockjs_integration.py new file mode 100644 index 0000000..e990a18 --- /dev/null +++ b/tests/test_sockjs_integration.py @@ -0,0 +1,31 @@ +import asyncio +import unittest +from multiprocessing import Process +import time +import websockets + +from sockjstest.sockjs import app + + +class SockjsIntegrationTests(unittest.TestCase): + def setUp(self): + self.loop = asyncio.new_event_loop() + asyncio.set_event_loop(None) + self.server_process = Process(target=app.run, kwargs={'host': '127.0.0.1', 'port': 8080}) + self.server_process.start() + time.sleep(1) + + def tearDown(self): + self.server_process.terminate() + self.server_process.join() + self.loop.close() + + def test_echo(self): + async def echo(): + uri = "ws://127.0.0.1:8080/echo/websocket" + async with websockets.connect(uri) as websocket: + await websocket.send("Hello world!") + response = await websocket.recv() + self.assertEqual(response, "Hello world!") + + self.loop.run_until_complete(echo()) diff --git a/vase/app.py b/vase/app.py index ce9eae2..5dcb02c 100644 --- a/vase/app.py +++ b/vase/app.py @@ -31,8 +31,7 @@ def __init__(self, name): self._name = name self._routes = [] - @asyncio.coroutine - def _handle_404(self, request, start_response): + async def _handle_404(self, request, start_response): data = 'Not found'.encode('utf-8') headers = ( (b'Content-Type', b'text/plain'), @@ -67,16 +66,15 @@ def run(self, *, host='0.0.0.0', port=3000, loop=None): def processor_factory(transport, protocol, reader, writer): return RoutingHttpProcessor(transport, protocol, reader, writer, routes=self._routes) - asyncio.async(loop.create_server(lambda: BaseHttpProtocol(processor_factory, loop=loop), + asyncio.ensure_future(loop.create_server(lambda: BaseHttpProtocol(processor_factory, loop=loop), host, port)) loop.run_forever() @staticmethod def _decorate_callback(callback): - @asyncio.coroutine - def handle_normal(request, start_response, **kwargs): + async def handle_normal(request, start_response, **kwargs): - data = yield from asyncio.coroutine(callback)(request, **kwargs) + data = await callback(request, **kwargs) if isinstance(data, HttpResponse): response = data else: diff --git a/vase/handlers.py b/vase/handlers.py index 72d7094..339d809 100644 --- a/vase/handlers.py +++ b/vase/handlers.py @@ -34,7 +34,7 @@ def __init__(self, request, reader, writer, callback): self._writer = writer self._callback = callback - def handle(self, **kwargs): + async def handle(self, **kwargs): def start_response(status, headers): self._writer.write_status(status) self._writer.add_headers(*headers) @@ -43,7 +43,7 @@ def write(data): self._writer.write(data) return write - result = yield from self._callback(self._request, start_response, **kwargs) + result = await self._callback(self._request, start_response, **kwargs) self._writer.writelines(result) @@ -56,14 +56,14 @@ def __init__(self, request, reader, writer, endpoint_factory, context): self._endpoint = None self._context = context - def handle(self, **kwargs): + async def handle(self, **kwargs): self._endpoint = self._endpoint_factory() self._endpoint.bag = self._context self._endpoint.transport = WebSocketWriter(self._writer) if hasattr(self._endpoint, 'authorize_request'): - if not (yield from asyncio.coroutine(self._endpoint.authorize_request)(self._request)): + if not await self._endpoint.authorize_request(self._request): self._writer.write_status(b'401 Anauthorized') self._writer.write_body(b'') return @@ -86,19 +86,18 @@ def handle(self, **kwargs): ) self._writer.write_body(b'') - yield from self._switch_protocol() + await self._switch_protocol() - def _switch_protocol(self): + async def _switch_protocol(self): self._endpoint.on_connect() - yield from self._parse_messages() + await self._parse_messages() - @asyncio.coroutine - def _parse_messages(self): + async def _parse_messages(self): parser = WebSocketParser(self._reader) while True: try: - msg = yield from parser.get_message() + msg = await parser.get_message() except WebSocketFormatException: self._writer.close() return @@ -114,7 +113,7 @@ def _parse_messages(self): elif msg.opcode == OpCode.ping: self._writer.write(FrameBuilder.pong(masked=False, payload=msg.payload)) else: - yield from asyncio.coroutine(self._endpoint.on_message)(msg.payload) + await self._endpoint.on_message(msg.payload) def persistent_connection(self): return True diff --git a/vase/http.py b/vase/http.py index b12764e..89aa71e 100644 --- a/vase/http.py +++ b/vase/http.py @@ -99,12 +99,11 @@ def append_to_last_header(self, value): def _has_form(self): return self.get('content-type', '').lower() == _FORM_URLENCODED - @asyncio.coroutine - def _maybe_init_post(self): + async def _maybe_init_post(self): if self._post_inited: return if self._has_form(): - body = yield from self.body.read() + body = await self.body.read() self.POST = MultiDict(urllib.parse.parse_qs(body.decode('utf-8'))) self._post_inited = True @@ -200,9 +199,8 @@ def _write_chunk(self, data): class HttpParser: @staticmethod - @asyncio.coroutine - def parse(reader): - l = yield from reader.readline() + async def parse(reader): + l = await reader.readline() if not l: return l = l.rstrip(DELIMITER) @@ -224,7 +222,7 @@ def parse(reader): request = HttpRequest(method, uri, version, extra) while True: - l = yield from reader.readline() + l = await reader.readline() if not l: return if l == DELIMITER: @@ -243,5 +241,5 @@ def parse(reader): request.append_to_last_header(value) request.body = reader - yield from request._maybe_init_post() + await request._maybe_init_post() return request diff --git a/vase/protocol.py b/vase/protocol.py index bcf283b..5cc99b4 100644 --- a/vase/protocol.py +++ b/vase/protocol.py @@ -17,8 +17,7 @@ def __init__(self, transport, protocol, reader, writer): self._reader = reader self._writer = writer - @asyncio.coroutine - def handle_request(self, request): + async def handle_request(self, request): self._transport.write(b'HTTP/1.1 404 NOT FOUND\r\nContent-Length: 13\r\n\r\n404 Not Found') def on_timeout(self): @@ -50,7 +49,7 @@ def connection_made(self, transport): self._handler = self._build_handler() - self._task = asyncio.async(self._handle_client()) + self._task = asyncio.ensure_future(self._handle_client()) self._task.add_done_callback(self._maybe_log_exception) self._reset_timeout() @@ -71,11 +70,10 @@ def data_received(self, data): self._reset_timeout() super().data_received(data) - @asyncio.coroutine - def _handle_client(self): + async def _handle_client(self): while True: try: - req = yield from HttpParser.parse(self._reader) + req = await HttpParser.parse(self._reader) except BadRequestException as e: self._bad_request(self._writer, e) self._writer.close() @@ -85,13 +83,13 @@ def _handle_client(self): break try: - yield from self._handler.handle_request(req) + await self._handler.handle_request(req) finally: if self._should_close_conn_immediately(req): if self._writer: self._writer.close() else: - yield from req.body.read() + await req.body.read() if self._writer is not None: self._writer.restore() @@ -144,6 +142,6 @@ def _maybe_log_exception(self, task): if __name__ == '__main__': # pragma: no cover loop = asyncio.get_event_loop() host, port = 'localhost', 3000 - asyncio.async(loop.create_server(lambda: BaseHttpProtocol(loop=loop), + asyncio.ensure_future(loop.create_server(lambda: BaseHttpProtocol(loop=loop), host, port)) loop.run_forever() diff --git a/vase/routing.py b/vase/routing.py index eac6be2..36b2287 100644 --- a/vase/routing.py +++ b/vase/routing.py @@ -12,8 +12,7 @@ def __init__(self, transport, protocol, reader, writer, *, routes=None): self._handler = None super().__init__(transport, protocol, reader, writer) - @asyncio.coroutine - def handle_request(self, request): + async def handle_request(self, request): current_route = None matchdict = {} for route in self._routes: @@ -22,10 +21,10 @@ def handle_request(self, request): current_route = route break if current_route is None: - return (yield from super().handle_request(request)) + return await super().handle_request(request) self._handler = current_route.handler_factory(request, self._reader, self._writer) - return (yield from self._handler.handle(**matchdict)) + return await self._handler.handle(**matchdict) def on_timeout(self): self._handler.on_timeout() @@ -53,7 +52,7 @@ def __str__(self): ) -_R = re.compile("{((\w+:)?[^{}]+)}") +_R = re.compile(r"{((\w+:)?[^{}]+)}") def _regex_substituter(m): diff --git a/vase/sockjs/__init__.py b/vase/sockjs/__init__.py index 8542765..11ec9f3 100644 --- a/vase/sockjs/__init__.py +++ b/vase/sockjs/__init__.py @@ -44,12 +44,11 @@ def __init__(self, name): def attach(self, endpoint): self.endpoint = endpoint - @asyncio.coroutine - def consume(self): + async def consume(self): if self.endpoint: while self.pending_messages: msg = self.pending_messages.popleft() - yield from asyncio.coroutine(self.endpoint.on_message)(msg) + await self.endpoint.on_message(msg) class SockJsRoute(ContextHandlingCallbackRoute): @@ -95,8 +94,7 @@ def __init__(self, request, reader, writer, endpoint, context, sessions): self._session = None - @asyncio.coroutine - def handle(self, **kwargs): + async def handle(self, **kwargs): path = kwargs.pop(SockJsRoute.SOCKJS_ROUTE_MATCH, '') if not path or path == '/': self._writer.status = 200 @@ -108,13 +106,13 @@ def handle(self, **kwargs): self._writer.write_body(content) return if path == '/info': - yield from self._info_handler.handle(self._request, self._writer) + await self._info_handler.handle(self._request, self._writer) return elif self._iframe_handler.path_re.match(path): - yield from self._iframe_handler.handle(self._request, self._writer) + await self._iframe_handler.handle(self._request, self._writer) return elif path == '/websocket': - yield from self._ws_handler.handle(self._request, self._writer) + await self._ws_handler.handle(self._request, self._writer) return else: parts = path[1:].split("/") @@ -124,7 +122,7 @@ def handle(self, **kwargs): server, session, transport = parts if not self._validate_parts(server, session, transport): return self.not_found() - return (yield from self._handle_transport(session, transport)) + return await self._handle_transport(session, transport) def _validate_parts(self, server, session, transport): if not server or not session or not transport: @@ -135,8 +133,7 @@ def _validate_parts(self, server, session, transport): return False return True - @asyncio.coroutine - def _handle_transport(self, session, transport): + async def _handle_transport(self, session, transport): handler = self._transport_handlers.get(transport) if handler is None: return self.not_found() @@ -152,7 +149,7 @@ def _handle_transport(self, session, transport): else: endpoint = self._instantiate_endpoint() if hasattr(endpoint, 'authorize_request'): - if not (yield from asyncio.coroutine(sess.authorize_request)(self._request)): + if not await sess.authorize_request(self._request): self._writer.status = 401 self._writer.write_body('') return @@ -174,7 +171,7 @@ def _handle_transport(self, session, transport): handler = handler(self._request, sess, self._context) self._current_handler = handler - yield from handler.handle(self._request, self._writer) + await handler.handle(self._request, self._writer) def _instantiate_endpoint(self): end = self._endpoint() diff --git a/vase/sockjs/handlers.py b/vase/sockjs/handlers.py index 0993248..c4e765d 100644 --- a/vase/sockjs/handlers.py +++ b/vase/sockjs/handlers.py @@ -17,8 +17,7 @@ class Handler(object): initiates_session = False allowed_methods = ('GET',) - @asyncio.coroutine - def handle(self, request, writer): + async def handle(self, request, writer): raise NotImplementedError def connection_lost(self, exc): @@ -93,8 +92,7 @@ class InfoHandler(Handler): def __init__(self, allow_websocket=True): self._allow_websocket = allow_websocket - @asyncio.coroutine - def handle(self, request, writer): + async def handle(self, request, writer): origin = request.get('origin', 'null') if origin == 'null': origin = '*' @@ -155,8 +153,7 @@ class IFrameHandler(Handler): path_re = re.compile("/iframe[0-9-.a-z_]*.html"); - @asyncio.coroutine - def handle(self, request, writer): + async def handle(self, request, writer): if request.method != 'GET': return self.not_allowed(writer, ['GET']) @@ -186,10 +183,9 @@ def __init__(self, reader, endpoint, context): self._context = context self._reader = reader - @asyncio.coroutine - def handle(self, request, writer): + async def handle(self, request, writer): ws_handler = WebSocketHandler(request, self._reader, writer, self._endpoint, self._context) - return (yield from ws_handler.handle()) + return await ws_handler.handle() class XhrTransportHandler(Handler): @@ -202,8 +198,7 @@ def __init__(self, reader, session, context): self._context = context self._reader = reader - @asyncio.coroutine - def handle(self, request, writer): + async def handle(self, request, writer): if self._session.is_new: self._session.is_new = False self._session.endpoint.on_connect() @@ -214,7 +209,7 @@ def handle(self, request, writer): if not self._session.outgoing_messages: self._session.waiter = Future() - yield from self._session.waiter + await self._session.waiter if self._session.closed: self._send_message(request, writer, 'c[3000,"Go away!"]') @@ -272,8 +267,7 @@ def __init__(self, reader, session, context): self._context = context self._reader = reader - @asyncio.coroutine - def handle(self, request, writer): + async def handle(self, request, writer): new = self._session.is_new if self._session.is_new: self._session.is_new = False @@ -309,7 +303,7 @@ def handle(self, request, writer): written += self._send_messages(writer) while True: self._session.waiter = Future() - yield from self._session.waiter + await self._session.waiter written += self._send_messages(writer) if written >= 4096: writer.write(b'0\r\n\r\n') @@ -365,8 +359,7 @@ class EventSourceHandler(XhrStreamingHandler): initiates_session = True allowed_methods = ('POST',) - @asyncio.coroutine - def handle(self, request, writer): + async def handle(self, request, writer): new = self._session.is_new if self._session.is_new: self._session.is_new = False @@ -393,7 +386,7 @@ def handle(self, request, writer): written += self._send_messages(writer) while True: self._session.waiter = Future() - yield from self._session.waiter + await self._session.waiter written += self._send_messages(writer) if written >= 4096: writer.write(b'0\r\n\r\n') @@ -423,9 +416,8 @@ def __init__(self, reader, session, context): self._context = context self._reader = reader - @asyncio.coroutine - def handle(self, request, writer): - message = yield from request.body.read() + async def handle(self, request, writer): + message = await request.body.read() if not message: return self.send_500('Payload expected.', writer) try: @@ -434,7 +426,7 @@ def handle(self, request, writer): return self.send_500('Broken JSON encoding.', writer) for m in msgs: self._session.pending_messages.append(m) - yield from self._session.consume() + await self._session.consume() allow = request.get('access-control-request-headers') origin = request.get('origin', 'null') @@ -467,8 +459,7 @@ class HtmlFileHandler(XhrStreamingHandler): '''.strip() - @asyncio.coroutine - def handle(self, request, writer): + async def handle(self, request, writer): callback = request.GET.get('c') if not callback: self.send_500('"callback" parameter required', writer) @@ -493,7 +484,7 @@ def handle(self, request, writer): written += self._send_messages(writer) while True: self._session.waiter = Future() - yield from self._session.waiter + await self._session.waiter written += self._send_messages(writer) if written >= 4096: writer.write(b'0\r\n\r\n') @@ -524,8 +515,7 @@ def write_chunk(writer, message): class JsonpHandler(XhrStreamingHandler): - @asyncio.coroutine - def handle(self, request, writer): + async def handle(self, request, writer): callback = request.GET.get('c') if not callback: self.send_500('"callback" parameter required', writer) @@ -554,7 +544,7 @@ def handle(self, request, writer): return self._session.waiter = Future() - yield from self._session.waiter + await self._session.waiter self._send_messages(writer, callback) writer.close() @@ -588,11 +578,10 @@ def _send_messages(self, writer, callback): class JsonpSendingHandler(XhrRecievingHandler): - @asyncio.coroutine - def handle(self, request, writer): + async def handle(self, request, writer): message = request.POST.get('d') if not message: - message = (yield from request.body.read()).decode('utf-8') + message = (await request.body.read()).decode('utf-8') if not message: return self.send_500('Payload expected.', writer) try: @@ -601,7 +590,7 @@ def handle(self, request, writer): return self.send_500('Broken JSON encoding.', writer) for m in msgs: self._session.pending_messages.append(m) - yield from self._session.consume() + await self._session.consume() writer.status = 200 writer.add_headers( diff --git a/vase/stream.py b/vase/stream.py index b8088b5..3705851 100644 --- a/vase/stream.py +++ b/vase/stream.py @@ -1,20 +1,16 @@ -from asyncio import coroutine - - class LimitedReader: def __init__(self, reader, limit): self._reader = reader self._limit = limit self._read_count = 0 - @coroutine - def read(self, n=-1): + async def read(self, n=-1): if not n or not self.bytes_left: return b'' if n < 0 or n > self.bytes_left: n = self.bytes_left self._read_count += n - return (yield from self._reader.read(n)) + return await self._reader.read(n) @property def bytes_left(self): diff --git a/vase/websocket.py b/vase/websocket.py index 97d2efe..97c3fe9 100644 --- a/vase/websocket.py +++ b/vase/websocket.py @@ -1,5 +1,6 @@ import asyncio -from asyncio.streams import StreamWriter, IncompleteReadError +from asyncio.streams import StreamWriter +from asyncio.exceptions import IncompleteReadError import collections import struct from enum import Enum @@ -171,11 +172,10 @@ def __init__(self, reader): self._reader = reader self._frames = collections.deque() - @asyncio.coroutine - def get_message(self): + async def get_message(self): while True: try: - frame = yield from self.parse_frame(self._reader) + frame = await self.parse_frame(self._reader) except IncompleteReadError: frame = None if frame is None: @@ -210,8 +210,8 @@ def _build_message(self): return Message(opcode, payload, b'') @classmethod - def parse_frame(cls, reader): - data = yield from reader.readexactly(2) + async def parse_frame(cls, reader): + data = await reader.readexactly(2) if not data: return None first_byte, second_byte = struct.unpack('!BB', data) @@ -244,22 +244,22 @@ def parse_frame(cls, reader): raise WebSocketFormatException("Clients MUST mask their frames") if length == 126: - data = yield from reader.readexactly(2) + data = await reader.readexactly(2) if not data: return None length = struct.unpack_from('!H', data)[0] elif length == 127: - data = yield from reader.readexactly(8) + data = await reader.readexactly(8) if not data: return None length = struct.unpack_from('!Q', data)[0] - mask = yield from reader.readexactly(4) + mask = await reader.readexactly(4) if not mask: return None if length: - payload = yield from reader.readexactly(length) + payload = await reader.readexactly(length) if not payload: return None else: