Skip to content

Commit a91c034

Browse files
committed
Address Brian feedback
1 parent 076c8e2 commit a91c034

3 files changed

Lines changed: 86 additions & 143 deletions

File tree

Lines changed: 16 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,24 @@
11
from __future__ import annotations
22

3-
from typing import Union
4-
from typing_extensions import TypedDict, assert_type
3+
from typing import Any
4+
from typing_extensions import assert_type
5+
from urllib.parse import ParseResult
56

67
import environ
78

89
env = environ.Env()
910

1011
assert_type(env.parse_value("just-a-value123", None), str)
1112

13+
# helpers preserve default value types for missing environment variables
14+
assert_type(env.str("NAME"), str)
15+
assert_type(env.str("NAME", default=None), str | None)
16+
assert_type(env.bool("DEBUG", default=None), bool | None)
17+
assert_type(env.int("PORT", default=None), int | None)
18+
assert_type(env.float("RATIO", default=None), float | None)
19+
assert_type(env.url("URL"), ParseResult)
20+
assert_type(env.url("URL", default=None), ParseResult | None)
21+
1222
# builtin types
1323
assert_type(env.parse_value("string", str), str)
1424
assert_type(env.parse_value("TRUE", bool), bool)
@@ -31,15 +41,10 @@
3141
assert_type(env.parse_value("(20.5,-0.2)", (int,)), tuple[int, ...])
3242
assert_type(env.parse_value("(20.5,-0.2)", (float,)), tuple[float, ...])
3343

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]])
44+
# dict-valued casts split pairs with semicolons.
45+
assert_type(env.parse_value("0=TRUE;99=FALSE", {}), dict[Any, Any])
46+
assert_type(env.parse_value("0=TRUE;99=FALSE", {"key": int, "value": bool}), dict[Any, Any])
47+
assert_type(env.parse_value("0=TRUE;99=FALSE", {"cast": {"0": bool}}), dict[Any, Any])
4348

4449

4550
# custom cast functions
@@ -48,17 +53,3 @@ def cast_float(x: str) -> float:
4853

4954

5055
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/__init__.pyi

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
from typing import Final
22

3-
from .compat import DJANGO_POSTGRES as DJANGO_POSTGRES, PYMEMCACHE_DRIVER as PYMEMCACHE_DRIVER, REDIS_DRIVER as REDIS_DRIVER
43
from .environ import *
54

65
__copyright__: Final[str]

stubs/django-environ/environ/environ.pyi

Lines changed: 70 additions & 117 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from _typeshed import Incomplete, StrPath
22

3-
# Use aliases to avoid name conflicts with Path methods
3+
# Use aliases to avoid name conflicts with Env methods.
44
from builtins import (
55
bool as _bool,
66
bytes as _bytes,
@@ -13,10 +13,11 @@ 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, Literal, SupportsIndex, TypedDict, TypeVar, overload, type_check_only
17-
from typing_extensions import NotRequired, Required, TypeAlias, Unpack
16+
from typing import IO, Any, ClassVar, SupportsIndex, TypedDict, TypeVar, overload, type_check_only
17+
from typing_extensions import Required, TypeAlias, Unpack
1818
from urllib.parse import ParseResult
1919

20+
from .compat import DJANGO_POSTGRES as DJANGO_POSTGRES, PYMEMCACHE_DRIVER as PYMEMCACHE_DRIVER, REDIS_DRIVER as REDIS_DRIVER
2021
from .fileaware_mapping import FileAwareMapping
2122

2223
Openable: tuple[type, ...]
@@ -25,65 +26,16 @@ logger: Logger
2526
class NoValue: ...
2627

2728
_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]
29+
_CastFunc: TypeAlias = Callable[[str], _T]
30+
_SchemeValue: TypeAlias = _CastFunc[object] | tuple[_CastFunc[object], object]
3331
_EmptyDict: TypeAlias = dict[object, object] # stands for {}
3432

3533
@type_check_only
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]]
34+
class _CastDict(TypedDict, total=False):
35+
key: _CastFunc[Any]
36+
value: _CastFunc[Any]
37+
# Key-specific value casts can make the value type depend on the input key.
38+
cast: dict[str, _CastFunc[Any]]
8739

