Skip to content

Commit e9e59e9

Browse files
committed
Correct type annotations in uvicorn/importer.py
#1046 #1067 The relationship between module imports and calls in uvicorn/config.py was previously unclear. `import_from_string` was annotated as returning a `ModuleType`, but was being used as if it were returning callables. Mypy was raising "module not callable" errors, as reported in #1046. Current use cases of `import_from_string` include: - `uvicorn.Config.HTTP_PROTOCOLS`, `uvicorn.Config.WS_PROTOCOLS`: `Type[asyncio.Protocol]` (these are subclasses of asyncio protocols, so they are annotated with `Type[Class]` as explained in the mypy docs - `uvicorn.Config.LOOP_SETUPS`: `Callable` (each loop is a function) - `uvicorn.Config().loaded_app`: `ASGIApplication` (should be an ASGI application instance, like `Starlette()` or `FastAPI()`) Ideally, the return type of `import_from_string` would reflect these use cases, but the complex typing will be difficult to maintain. For easier maintenance, the return type will be `Any`, and objects returned by `import_from_string` will be annotated directly.
1 parent 8d2cd6f commit e9e59e9

File tree

5 files changed

+21
-15
lines changed

5 files changed

+21
-15
lines changed

setup.cfg

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ files =
1414
uvicorn/middleware/message_logger.py,
1515
uvicorn/supervisors/basereload.py,
1616
uvicorn/importer.py,
17+
tests/importer/test_importer.py,
1718
uvicorn/protocols/utils.py,
1819
uvicorn/loops,
1920
uvicorn/main.py,

tests/importer/test_importer.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,40 +3,40 @@
33
from uvicorn.importer import ImportFromStringError, import_from_string
44

55

6-
def test_invalid_format():
6+
def test_invalid_format() -> None:
77
with pytest.raises(ImportFromStringError) as exc_info:
88
import_from_string("example:")
99
expected = 'Import string "example:" must be in format "<module>:<attribute>".'
1010
assert expected in str(exc_info.value)
1111

1212

13-
def test_invalid_module():
13+
def test_invalid_module() -> None:
1414
with pytest.raises(ImportFromStringError) as exc_info:
1515
import_from_string("module_does_not_exist:myattr")
1616
expected = 'Could not import module "module_does_not_exist".'
1717
assert expected in str(exc_info.value)
1818

1919

20-
def test_invalid_attr():
20+
def test_invalid_attr() -> None:
2121
with pytest.raises(ImportFromStringError) as exc_info:
2222
import_from_string("tempfile:attr_does_not_exist")
2323
expected = 'Attribute "attr_does_not_exist" not found in module "tempfile".'
2424
assert expected in str(exc_info.value)
2525

2626

27-
def test_internal_import_error():
27+
def test_internal_import_error() -> None:
2828
with pytest.raises(ImportError):
2929
import_from_string("tests.importer.raise_import_error:myattr")
3030

3131

32-
def test_valid_import():
32+
def test_valid_import() -> None:
3333
instance = import_from_string("tempfile:TemporaryFile")
3434
from tempfile import TemporaryFile
3535

3636
assert instance == TemporaryFile
3737

3838

39-
def test_no_import_needed():
39+
def test_no_import_needed() -> None:
4040
from tempfile import TemporaryFile
4141

4242
instance = import_from_string(TemporaryFile)

uvicorn/_handlers/http.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ async def handle_http(
3636
connection_lost = loop.create_future()
3737

3838
# Switch the protocol from the stream reader to our own HTTP protocol class.
39-
protocol = config.http_protocol_class(
39+
protocol = config.http_protocol_class( # type: ignore[call-arg, operator]
4040
config=config,
4141
server_state=server_state,
4242
on_connection_lost=lambda: connection_lost.set_result(True),

uvicorn/config.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -316,12 +316,14 @@ def load(self) -> None:
316316
)
317317

318318
if isinstance(self.http, str):
319-
self.http_protocol_class = import_from_string(HTTP_PROTOCOLS[self.http])
319+
http_protocol_class = import_from_string(HTTP_PROTOCOLS[self.http])
320+
self.http_protocol_class: Type[asyncio.Protocol] = http_protocol_class
320321
else:
321322
self.http_protocol_class = self.http
322323

323324
if isinstance(self.ws, str):
324-
self.ws_protocol_class = import_from_string(WS_PROTOCOLS[self.ws])
325+
ws_protocol_class = import_from_string(WS_PROTOCOLS[self.ws])
326+
self.ws_protocol_class: Optional[Type[asyncio.Protocol]] = ws_protocol_class
325327
else:
326328
self.ws_protocol_class = self.ws
327329

@@ -374,9 +376,13 @@ def load(self) -> None:
374376
self.loaded = True
375377

376378
def setup_event_loop(self) -> None:
377-
loop_setup = import_from_string(LOOP_SETUPS[self.loop])
378-
if loop_setup is not None:
379-
loop_setup()
379+
loop_setup_str: Optional[str] = LOOP_SETUPS[self.loop]
380+
if loop_setup_str:
381+
loop_setup: Callable = import_from_string(loop_setup_str)
382+
if not inspect.isfunction(loop_setup):
383+
raise ImportFromStringError("Asyncio event loop must be a callable.")
384+
else:
385+
loop_setup()
380386

381387
def bind_socket(self) -> socket.socket:
382388
family = socket.AF_INET

uvicorn/importer.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
import importlib
2-
from types import ModuleType
3-
from typing import Union
2+
from typing import Any
43

54

65
class ImportFromStringError(Exception):
76
pass
87

98

10-
def import_from_string(import_str: Union[ModuleType, str]) -> ModuleType:
9+
def import_from_string(import_str: Any) -> Any:
1110
if not isinstance(import_str, str):
1211
return import_str
1312

0 commit comments

Comments
 (0)