Skip to content

Commit 966c757

Browse files
lucas-bremondLucas Brémondsloria
authored
fix: properly handle typing of list and dict with defined defaults (#407)
* fix: properly handle typing of list and dict with defined defaults * fix typing for list and dict handling with subcasts and None default; expand tests * Update changelog --------- Co-authored-by: Lucas Brémond <[email protected]> Co-authored-by: Steven Loria <[email protected]>
1 parent a5534cb commit 966c757

File tree

3 files changed

+64
-9
lines changed

3 files changed

+64
-9
lines changed

CHANGELOG.md

+3
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44

55
Bug fixes:
66

7+
- Typing: Fix typing for `env.list` and `env.dict` to properly handle
8+
`default` and `subcast` arguments ([#406](https://github.com/sloria/environs/issues/406)).
9+
Thanks [lucas-bremond](https://github.com/lucas-bremond) for the PR.
710
- Add `env` to `__all__` ([#396](https://github.com/sloria/environs/issues/396)).
811
Thanks [daveflr](https://github.com/daveflr) for reporting.
912

src/environs/types.py

+44-9
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
from marshmallow.fields import Field
2222

2323
T = typing.TypeVar("T")
24+
SubcastT = typing.TypeVar("SubcastT")
2425
EnumT = typing.TypeVar("EnumT", bound=enum.Enum)
2526

2627
ErrorMapping: typing.TypeAlias = typing.Mapping[str, list[str]]
@@ -70,7 +71,6 @@ def __call__(
7071
self,
7172
name: str,
7273
default: typing.Any = ...,
73-
subcast: Subcast[T] | None = ...,
7474
**kwargs: Unpack[BaseMethodKwargs],
7575
) -> T | None: ...
7676

@@ -80,40 +80,75 @@ class ListFieldMethod:
8080
def __call__(
8181
self,
8282
name: str,
83-
default: typing.Any = ...,
83+
default: list[T] = ...,
84+
subcast: Subcast[T] = ...,
85+
*,
86+
delimiter: str | None = ...,
87+
**kwargs: Unpack[BaseMethodKwargs],
88+
) -> list[T]: ...
89+
90+
@typing.overload
91+
def __call__(
92+
self,
93+
name: str,
94+
default: T = ...,
8495
subcast: None = ...,
8596
*,
8697
delimiter: str | None = ...,
8798
**kwargs: Unpack[BaseMethodKwargs],
88-
) -> list[typing.Any] | None: ...
99+
) -> list[typing.Any] | T: ...
89100

90101
@typing.overload
91102
def __call__(
92103
self,
93104
name: str,
94-
default: typing.Any = ...,
95-
subcast: Subcast[T] = ...,
105+
default: T = ...,
106+
subcast: Subcast[SubcastT] = ...,
96107
*,
97108
delimiter: str | None = ...,
98109
**kwargs: Unpack[BaseMethodKwargs],
99-
) -> list[T] | None: ...
110+
) -> list[SubcastT] | T: ...
100111

101112
def __call__(
102113
self,
103114
name: str,
104-
default: typing.Any = ...,
105-
subcast: Subcast[T] | None = ...,
115+
default: T = ...,
116+
subcast: Subcast[SubcastT] | None = ...,
106117
*,
107118
delimiter: str | None = ...,
108119
**kwargs: Unpack[BaseMethodKwargs],
109-
) -> list[T] | None: ...
120+
) -> list[SubcastT] | list[typing.Any] | T | None: ...
110121

111122

112123
KeysT = typing.TypeVar("KeysT")
113124
ValuesT = typing.TypeVar("ValuesT")
114125

115126

116127
class DictFieldMethod:
128+
@typing.overload
129+
def __call__(
130+
self,
131+
name: str,
132+
default: dict[KeysT, ValuesT] = ...,
133+
*,
134+
subcast_keys: Subcast[KeysT] | None = None,
135+
subcast_values: Subcast[ValuesT] | None = None,
136+
delimiter: str | None = None,
137+
**kwargs: Unpack[BaseMethodKwargs],
138+
) -> dict[KeysT, ValuesT]: ...
139+
140+
@typing.overload
141+
def __call__(
142+
self,
143+
name: str,
144+
default: None = ...,
145+
*,
146+
subcast_keys: Subcast[KeysT] | None = None,
147+
subcast_values: Subcast[ValuesT] | None = None,
148+
delimiter: str | None = None,
149+
**kwargs: Unpack[BaseMethodKwargs],
150+
) -> dict[KeysT, ValuesT] | None: ...
151+
117152
def __call__(
118153
self,
119154
name: str,

tests/mypy_test_cases/env.py

+17
Original file line numberDiff line numberDiff line change
@@ -41,13 +41,30 @@ class Color(enum.IntEnum):
4141
DECIMAL0: decimal.Decimal | None = env.decimal("FOO", None)
4242
LIST0: list | None = env.list("FOO", None)
4343
LIST1: list[int] | None = env.list("FOO", None, subcast=int)
44+
LIST2: list[Any] = env.list("FOO")
45+
LIST3: list[int] = env.list("FOO", subcast=int)
46+
LIST4: list[int] | bool = env.list("FOO", default=False, subcast=int)
47+
LIST5: list[int] = env.list("FOO", default=[], subcast=int)
4448
DICT0: dict | None = env.dict("FOO", None)
4549
DICT1: dict[str, int] | None = env.dict(
4650
"FOO",
4751
None,
4852
subcast_keys=str,
4953
subcast_values=int,
5054
)
55+
DICT2: dict[str, int] = env.dict(
56+
"FOO",
57+
subcast_keys=str,
58+
subcast_values=int,
59+
)
60+
DICT3: dict[int, Any] = env.dict(
61+
"FOO",
62+
subcast_keys=int,
63+
)
64+
DICT4: dict[Any, int] = env.dict(
65+
"FOO",
66+
subcast_values=int,
67+
)
5168
JSON0: list | dict | None = env.json("FOO", None)
5269
DATETIME0: dt.datetime | None = env.datetime("FOO", None)
5370
DATE0: dt.date | None = env.date("FOO", None)

0 commit comments

Comments
 (0)