Skip to content

Commit 076c8e2

Browse files
author
marcus
committed
[django-environ] Add tests for parse_value and fix/extend/improve some types.
1 parent f8a7a38 commit 076c8e2

2 files changed

Lines changed: 184 additions & 42 deletions

File tree

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
from __future__ import annotations
2+
3+
from typing import Union
4+
from typing_extensions import TypedDict, assert_type
5+
6+
import environ
7+
8+
env = environ.Env()
9+
10+
assert_type(env.parse_value("just-a-value123", None), str)
11+
12+
# builtin types
13+
assert_type(env.parse_value("string", str), str)
14+
assert_type(env.parse_value("TRUE", bool), bool)
15+
assert_type(env.parse_value("2000", int), int)
16+
assert_type(env.parse_value("-500.01", float), float)
17+
assert_type(env.parse_value("first,second,", list), list[str])
18+
assert_type(env.parse_value("(first,second)", tuple), tuple[str, ...])
19+
assert_type(env.parse_value("a=first,b=second", dict), dict[str, str])
20+
21+
22+
# cast list values (first list element is used)
23+
assert_type(env.parse_value("20.5,-0.2", [str]), list[str])
24+
assert_type(env.parse_value("20.5,-0.2", [bool]), list[bool])
25+
assert_type(env.parse_value("20.5,-0.2", [int]), list[int])
26+
assert_type(env.parse_value("20.5,-0.2", [float]), list[float])
27+
28+
# cast tuple values (first tuple element is used)
29+
assert_type(env.parse_value("(20.5,-0.2)", (str,)), tuple[str, ...])
30+
assert_type(env.parse_value("(20.5,-0.2)", (bool,)), tuple[bool, ...])
31+
assert_type(env.parse_value("(20.5,-0.2)", (int,)), tuple[int, ...])
32+
assert_type(env.parse_value("(20.5,-0.2)", (float,)), tuple[float, ...])
33+
34+
# cast dict values
35+
assert_type(env.parse_value("0=TRUE,99=FALSE", {}), dict[str, str])
36+
assert_type(env.parse_value("0=TRUE,99=FALSE", {"cast": {}}), dict[str, Union[str, object]])
37+
assert_type(env.parse_value("0=TRUE,99=FALSE", {"value": bool}), dict[str, bool])
38+
assert_type(env.parse_value("0=TRUE,99=FALSE", {"value": bool, "cast": {}}), dict[str, Union[bool, object]])
39+
assert_type(env.parse_value("0=TRUE,99=FALSE", {"key": int}), dict[int, str])
40+
assert_type(env.parse_value("0=TRUE,99=FALSE", {"key": int, "cast": {}}), dict[int, Union[str, object]])
41+
assert_type(env.parse_value("0=TRUE,99=FALSE", {"key": int, "value": bool}), dict[int, bool])
42+
assert_type(env.parse_value("0=TRUE,99=FALSE", {"key": int, "value": bool, "cast": {}}), dict[int, Union[bool, object]])
43+
44+
45+
# custom cast functions
46+
def cast_float(x: str) -> float:
47+
return float(x)
48+
49+
50+
assert_type(env.parse_value("20.5", cast_float), float)
51+
52+
53+
class Person(TypedDict):
54+
first_name: str
55+
last_name: str
56+
age: int
57+
58+
59+
def cast_person(v: str) -> Person:
60+
parts = v.split(",")
61+
return {"first_name": parts[0], "last_name": parts[1], "age": int(parts[2])}
62+
63+
64+
assert_type(env.parse_value("Bob,Riveira,30", cast_person), Person)

stubs/django-environ/environ/environ.pyi

Lines changed: 120 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from _typeshed import StrPath
1+
from _typeshed import Incomplete, StrPath
22

