Skip to content

Commit 40e11ba

Browse files
authored
Expect default to be in deserialized form (#379)
* Expect default to be in its internal type * Remove unnecessary xfail * Update changelog
1 parent 1536a04 commit 40e11ba

File tree

4 files changed

+45
-36
lines changed

4 files changed

+45
-36
lines changed

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ repos:
1414
rev: 1.19.1
1515
hooks:
1616
- id: blacken-docs
17-
additional_dependencies: [black==24.10.0 ]
17+
additional_dependencies: [black==24.10.0]
1818
- repo: https://github.com/thlorenz/doctoc.git
1919
rev: v2.2.0
2020
hooks:

CHANGELOG.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,33 @@
11
# Changelog
22

3+
## 14.0.0 (unreleased)
4+
5+
Changes:
6+
7+
- `default` values are expected to be their in their deserialized form.
8+
_Backwards-incompatible_: Passing serialized values to `default`
9+
is no longer supported.
10+
11+
```python
12+
from datetime import date, timedelta
13+
import environs
14+
15+
# DO
16+
enable_login = env.bool("ENABLE_LOGIN", True)
17+
ttl = env.timedelta("TTL", default=timedelta(seconds=600))
18+
release_date = env.date("RELEASE", date(2025, 1, 7))
19+
numbers = env.list("FOO", [1.0, 2.0, 3.0], subcast=float)
20+
21+
# DON'T
22+
enable_login = env.bool("ENABLE_LOGIN", "true")
23+
ttl = env.timedelta("TTL", default=600)
24+
release_date = env.date("RELEASE", "2025-01-07")
25+
numbers = env.list("NUMBERS", "1,2,42", subcast=float)
26+
```
27+
28+
This fixes [#297](https://github.com/sloria/environs/issues/297)
29+
and [#270](https://github.com/sloria/environs/issues/270).
30+
331
## 13.0.0 (2025-01-07)
432

533
Features:

src/environs/__init__.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,11 +107,14 @@ def method(
107107
load_default=load_default,
108108
)
109109
parsed_key, value, proxied_key = self._get_from_environ(
110-
name, default=field.load_default
110+
name, default=ma.missing
111111
)
112112
self._fields[parsed_key] = field
113113
source_key = proxied_key or parsed_key
114114
if value is ma.missing:
115+
if default is not ...:
116+
self._values[parsed_key] = default
117+
return default
115118
if self.eager:
116119
raise EnvError(
117120
f'Environment variable "{proxied_key or parsed_key}" not set'

tests/test_environs.py

Lines changed: 12 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -90,12 +90,17 @@ def test_list_cast(self, set_env, env: environs.Env):
9090
set_env({"LIST": "1,2,3"})
9191
assert env.list("LIST") == ["1", "2", "3"]
9292

93-
def test_list_with_default_from_string(self, env: environs.Env):
94-
assert env.list("LIST", "1,2") == ["1", "2"]
95-
9693
def test_list_with_default_from_list(self, env: environs.Env):
9794
assert env.list("LIST", ["1"]) == ["1"]
9895

96+
# https://github.com/sloria/environs/issues/270
97+
def test_list_with_default_list_and_subcast(self, set_env, env: environs.Env):
98+
expected = [("a", "b"), ("b", "c")]
99+
assert (
100+
env.list("LIST", expected, subcast=lambda s: tuple(s.split(":")))
101+
== expected
102+
)
103+
99104
# https://github.com/sloria/environs/issues/298
100105
def test_list_with_default_none(self, env: environs.Env):
101106
assert env.list("LIST", default=None) is None
@@ -174,10 +179,7 @@ def custom_tuple(value: str):
174179
"DICT", subcast_keys=custom_tuple, subcast_values=custom_tuple
175180
) == {("1", "1"): ("foo", "bar")}
176181

177-
def test_dict_with_default_from_string(self, set_env, env: environs.Env):
178-
assert env.dict("DICT", "key1=1,key2=2") == {"key1": "1", "key2": "2"}
179-
180-
def test_dict_with_default_from_dict(self, set_env, env: environs.Env):
182+
def test_dict_with_dict_default(self, env: environs.Env):
181183
assert env.dict("DICT", {"key1": "1"}) == {"key1": "1"}
182184

183185
def test_dict_with_equal(self, set_env, env: environs.Env):
@@ -212,9 +214,6 @@ def test_invalid_json_raises_error(self, set_env, env: environs.Env):
212214
def test_json_default(self, set_env, env: environs.Env):
213215
assert env.json("JSON", {"foo": "bar"}) == {"foo": "bar"}
214216
assert env.json("JSON", ["foo", "bar"]) == ["foo", "bar"]
215-
with pytest.raises(environs.EnvError) as exc:
216-
env.json("JSON", "invalid") # type: ignore[call-overload]
217-
assert "Not valid JSON." in exc.value.args[0]
218217

219218
def test_datetime_cast(self, set_env, env: environs.Env):
220219
dtime = dt.datetime.now(dt.timezone.utc)
@@ -230,10 +229,6 @@ def test_date_cast(self, set_env, env: environs.Env):
230229
set_env({"DATE": date.isoformat()})
231230
assert env.date("DATE") == date
232231

233-
@pytest.mark.xfail(
234-
MARSHMALLOW_VERSION.major < 4,
235-
reason="marshmallow 3 does not allow all fields to accept internal types",
236-
)
237232
@pytest.mark.parametrize(
238233
("method_name", "value"),
239234
[
@@ -242,16 +237,13 @@ def test_date_cast(self, set_env, env: environs.Env):
242237
pytest.param("time", dt.time(1, 2, 3), id="time"),
243238
],
244239
)
245-
def test_default_can_be_set_to_internal_type(
240+
def test_default_set_to_internal_type(
246241
self, env: environs.Env, method_name: str, value
247242
):
248243
method = getattr(env, method_name)
249244
assert method("NOTFOUND", value) == value
250245

251246
def test_timedelta_cast(self, set_env, env: environs.Env):
252-
# default values may be in serialized form
253-
assert env.timedelta("TIMEDELTA", "42") == dt.timedelta(seconds=42) # type: ignore[call-overload]
254-
assert env.timedelta("TIMEDELTA", 42) == dt.timedelta(seconds=42) # type: ignore[call-overload]
255247
# marshmallow 4 preserves float values as microseconds
256248
if MARSHMALLOW_VERSION.major >= 4:
257249
set_env({"TIMEDELTA": "42.9"})
@@ -490,8 +482,9 @@ def test_can_add_marshmallow_validator(self, set_env, env: environs.Env):
490482
env("NODE_ENV", validate=validate.OneOf(["development", "production"]))
491483

492484
def test_validator_can_raise_enverror(self, set_env, env: environs.Env):
485+
set_env({"NODE_ENV": "test"})
493486
with pytest.raises(environs.EnvError) as excinfo:
494-
env("NODE_ENV", "development", validate=always_fail)
487+
env("NODE_ENV", validate=always_fail)
495488
assert "something went wrong" in excinfo.value.args[0]
496489

497490
def test_failed_vars_are_not_serialized(self, set_env, env: environs.Env):
@@ -974,21 +967,6 @@ def test_recursive_expands(self, env: environs.Env, set_env):
974967
)
975968
assert env.str("PGURL") == "postgres://gnarvaja:secret@localhost"
976969

977-
def test_default_expands(self, env: environs.Env, set_env):
978-
set_env(
979-
{
980-
"MAIN": "${SUBSTI}",
981-
"SUBSTI": "substivalue",
982-
}
983-
)
984-
assert env.str("NOT_SET", "${SUBSTI}") == "substivalue"
985-
assert env.str("NOT_SET", "${MAIN}") == "substivalue"
986-
assert env.str("NOT_SET", "${NOT_SET2:-set2}") == "set2"
987-
with pytest.raises(
988-
environs.EnvError, match='Environment variable "NOT_SET2" not set'
989-
):
990-
assert env.str("NOT_SET", "${NOT_SET2}")
991-
992970
def test_escaped_expand(self, env: environs.Env, set_env):
993971
set_env({"ESCAPED_EXPAND": r"\${ESCAPED}", "ESCAPED": "fail"})
994972
assert env.str("ESCAPED_EXPAND") == r"${ESCAPED}"

0 commit comments

Comments
 (0)