]> git.ipfire.org Git - thirdparty/starlette.git/commitdiff
type config with `None` default as `str | None` instead of `Any` (#1732)
authorChristopher Dignam <chris@dignam.xyz>
Sun, 10 Jul 2022 13:00:23 +0000 (09:00 -0400)
committerGitHub <noreply@github.com>
Sun, 10 Jul 2022 13:00:23 +0000 (15:00 +0200)
* fix type annotations for config

* wip

* fix simple case

* wip

* isort

* remove mypy config change

* fix test

* fix coverage

* use assert_type

* format

* CR

* Update tests/test_config.py

Co-authored-by: Marcelo Trylesinski <marcelotryle@gmail.com>
requirements.txt
starlette/config.py
tests/test_config.py

index d1218aaf4b945cc56e018f52500273cfb735243b..38d9f8f5eebf102ebaa2aa69d4e51b352709c1cb 100644 (file)
@@ -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
index e9e809c735f42c75302e8a7d4c4a6b9b4e546155..a4abf49a6537475a77967ed6b6552932d5eee879 100644 (file)
@@ -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 = ...
index 5ba7aefd78e159cf7a7059ef9f33c43aa7cd5bda..d33000389c46cf0cdf8a08718e76a635d682e3c3 100644 (file)
@@ -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)