Skip to content

Commit

Permalink
Use deffered imports and nicer import-from-string behaviors (#134)
Browse files Browse the repository at this point in the history
* Use defered imports and nicer import-from-string behaviors

* Black formatting

* importlib related changes for Python 3.5 compat
  • Loading branch information
tomchristie authored Jul 19, 2018
1 parent c1f1d17 commit 1f922a5
Show file tree
Hide file tree
Showing 14 changed files with 230 additions and 104 deletions.
30 changes: 19 additions & 11 deletions tests/protocols/test_http.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from uvicorn.protocols.http.h11 import H11Protocol
from uvicorn.protocols.http.httptools import HttpToolsProtocol
import asyncio
from uvicorn.protocols.http import H11Protocol, HttpToolsProtocol
import h11
import pytest

Expand All @@ -21,7 +22,8 @@ async def __call__(self, receive, send) -> None:
"type": "http.response.start",
"status": self.status_code,
"headers": [
[key.encode(), value.encode()] for key, value in self.headers.items()
[key.encode(), value.encode()]
for key, value in self.headers.items()
],
}
)
Expand Down Expand Up @@ -64,7 +66,7 @@ def set_content_type(self):
b"Content-Type: text/plain",
b"Content-Length: 100000",
b"",
b'x' * 100000,
b"x" * 100000,
]
)

Expand Down Expand Up @@ -156,13 +158,14 @@ def test_post_request(protocol_cls):
class App:
def __init__(self, scope):
self.scope = scope

async def __call__(self, receive, send):
body = b''
body = b""
more_body = True
while more_body:
message = await receive()
body += message.get('body', b'')
more_body = message.get('more_body', False)
body += message.get("body", b"")
more_body = message.get("more_body", False)
response = Response(b"Body: " + body, media_type="text/plain")
await response(receive, send)

Expand Down Expand Up @@ -200,7 +203,9 @@ def app(scope):
@pytest.mark.parametrize("protocol_cls", [HttpToolsProtocol, H11Protocol])
def test_chunked_encoding(protocol_cls):
def app(scope):
return Response(b"Hello, world!", status_code=200, headers={"transfer-encoding": "chunked"})
return Response(
b"Hello, world!", status_code=200, headers={"transfer-encoding": "chunked"}
)

protocol = get_connected_protocol(app, protocol_cls)
protocol.data_received(SIMPLE_GET_REQUEST)
Expand Down Expand Up @@ -274,7 +279,7 @@ def app(scope):
def test_invalid_http(protocol_cls):
app = lambda scope: None
protocol = get_connected_protocol(app, protocol_cls)
protocol.data_received(b'x' * 100000)
protocol.data_received(b"x" * 100000)
assert protocol.transport.is_closing()


Expand All @@ -283,6 +288,7 @@ def test_app_exception(protocol_cls):
class App:
def __init__(self, scope):
self.scope = scope

async def __call__(self, receive, send):
raise Exception()

Expand Down Expand Up @@ -435,9 +441,11 @@ def __init__(self, scope):
async def __call__(self, receive, send):
nonlocal got_disconnect_event

message = await receive()
while message['type'] != 'http.disconnect':
continue
while True:
message = await receive()
if message["type"] == "http.disconnect":
break

got_disconnect_event = True

protocol = get_connected_protocol(App, protocol_cls)
Expand Down
7 changes: 5 additions & 2 deletions tests/protocols/test_websocket.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
from uvicorn.protocols.http.h11 import H11Protocol
from uvicorn.protocols.http.httptools import HttpToolsProtocol
import asyncio
import functools
import threading
import requests
import pytest
import websockets
from contextlib import contextmanager
from uvicorn.protocols.http import HttpToolsProtocol, H11Protocol


class WebSocketResponse:
Expand Down Expand Up @@ -229,7 +230,9 @@ def test_subprotocols(protocol_cls, acceptable_subprotocol):
class App(WebSocketResponse):
async def websocket_connect(self, message):
if acceptable_subprotocol in self.scope["subprotocols"]:
await self.send({"type": "websocket.accept", "subprotocol": acceptable_subprotocol})
await self.send(
{"type": "websocket.accept", "subprotocol": acceptable_subprotocol}
)
else:
await self.send({"type": "websocket.close"})

