diff --git a/setup.cfg b/setup.cfg index 6369fcfd2..0b3f3c0f4 100644 --- a/setup.cfg +++ b/setup.cfg @@ -10,6 +10,7 @@ files = uvicorn/lifespan, tests/test_lifespan.py, uvicorn/config.py, + tests/test_config.py, uvicorn/middleware/message_logger.py, uvicorn/supervisors/basereload.py, uvicorn/importer.py, diff --git a/tests/test_config.py b/tests/test_config.py index 0b7383e52..48a7a5fc7 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -2,10 +2,21 @@ import logging import os import socket +import sys from copy import deepcopy +from pathlib import Path +from typing import Any, Callable, Iterator, Optional +from unittest.mock import MagicMock + +if sys.version_info < (3, 8): + from typing_extensions import Literal +else: + from typing import Literal import pytest import yaml +from asgiref.typing import ASGIApplication, ASGIReceiveCallable, ASGISendCallable, Scope +from pytest_mock import MockerFixture from uvicorn.config import LOGGING_CONFIG, Config from uvicorn.middleware.debug import DebugMiddleware @@ -15,34 +26,36 @@ @pytest.fixture -def mocked_logging_config_module(mocker): +def mocked_logging_config_module(mocker: MockerFixture) -> MagicMock: return mocker.patch("logging.config") @pytest.fixture(scope="function") -def logging_config(): +def logging_config() -> dict: return deepcopy(LOGGING_CONFIG) @pytest.fixture -def json_logging_config(logging_config): +def json_logging_config(logging_config: dict) -> str: return json.dumps(logging_config) @pytest.fixture -def yaml_logging_config(logging_config): +def yaml_logging_config(logging_config: dict) -> str: return yaml.dump(logging_config) -async def asgi_app(scope, receive, send): +async def asgi_app( + scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable +) -> None: pass # pragma: nocover -def wsgi_app(environ, start_response): +def wsgi_app(environ: Any, start_response: Any) -> None: pass # pragma: nocover -def test_debug_app(): +def test_debug_app() -> None: config = Config(app=asgi_app, debug=True, proxy_headers=False) config.load() @@ -54,7 +67,9 @@ def test_debug_app(): "app, expected_should_reload", [(asgi_app, False), ("tests.test_config:asgi_app", True)], ) -def test_config_should_reload_is_set(app, expected_should_reload): +def test_config_should_reload_is_set( + app: ASGIApplication, expected_should_reload: bool +) -> None: config_debug = Config(app=app, debug=True) assert config_debug.debug is True assert config_debug.should_reload is expected_should_reload @@ -64,12 +79,12 @@ def test_config_should_reload_is_set(app, expected_should_reload): assert config_reload.should_reload is expected_should_reload -def test_reload_dir_is_set(): +def test_reload_dir_is_set() -> None: config = Config(app=asgi_app, reload=True, reload_dirs="reload_me") assert config.reload_dirs == ["reload_me"] -def test_wsgi_app(): +def test_wsgi_app() -> None: config = Config(app=wsgi_app, interface="wsgi", proxy_headers=False) config.load() @@ -78,7 +93,7 @@ def test_wsgi_app(): assert config.asgi_version == "3.0" -def test_proxy_headers(): +def test_proxy_headers() -> None: config = Config(app=asgi_app) config.load() @@ -86,13 +101,13 @@ def test_proxy_headers(): assert isinstance(config.loaded_app, ProxyHeadersMiddleware) -def test_app_unimportable_module(): +def test_app_unimportable_module() -> None: config = Config(app="no.such:app") with pytest.raises(ImportError): config.load() -def test_app_unimportable_other(caplog): +def test_app_unimportable_other(caplog: pytest.LogCaptureFixture) -> None: config = Config(app="tests.test_config:app") with pytest.raises(SystemExit): config.load() @@ -107,8 +122,8 @@ def test_app_unimportable_other(caplog): ) -def test_app_factory(caplog): - def create_app(): +def test_app_factory(caplog: pytest.LogCaptureFixture) -> None: + def create_app() -> ASGIApplication: return asgi_app config = Config(app=create_app, factory=True, proxy_headers=False) @@ -125,19 +140,19 @@ def create_app(): assert len(caplog.records) == 1 assert "--factory" in caplog.records[0].message - # App not a no-arguments callable. + # App not a no-arguments ASGIApplication. config = Config(app=asgi_app, factory=True) with pytest.raises(SystemExit): config.load() -def test_concrete_http_class(): +def test_concrete_http_class() -> None: config = Config(app=asgi_app, http=H11Protocol) config.load() assert config.http_protocol_class is H11Protocol -def test_socket_bind(): +def test_socket_bind() -> None: config = Config(app=asgi_app) config.load() sock = config.bind_socket() @@ -145,7 +160,10 @@ def test_socket_bind(): sock.close() -def test_ssl_config(tls_ca_certificate_pem_path, tls_ca_certificate_private_key_path): +def test_ssl_config( + tls_ca_certificate_pem_path: str, + tls_ca_certificate_private_key_path: str, +) -> None: config = Config( app=asgi_app, ssl_certfile=tls_ca_certificate_pem_path, @@ -156,7 +174,7 @@ def test_ssl_config(tls_ca_certificate_pem_path, tls_ca_certificate_private_key_ assert config.is_ssl is True -def test_ssl_config_combined(tls_certificate_pem_path): +def test_ssl_config_combined(tls_certificate_pem_path: str) -> None: config = Config( app=asgi_app, ssl_certfile=tls_certificate_pem_path, @@ -166,8 +184,10 @@ def test_ssl_config_combined(tls_certificate_pem_path): assert config.is_ssl is True -def asgi2_app(scope): - async def asgi(receive, send): # pragma: nocover +def asgi2_app(scope: Scope) -> Callable: + async def asgi( + receive: ASGIReceiveCallable, send: ASGISendCallable + ) -> None: # pragma: nocover pass return asgi # pragma: nocover @@ -176,7 +196,9 @@ async def asgi(receive, send): # pragma: nocover @pytest.mark.parametrize( "app, expected_interface", [(asgi_app, "3.0"), (asgi2_app, "2.0")] ) -def test_asgi_version(app, expected_interface): +def test_asgi_version( + app: ASGIApplication, expected_interface: Literal["2.0", "3.0"] +) -> None: config = Config(app=app) config.load() assert config.asgi_version == expected_interface @@ -191,7 +213,11 @@ def test_asgi_version(app, expected_interface): pytest.param(False, False, id="use_colors_disabled"), ], ) -def test_log_config_default(mocked_logging_config_module, use_colors, expected): +def test_log_config_default( + mocked_logging_config_module: MagicMock, + use_colors: Optional[bool], + expected: Optional[bool], +) -> None: """ Test that one can specify the use_colors option when using the default logging config. @@ -206,8 +232,11 @@ def test_log_config_default(mocked_logging_config_module, use_colors, expected): def test_log_config_json( - mocked_logging_config_module, logging_config, json_logging_config, mocker -): + mocked_logging_config_module: MagicMock, + logging_config: dict, + json_logging_config: str, + mocker: MockerFixture, +) -> None: """ Test that one can load a json config from disk. """ @@ -224,12 +253,12 @@ def test_log_config_json( @pytest.mark.parametrize("config_filename", ["log_config.yml", "log_config.yaml"]) def test_log_config_yaml( - mocked_logging_config_module, - logging_config, - yaml_logging_config, - mocker, - config_filename, -): + mocked_logging_config_module: MagicMock, + logging_config: dict, + yaml_logging_config: str, + mocker: MockerFixture, + config_filename: str, +) -> None: """ Test that one can load a yaml config from disk. """ @@ -244,7 +273,7 @@ def test_log_config_yaml( mocked_logging_config_module.dictConfig.assert_called_once_with(logging_config) -def test_log_config_file(mocked_logging_config_module): +def test_log_config_file(mocked_logging_config_module: MagicMock) -> None: """ Test that one can load a configparser config from disk. """ @@ -257,20 +286,25 @@ def test_log_config_file(mocked_logging_config_module): @pytest.fixture(params=[0, 1]) -def web_concurrency(request): +def web_concurrency(request: Any) -> Iterator[int]: yield request.param if os.getenv("WEB_CONCURRENCY"): del os.environ["WEB_CONCURRENCY"] @pytest.fixture(params=["127.0.0.1", "127.0.0.2"]) -def forwarded_allow_ips(request): +def forwarded_allow_ips(request: Any) -> Iterator[str]: yield request.param if os.getenv("FORWARDED_ALLOW_IPS"): del os.environ["FORWARDED_ALLOW_IPS"] -def test_env_file(web_concurrency: int, forwarded_allow_ips: str, caplog, tmp_path): +def test_env_file( + web_concurrency: int, + forwarded_allow_ips: str, + caplog: pytest.LogCaptureFixture, + tmp_path: Path, +) -> None: """ Test that one can load environment variables using an env file. """ @@ -284,7 +318,7 @@ def test_env_file(web_concurrency: int, forwarded_allow_ips: str, caplog, tmp_pa config = Config(app=asgi_app, env_file=fp) config.load() - assert config.workers == int(os.getenv("WEB_CONCURRENCY")) + assert config.workers == int(str(os.getenv("WEB_CONCURRENCY"))) assert config.forwarded_allow_ips == os.getenv("FORWARDED_ALLOW_IPS") assert len(caplog.records) == 1 assert f"Loading environment from '{fp}'" in caplog.records[0].message @@ -297,7 +331,7 @@ def test_env_file(web_concurrency: int, forwarded_allow_ips: str, caplog, tmp_pa pytest.param(False, 0, id="access log disabled shouldn't have handlers"), ], ) -def test_config_access_log(access_log: bool, handlers: int): +def test_config_access_log(access_log: bool, handlers: int) -> None: config = Config(app=asgi_app, access_log=access_log) config.load() @@ -306,7 +340,7 @@ def test_config_access_log(access_log: bool, handlers: int): @pytest.mark.parametrize("log_level", [5, 10, 20, 30, 40, 50]) -def test_config_log_level(log_level): +def test_config_log_level(log_level: int) -> None: config = Config(app=asgi_app, log_level=log_level) config.load() @@ -316,7 +350,7 @@ def test_config_log_level(log_level): assert config.log_level == log_level -def test_ws_max_size(): +def test_ws_max_size() -> None: config = Config(app=asgi_app, ws_max_size=1000) config.load() assert config.ws_max_size == 1000 diff --git a/uvicorn/config.py b/uvicorn/config.py index b917989e4..1daca9d00 100644 --- a/uvicorn/config.py +++ b/uvicorn/config.py @@ -149,7 +149,7 @@ def __init__( interface: Literal["auto", "asgi3", "asgi2", "wsgi"] = "auto", debug: bool = False, reload: bool = False, - reload_dirs: Optional[List[str]] = None, + reload_dirs: Optional[Union[List[str], str]] = None, reload_delay: Optional[float] = None, workers: Optional[int] = None, proxy_headers: bool = True,