8840
@type_check_only
8941
class PathKwargs(TypedDict, total=False):
@@ -204,20 +156,20 @@ class Env:
204156
def __init__(self, **scheme: _SchemeValue) -> None: ...
205157
@overload
206158
def __call__(
207-
self, var: _str, cast: _Cast[_T] | None = None, default: _T | NoValue = ..., parse_default: _bool = False
159+
self, var: _str, cast: _CastFunc[_T] | None = None, default: _T | NoValue = ..., parse_default: _bool = False
208160
) -> _T: ...
209161
@overload
210162
def __call__(
211-
self, var: _str, cast: _list[_Cast[_T]], default: _list[_T] | NoValue = ..., parse_default: _bool = False
163+
self, var: _str, cast: _list[_CastFunc[_T]], default: _list[_T] | NoValue = ..., parse_default: _bool = False
212164
) -> _list[_T]: ...
213165
@overload
214166
def __call__(
215-
self, var: _str, cast: _tuple[_Cast[_T], ...], default: _tuple[_T, ...] | NoValue = ..., parse_default: _bool = False
167+
self, var: _str, cast: _tuple[_CastFunc[_T], ...], default: _tuple[_T, ...] | NoValue = ..., parse_default: _bool = False
216168
) -> _tuple[_T, ...]: ...
217169
@overload
218170
def __call__(
219-
self, var: _str, cast: CastDict[_KT, _VT], default: _dict[_KT, _VT] | NoValue = ..., parse_default: _bool = False
220-
) -> _dict[_KT, _VT | object]: ...
171+
self, var: _str, cast: _CastDict, default: _dict[Any, Any] | NoValue = ..., parse_default: _bool = False
172+
) -> _dict[Any, Any]: ...
221173
@overload
222174
# Any (subclass of) list/tuple/dict builtin types are valid for cast.
223175
def __call__(
@@ -232,24 +184,48 @@ class Env:
232184
self, var: _str, cast: type[_dict[Any, Any]], default: _dict[_str, _str] | NoValue = ..., parse_default: _bool = False
233185
) -> _dict[_str, _str]: ...
234186
def __contains__(self, var: _str) -> _bool: ...
235-
def str(self, var: _str, default: _str | NoValue = ..., multiline: _bool = False) -> _str: ...
236-
def bytes(self, var: _str, default: _bytes | NoValue = ..., encoding: _str = "utf8") -> _bytes: ...
237-
def bool(self, var: _str, default: _bool | NoValue = ...) -> _bool: ...
238-
def int(self, var: _str, default: _int | NoValue = ...) -> _int: ...
239-
def float(self, var: _str, default: _float | NoValue = ...) -> _float: ...
187+
@overload
188+
def str(self, var: _str, default: NoValue = ..., multiline: _bool = False) -> _str: ...
189+
@overload
190+
def str(self, var: _str, default: _T, multiline: _bool = False) -> _str | _T: ...
191+
@overload
192+
def bytes(self, var: _str, default: NoValue = ..., encoding: _str = "utf8") -> _bytes: ...
193+
@overload
194+
def bytes(self, var: _str, default: _T, encoding: _str = "utf8") -> _bytes | _T: ...
195+
@overload
196+
def bool(self, var: _str, default: NoValue = ...) -> _bool: ...
197+
@overload
198+
def bool(self, var: _str, default: _T) -> _bool | _T: ...
199+
@overload
200+
def int(self, var: _str, default: NoValue = ...) -> _int: ...
201+
@overload
202+
def int(self, var: _str, default: _T) -> _int | _T: ...
203+
@overload
204+
def float(self, var: _str, default: NoValue = ...) -> _float: ...
205+
@overload
206+
def float(self, var: _str, default: _T) -> _float | _T: ...
240207
@overload
241208
def json(self, var: _str, default: NoValue = ...) -> Any: ...
242209
@overload
243210
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, ...]: ...
211+
def list(self, var: _str, cast: _CastFunc[_T] | None = None, default: _list[_T] | NoValue = ...) -> _list[_T]: ...
212+
def tuple(
213+
self, var: _str, cast: _CastFunc[_T] | None = None, default: _tuple[_T, ...] | NoValue = ...
214+
) -> _tuple[_T, ...]: ...
246215
@overload
247216
def dict(
248-
self, var: _str, cast: CastDict[_KT, _VT] | None = None, default: _dict[_KT, _VT] | NoValue = ...
249-
) -> _dict[_KT, _VT | object]: ...
217+
self, var: _str, cast: type[_dict[Any, Any]] = ..., default: _dict[_str, _str] | NoValue = ...
218+
) -> _dict[_str, _str]: ...
219+
@overload
220+
def dict(self, var: _str, cast: _CastDict, default: _dict[Any, Any] | NoValue = ...) -> _dict[Any, Any]: ...
221+
@overload
222+
def dict(self, var: _str, cast: _CastFunc[_T], default: _T | NoValue = ...) -> _T: ...
250223
@overload
251-
def dict(self, var: _str, cast: _Cast[_T], default: _T | NoValue = ...) -> _T: ...
252-
def url(self, var: _str, default: _str | NoValue = ...) -> _str: ...
224+
def url(self, var: _str, default: NoValue = ...) -> ParseResult: ...
225+
@overload
226+
def url(self, var: _str, default: None) -> ParseResult | None: ...
227+
@overload
228+
def url(self, var: _str, default: _str) -> ParseResult: ...
253229
def db_url(
254230
self, var: _str = ..., default: _str | NoValue = ..., engine: _str | None = None
255231
) -> MemoryDbConfig | DbConfig | _EmptyDict: ...
@@ -276,20 +252,20 @@ class Env:
276252
def path(self, var: _str, default: _str | NoValue = ..., **kwargs: Unpack[PathKwargs]) -> Path: ...
277253
@overload
278254
def get_value(
279-
self, var: _str, cast: _Cast[_T] | None = None, default: _T | NoValue = ..., parse_default: _bool = False
255+
self, var: _str, cast: _CastFunc[_T] | None = None, default: _T | NoValue = ..., parse_default: _bool = False
280256
) -> _T: ...
281257
@overload
282258
def get_value(
283-
self, var: _str, cast: _list[_Cast[_T]], default: _list[_T] | NoValue = ..., parse_default: _bool = False
259+
self, var: _str, cast: _list[_CastFunc[_T]], default: _list[_T] | NoValue = ..., parse_default: _bool = False
284260
) -> _list[_T]: ...
285261
@overload
286262
def get_value(
287-
self, var: _str, cast: _tuple[_Cast[_T], ...], default: _tuple[_T, ...] | NoValue = ..., parse_default: _bool = False
263+
self, var: _str, cast: _tuple[_CastFunc[_T], ...], default: _tuple[_T, ...] | NoValue = ..., parse_default: _bool = False
288264
) -> _tuple[_T, ...]: ...
289265
@overload
290266
def get_value(
291-
self, var: _str, cast: CastDict[_KT, _VT], default: _dict[_KT, _VT] | NoValue = ..., parse_default: _bool = False
292-
) -> _dict[_KT, _VT | object]: ...
267+
self, var: _str, cast: _CastDict, default: _dict[Any, Any] | NoValue = ..., parse_default: _bool = False
268+
) -> _dict[Any, Any]: ...
293269
@overload
294270
# Any (subclass of) list/tuple/dict builtin types are valid for cast.
295271
def get_value(
@@ -308,40 +284,16 @@ class Env:
308284
def parse_value(cls, value: _str, cast: None) -> _str: ...
309285
@classmethod
310286
@overload
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]: ...
287+
def parse_value(cls, value: _str, cast: _CastFunc[_T]) -> _T: ...
336288
@classmethod
337289
@overload
338-
def parse_value(cls, value: _str, cast: CastDict110[_KT, _VT]) -> _dict[_KT, _VT]: ...
290+
def parse_value(cls, value: _str, cast: _list[_CastFunc[_T]]) -> _list[_T]: ...
339291
@classmethod
340292
@overload
341-
def parse_value(cls, value: _str, cast: CastDict111[_KT, _VT]) -> _dict[_KT, _VT | object]: ...
293+
def parse_value(cls, value: _str, cast: _tuple[_CastFunc[_T], ...]) -> _tuple[_T, ...]: ...
342294
@classmethod
343295
@overload
344-
def parse_value(cls, value: _str, cast: CastDict[_KT, _VT]) -> _dict[_KT, _VT | object]: ...
296+
def parse_value(cls, value: _str, cast: _CastDict) -> _dict[Any, Any]: ...
345297
@classmethod
346298
@overload
347299
# Any (subclass of) list/tuple/dict builtin types are valid for cast.
@@ -378,18 +330,19 @@ class FileAwareEnv(Env):
378330
ENVIRON: FileAwareMapping
379331