Expand Down
5 changes: 5 additions & 0 deletions tests/raise_import_error.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Used by test_importer.py

myattr = 123

import does_not_exist
18 changes: 18 additions & 0 deletions tests/test_auto_detection.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from uvicorn.protocols.http.httptools import HttpToolsProtocol
from uvicorn.protocols.http.auto import AutoHTTPProtocol
from uvicorn.loops.auto import auto_loop_setup
import asyncio
import uvloop

# TODO: Add pypy to our testing matrix, and assert we get the correct classes
# dependent on the platform we're running the tests under.


def test_http_auto():
protocol = AutoHTTPProtocol(app=None)
assert isinstance(protocol, HttpToolsProtocol)


def test_loop_auto():
loop = auto_loop_setup()
assert isinstance(asyncio.get_event_loop_policy(), uvloop.EventLoopPolicy)
44 changes: 44 additions & 0 deletions tests/test_importer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
from uvicorn.importer import import_from_string, ImportFromStringError
import pytest
import os
import sys


def test_invalid_format():
with pytest.raises(ImportFromStringError) as exc:
import_from_string("example:")
expected = 'Import string "example:" must be in format "<module>:<attribute>".'
assert expected in str(exc)


def test_invalid_module():
with pytest.raises(ImportFromStringError) as exc:
import_from_string("module_does_not_exist:myattr")
expected = 'Could not import module "module_does_not_exist".'
assert expected in str(exc)


def test_invalid_attr():
with pytest.raises(ImportFromStringError) as exc:
import_from_string("tempfile:attr_does_not_exist")
expected = 'Attribute "attr_does_not_exist" not found in module "tempfile".'
assert expected in str(exc)


def test_internal_import_error():
with pytest.raises(ImportError) as exc:
import_from_string("tests.raise_import_error:myattr")


def test_valid_import():
instance = import_from_string("tempfile:TemporaryFile")
from tempfile import TemporaryFile

assert instance == TemporaryFile


def test_no_import_needed():
from tempfile import TemporaryFile

instance = import_from_string(TemporaryFile)
assert instance == TemporaryFile
37 changes: 37 additions & 0 deletions uvicorn/importer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import importlib


class ImportFromStringError(Exception):
pass


def import_from_string(import_str):
if not isinstance(import_str, str):
return import_str

module_str, _, attrs_str = import_str.partition(":")
if not module_str or not attrs_str:
message = (
'Import string "{import_str}" must be in format "<module>:<attribute>".'
)
raise ImportFromStringError(message.format(import_str=import_str))

try:
module = importlib.import_module(module_str)
except ImportError as exc:
if exc.name != module_str:
raise
message = 'Could not import module "{module_str}".'
raise ImportFromStringError(message.format(module_str=module_str))

instance = module
try:
for attr_str in attrs_str.split("."):
instance = getattr(instance, attr_str)
except AttributeError:
message = 'Attribute "{attrs_str}" not found in module "{module_str}".'
raise ImportFromStringError(
message.format(attrs_str=attrs_str, module_str=module_str)
)

return instance
5 changes: 5 additions & 0 deletions uvicorn/loops/asyncio.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import asyncio


def asyncio_setup():
return asyncio.get_event_loop()
11 changes: 11 additions & 0 deletions uvicorn/loops/auto.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
def auto_loop_setup():
try:
import uvloop
except ImportError: # pragma: no cover
from uvicorn.loops.asyncio import asyncio_setup

return asyncio_setup()
else:
from uvicorn.loops.uvloop import uvloop_setup

return uvloop_setup()
8 changes: 8 additions & 0 deletions uvicorn/loops/uvloop.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import asyncio
import uvloop


def uvloop_setup():
asyncio.get_event_loop().close()
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
return asyncio.get_event_loop()
Loading

0 comments on commit 1f922a5

Please sign in to comment.