33
# Use aliases to avoid name conflicts with Path methods
44
from builtins import (
@@ -13,8 +13,8 @@ from builtins import (
1313
)
1414
from collections.abc import Callable, Mapping, MutableMapping
1515
from logging import Logger
16-
from typing import IO, Any, ClassVar, Generic, SupportsIndex, TypedDict, TypeVar, overload, type_check_only
17-
from typing_extensions import Required, TypeAlias, Unpack
16+
from typing import IO, Any, ClassVar, Generic, Literal, SupportsIndex, TypedDict, TypeVar, overload, type_check_only
17+
from typing_extensions import NotRequired, Required, TypeAlias, Unpack
1818
from urllib.parse import ParseResult
1919

2020
from .fileaware_mapping import FileAwareMapping
@@ -24,17 +24,66 @@ logger: Logger
2424

2525
class NoValue: ...
2626

27-
_T1 = TypeVar("_T1")
28-
_T2 = TypeVar("_T2")
29-
_Cast: TypeAlias = Callable[[str], _T1]
30-
_SchemeValue: TypeAlias = _Cast[Any] | tuple[_Cast[Any], Any]
27+
_T = TypeVar("_T")
28+
_KT = TypeVar("_KT")
29+
_VT = TypeVar("_VT")
30+
31+
_Cast: TypeAlias = Callable[[str], _T]
32+
_SchemeValue: TypeAlias = _Cast[object] | tuple[_Cast[object], object]
3133
_EmptyDict: TypeAlias = dict[object, object] # stands for {}
3234

3335
@type_check_only
34-
class CastDict(TypedDict, Generic[_T1, _T2], total=False):
35-
key: _Cast[_T1]
36-
value: _Cast[_T2]
37-
cast: dict[str, _Cast[Any]] # value cast by key
36+
class CastDict(TypedDict, Generic[_KT, _VT], total=False):
37+
key: _Cast[_KT]
38+
value: _Cast[_VT]
39+
# key-specific value casts
40+
cast: dict[str, _Cast[object]]
41+
42+
# One CastDict for each combination of 'key', 'value' and 'cast' (8 in total).
43+
# Use auxiliary '_type' to make them distinguishable for type checkers.
44+
@type_check_only
45+
class CastDict000(TypedDict):
46+
_type: NotRequired[Literal["000"]]
47+
48+
@type_check_only
49+
class CastDict001(TypedDict):
50+
_type: NotRequired[Literal["001"]]
51+
cast: dict[str, _Cast[object]]
52+
53+
@type_check_only
54+
class CastDict010(TypedDict, Generic[_VT]):
55+
_type: NotRequired[Literal["010"]]
56+
value: _Cast[_VT]
57+
58+
@type_check_only
59+
class CastDict011(TypedDict, Generic[_VT]):
60+
_type: NotRequired[Literal["011"]]
61+
value: _Cast[_VT]
62+
cast: dict[str, _Cast[object]]
63+
64+
@type_check_only
65+
class CastDict100(TypedDict, Generic[_KT]):
66+
_type: NotRequired[Literal["100"]]
67+
key: _Cast[_KT]
68+
69+
@type_check_only
70+
class CastDict101(TypedDict, Generic[_KT]):
71+
_type: NotRequired[Literal["101"]]
72+
key: _Cast[_KT]
73+
cast: dict[str, _Cast[object]]
74+
75+
@type_check_only
76+
class CastDict110(TypedDict, Generic[_KT, _VT]):
77+
_type: NotRequired[Literal["110"]]
78+
key: _Cast[_KT]
79+
value: _Cast[_VT]
80+
81+
@type_check_only
82+
class CastDict111(TypedDict, Generic[_KT, _VT]):
83+
_type: NotRequired[Literal["111"]]
84+
key: _Cast[_KT]
85+
value: _Cast[_VT]
86+
cast: dict[str, _Cast[object]]
3887

3988
@type_check_only
4089
class PathKwargs(TypedDict, total=False):
@@ -60,7 +109,7 @@ class DbConfig(MemoryDbConfig, total=False):
60109
AUTOCOMMIT: bool
61110
DISABLE_SERVER_SIDE_CURSORS: bool
62111
# Remaining options read from queryParams
63-
OPTIONS: dict[str, Any]
112+
OPTIONS: dict[str, Incomplete]
64113

65114
@type_check_only
66115
class EmailConfig(TypedDict, total=False):
@@ -74,13 +123,13 @@ class EmailConfig(TypedDict, total=False):
74123
EMAIL_USE_TLS: bool
75124
EMAIL_USE_SSL: bool
76125
# Remaining options read from queryParams
77-
OPTIONS: dict[str, Any]
126+
OPTIONS: dict[str, Incomplete]
78127

79128
# https://github.com/django/channels
80129
@type_check_only
81130
class ChannelsConfig(TypedDict, total=False):
82131
BACKEND: Required[str]
83-
CONFIG: dict[str, Any]
132+
CONFIG: dict[str, Incomplete]
84133

85134
# https://github.com/django-haystack/django-haystack
86135
@type_check_only
@@ -95,13 +144,13 @@ class SimpleSearchConfig(TypedDict, total=False):
95144
class SolrSearchConfig(SimpleSearchConfig, total=False):
96145
URL: Required[str]
97146
TIMEOUT: int
98-
KWARGS: dict[str, Any]
147+
KWARGS: dict[str, Incomplete]
99148

100149
@type_check_only
101150
class ElasticsearchSearchConfig(SimpleSearchConfig, total=False):
102151
URL: Required[str]
103152
TIMEOUT: int
104-
KWARGS: dict[str, Any]
153+
KWARGS: dict[str, Incomplete]
105154
INDEX_NAME: str
106155

107156
@type_check_only
@@ -127,7 +176,7 @@ class CacheConfig(TypedDict, total=False):
127176
TIMEOUT: int
128177
VERSION: int
129178
# Remaining options read from queryParams
130-
OPTIONS: dict[str, Any]
179+
OPTIONS: dict[str, Incomplete]
131180

132181
class Env:
133182
ENVIRON: MutableMapping[_str, _str]
@@ -155,20 +204,20 @@ class Env:
155204
def __init__(self, **scheme: _SchemeValue) -> None: ...
156205
@overload
157206
def __call__(
158-
self, var: _str, cast: _Cast[_T1] | None = None, default: _T1 | NoValue = ..., parse_default: _bool = False
159-
) -> _T1: ...
207+
self, var: _str, cast: _Cast[_T] | None = None, default: _T | NoValue = ..., parse_default: _bool = False
208+
) -> _T: ...
160209
@overload
161210
def __call__(
162-
self, var: _str, cast: _list[_Cast[_T1]], default: _list[_T1] | NoValue = ..., parse_default: _bool = False
163-
) -> _list[_T1]: ...
211+
self, var: _str, cast: _list[_Cast[_T]], default: _list[_T] | NoValue = ..., parse_default: _bool = False
212+
) -> _list[_T]: ...
164213
@overload
165214
def __call__(
166-
self, var: _str, cast: _tuple[_Cast[_T1]], default: _tuple[_T1] | NoValue = ..., parse_default: _bool = False
167-
) -> _tuple[_T1]: ...
215+
self, var: _str, cast: _tuple[_Cast[_T], ...], default: _tuple[_T, ...] | NoValue = ..., parse_default: _bool = False
216+
) -> _tuple[_T, ...]: ...
168217
@overload
169218
def __call__(
170-
self, var: _str, cast: CastDict[_T1, _T2], default: _dict[_T1, _T2] | NoValue = ..., parse_default: _bool = False
171-
) -> _dict[_T1, _T2]: ...
219+
self, var: _str, cast: CastDict[_KT, _VT], default: _dict[_KT, _VT] | NoValue = ..., parse_default: _bool = False
220+
) -> _dict[_KT, _VT | object]: ...
172221
@overload
173222
# Any (subclass of) list/tuple/dict builtin types are valid for cast.
174223
def __call__(
@@ -188,13 +237,18 @@ class Env:
188237
def bool(self, var: _str, default: _bool | NoValue = ...) -> _bool: ...
189238
def int(self, var: _str, default: _int | NoValue = ...) -> _int: ...
190239
def float(self, var: _str, default: _float | NoValue = ...) -> _float: ...
191-
def json(self, var: _str, default: Any | NoValue = ...) -> Any: ...
192-
def list(self, var: _str, cast: _Cast[_T1] | None = None, default: _list[_T1] | NoValue = ...) -> _list[_T1]: ...
193-
def tuple(self, var: _str, cast: _Cast[_T1] | None = None, default: _tuple[_T1] | NoValue = ...) -> _tuple[_T1]: ...
194240
@overload
195-
def dict(self, var: _str, cast: None = ..., default: _dict[_str, Any] | NoValue = ...) -> _dict[_str, Any]: ...
241+
def json(self, var: _str, default: NoValue = ...) -> Any: ...
242+
@overload
243+
def json(self, var: _str, default: _T) -> _T: ...
244+
def list(self, var: _str, cast: _Cast[_T] | None = None, default: _list[_T] | NoValue = ...) -> _list[_T]: ...
245+
def tuple(self, var: _str, cast: _Cast[_T] | None = None, default: _tuple[_T, ...] | NoValue = ...) -> _tuple[_T, ...]: ...
196246
@overload
197-
def dict(self, var: _str, cast: _Cast[_T1], default: _dict[Any, _T1] | NoValue = ...) -> _dict[Any, _T1]: ...
247+
def dict(
248+
self, var: _str, cast: CastDict[_KT, _VT] | None = None, default: _dict[_KT, _VT] | NoValue = ...
249+
) -> _dict[_KT, _VT | object]: ...
250+
@overload
251+
def dict(self, var: _str, cast: _Cast[_T], default: _T | NoValue = ...) -> _T: ...
198252
def url(self, var: _str, default: _str | NoValue = ...) -> _str: ...
199253
def db_url(
200254
self, var: _str = ..., default: _str | NoValue = ..., engine: _str | None = None
@@ -222,20 +276,20 @@ class Env:
222276
def path(self, var: _str, default: _str | NoValue = ..., **kwargs: Unpack[PathKwargs]) -> Path: ...
223277
@overload
224278
def get_value(
225-
self, var: _str, cast: _Cast[_T1] | None = None, default: _T1 | NoValue = ..., parse_default: _bool = False
226-
) -> _T1: ...
279+
self, var: _str, cast: _Cast[_T] | None = None, default: _T | NoValue = ..., parse_default: _bool = False
280+
) -> _T: ...
227281
@overload
228282
def get_value(
229-
self, var: _str, cast: _list[_Cast[_T1]], default: _list[_T1] | NoValue = ..., parse_default: _bool = False
230-
) -> _list[_T1]: ...
283+
self, var: _str, cast: _list[_Cast[_T]], default: _list[_T] | NoValue = ..., parse_default: _bool = False
284+
) -> _list[_T]: ...
231285
@overload
232286
def get_value(
233-
self, var: _str, cast: _tuple[_Cast[_T1]], default: _tuple[_T1] | NoValue = ..., parse_default: _bool = False
234-
) -> _tuple[_T1]: ...
287+
self, var: _str, cast: _tuple[_Cast[_T], ...], default: _tuple[_T, ...] | NoValue = ..., parse_default: _bool = False
288+
) -> _tuple[_T, ...]: ...
235289
@overload
236290
def get_value(
237-
self, var: _str, cast: CastDict[_T1, _T2], default: _dict[_T1, _T2] | NoValue = ..., parse_default: _bool = False
238-
) -> _dict[_T1, _T2]: ...
291+
self, var: _str, cast: CastDict[_KT, _VT], default: _dict[_KT, _VT] | NoValue = ..., parse_default: _bool = False
292+
) -> _dict[_KT, _VT | object]: ...
239293
@overload
240294
# Any (subclass of) list/tuple/dict builtin types are valid for cast.
241295
def get_value(
@@ -254,16 +308,40 @@ class Env:
254308
def parse_value(cls, value: _str, cast: None) -> _str: ...
255309
@classmethod
256310
@overload
257-
def parse_value(cls, value: _str, cast: _Cast[_T1]) -> _T1: ...
311+
def parse_value(cls, value: _str, cast: _Cast[_T]) -> _T: ...
312+
@classmethod
313+
@overload
314+
def parse_value(cls, value: _str, cast: _list[_Cast[_T]]) -> _list[_T]: ...
315+
@classmethod
316+
@overload
317+
def parse_value(cls, value: _str, cast: _tuple[_Cast[_T], ...]) -> _tuple[_T, ...]: ...
318+
@classmethod
319+
@overload
320+
def parse_value(cls, value: _str, cast: CastDict000) -> _dict[_str, _str]: ...
321+
@classmethod
322+
@overload
323+
def parse_value(cls, value: _str, cast: CastDict001) -> _dict[_str, _str | object]: ...
324+
@classmethod
325+
@overload
326+
def parse_value(cls, value: _str, cast: CastDict010[_VT]) -> _dict[_str, _VT]: ...
327+
@classmethod
328+
@overload
329+
def parse_value(cls, value: _str, cast: CastDict011[_VT]) -> _dict[_str, _VT | object]: ...
330+
@classmethod
331+
@overload
332+
def parse_value(cls, value: _str, cast: CastDict100[_KT]) -> _dict[_KT, _str]: ...
333+
@classmethod
334+
@overload
335+
def parse_value(cls, value: _str, cast: CastDict101[_KT]) -> _dict[_KT, _str | object]: ...
258336
@classmethod
259337
@overload
260-
def parse_value(cls, value: _str, cast: _list[_Cast[_T1]]) -> _list[_T1]: ...
338+
def parse_value(cls, value: _str, cast: CastDict110[_KT, _VT]) -> _dict[_KT, _VT]: ...
261339
@classmethod
262340
@overload
263-
def parse_value(cls, value: _str, cast: _tuple[_Cast[_T1], ...]) -> _tuple[_T1]: ...
341+
def parse_value(cls, value: _str, cast: CastDict111[_KT, _VT]) -> _dict[_KT, _VT | object]: ...
264342
@classmethod
265343
@overload
266-
def parse_value(cls, value: _str, cast: CastDict[_T1, _T2]) -> _dict[_T1, _T2]: ...
344+
def parse_value(cls, value: _str, cast: CastDict[_KT, _VT]) -> _dict[_KT, _VT | object]: ...
267345
@classmethod
268346
@overload
269347
# Any (subclass of) list/tuple/dict builtin types are valid for cast.

0 commit comments

Comments
 (0)