380332
class Path:
381-
def path(self, *paths: str, **kwargs: Unpack[PathKwargs]) -> Path: ...
382-
def file(self, name: str, *args, **kwargs) -> IO[str]: ...
333+
def path(self, *paths: StrPath, **kwargs: Unpack[PathKwargs]) -> Path: ...
334+
# *args and **kwargs are passed to open().
335+
def file(self, name: str, *args: Any, **kwargs: Any) -> IO[Any]: ...
383336
@property
384337
def root(self) -> str: ...
385338

386339
__root__: str
387340

388-
def __init__(self, start: str = "", *paths: str, **kwargs: Unpack[PathKwargs]) -> None: ...
389-
def __call__(self, *paths: str, **kwargs: Unpack[PathKwargs]) -> str: ...
390-
def __eq__(self, other: object | Path) -> bool: ...
391-
def __ne__(self, other: object | Path) -> bool: ...
392-
def __add__(self, other: object | Path) -> Path: ...
341+
def __init__(self, start: StrPath = "", *paths: StrPath, **kwargs: Unpack[PathKwargs]) -> None: ...
342+
def __call__(self, *paths: StrPath, **kwargs: Unpack[PathKwargs]) -> str: ...
343+
def __eq__(self, other: object) -> bool: ...
344+
def __ne__(self, other: object) -> bool: ...
345+
def __add__(self, other: object) -> Path: ...
393346
def __sub__(self, other: int | str) -> Path: ...
394347
def __invert__(self) -> Path: ...
395348
def __contains__(self, item: Path) -> bool: ...
@@ -399,5 +352,5 @@ class Path:
399352
@overload
400353
def __getitem__(self, key: slice, /) -> str: ...
401354
def __fspath__(self) -> str: ...
402-
def rfind(self, s: str, sub: str, start: int = 0, end: int = ...) -> int: ...
403-
def find(self, s: str, sub: str, start: int = 0, end: int = ...) -> int: ...
355+
def rfind(self, s: str, start: SupportsIndex | None = ..., end: SupportsIndex | None = ..., /) -> int: ...
356+
def find(self, s: str, start: SupportsIndex | None = ..., end: SupportsIndex | None = ..., /) -> int: ...

0 commit comments

Comments
 (0)