diff --git a/requirements.txt b/requirements.txt index d1218aaf..38d9f8f5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,6 +9,7 @@ databases[sqlite]==0.5.5 flake8==3.9.2 isort==5.10.1 mypy==0.961 +typing_extensions==4.2.0 types-requests==2.26.3 types-contextvars==2.4.7 types-PyYAML==6.0.4 diff --git a/starlette/config.py b/starlette/config.py index e9e809c7..a4abf49a 100644 --- a/starlette/config.py +++ b/starlette/config.py @@ -60,6 +60,12 @@ class Config: if env_file is not None and os.path.isfile(env_file): self.file_values = self._read_file(env_file) + @typing.overload + def __call__( + self, key: str, *, default: None + ) -> typing.Optional[str]: # pragma: no cover + ... + @typing.overload def __call__( self, key: str, cast: typing.Type[T], default: T = ... diff --git a/tests/test_config.py b/tests/test_config.py index 5ba7aefd..d3300038 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -1,12 +1,44 @@ import os from pathlib import Path +from typing import Any, Optional import pytest +from typing_extensions import assert_type from starlette.config import Config, Environ, EnvironError from starlette.datastructures import URL, Secret +def test_config_types() -> None: + """ + We use `assert_type` to test the types returned by Config via mypy. + """ + config = Config( + environ={"STR": "some_str_value", "STR_CAST": "some_str_value", "BOOL": "true"} + ) + + assert_type(config("STR"), str) + assert_type(config("STR_DEFAULT", default=""), str) + assert_type(config("STR_CAST", cast=str), str) + assert_type(config("STR_NONE", default=None), Optional[str]) + assert_type(config("STR_CAST_NONE", cast=str, default=None), Optional[str]) + assert_type(config("STR_CAST_STR", cast=str, default=""), str) + + assert_type(config("BOOL", cast=bool), bool) + assert_type(config("BOOL_DEFAULT", cast=bool, default=False), bool) + assert_type(config("BOOL_NONE", cast=bool, default=None), Optional[bool]) + + def cast_to_int(v: Any) -> int: + return int(v) + + # our type annotations allow these `cast` and `default` configurations, but + # the code will error at runtime. + with pytest.raises(ValueError): + config("INT_CAST_DEFAULT_STR", cast=cast_to_int, default="true") + with pytest.raises(ValueError): + config("INT_DEFAULT_STR", cast=int, default="true") + + def test_config(tmpdir, monkeypatch): path = os.path.join(tmpdir, ".env") with open(path, "w") as file: @@ -27,6 +59,7 @@ def test_config(tmpdir, monkeypatch): DATABASE_URL = config("DATABASE_URL", cast=URL) REQUEST_TIMEOUT = config("REQUEST_TIMEOUT", cast=int, default=10) REQUEST_HOSTNAME = config("REQUEST_HOSTNAME") + MAIL_HOSTNAME = config("MAIL_HOSTNAME", default=None) SECRET_KEY = config("SECRET_KEY", cast=Secret) UNSET_SECRET = config("UNSET_SECRET", cast=Secret, default=None) EMPTY_SECRET = config("EMPTY_SECRET", cast=Secret, default="") @@ -40,6 +73,7 @@ def test_config(tmpdir, monkeypatch): assert DATABASE_URL.username == "user" assert REQUEST_TIMEOUT == 10 assert REQUEST_HOSTNAME == "example.com" + assert MAIL_HOSTNAME is None assert repr(SECRET_KEY) == "Secret('**********')" assert str(SECRET_KEY) == "12345" assert bool(SECRET_KEY)