From e2ac9901e8197f05747127d4c279d3e45d0a07d6 Mon Sep 17 00:00:00 2001 From: ddelange <14880945+ddelange@users.noreply.github.com> Date: Fri, 15 Nov 2024 15:36:26 +0100 Subject: [PATCH 1/3] Fix timedelta parsing breaking change --- src/environs/__init__.py | 23 ++++++++++++----------- tests/test_environs.py | 6 ++++++ 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/src/environs/__init__.py b/src/environs/__init__.py index 8014260..ae3878a 100644 --- a/src/environs/__init__.py +++ b/src/environs/__init__.py @@ -378,17 +378,18 @@ class TimeDeltaField(ma.fields.TimeDelta): def _deserialize(self, value, *args, **kwargs) -> timedelta: if isinstance(value, timedelta): return value - match = _TIMEDELTA_PATTERN.match(value) - if match is not None and match.group(0): # disallow "", allow "0s" - return timedelta( - weeks=int(match.group(1) or 0), - days=int(match.group(2) or 0), - hours=int(match.group(3) or 0), - minutes=int(match.group(4) or 0), - seconds=int(match.group(5) or 0), - milliseconds=int(match.group(6) or 0), - microseconds=int(match.group(7) or 0), - ) + if isinstance(value, str): + match = _TIMEDELTA_PATTERN.match(value) + if match is not None and match.group(0): # disallow "", allow "0s" + return timedelta( + weeks=int(match.group(1) or 0), + days=int(match.group(2) or 0), + hours=int(match.group(3) or 0), + minutes=int(match.group(4) or 0), + seconds=int(match.group(5) or 0), + milliseconds=int(match.group(6) or 0), + microseconds=int(match.group(7) or 0), + ) return super()._deserialize(value, *args, **kwargs) diff --git a/tests/test_environs.py b/tests/test_environs.py index 1920c52..458430b 100644 --- a/tests/test_environs.py +++ b/tests/test_environs.py @@ -229,6 +229,12 @@ def test_date_cast(self, set_env, env): assert env.date("DATE") == date def test_timedelta_cast(self, set_env, env): + # default values + assert env.timedelta("TIMEDELTA", 42) == dt.timedelta(seconds=42) + assert env.timedelta("TIMEDELTA", 42.9) == dt.timedelta(seconds=42) # bug? + assert env.timedelta("TIMEDELTA", dt.timedelta(seconds=42)) == dt.timedelta( + seconds=42, + ) # seconds as integer set_env({"TIMEDELTA": "0"}) assert env.timedelta("TIMEDELTA") == dt.timedelta() From ec0d0bf8ebf71337a53426cdef1d113787304a13 Mon Sep 17 00:00:00 2001 From: ddelange <14880945+ddelange@users.noreply.github.com> Date: Fri, 15 Nov 2024 15:59:15 +0100 Subject: [PATCH 2/3] Account for empty string with whitespace --- src/environs/__init__.py | 16 ++++++++-------- tests/test_environs.py | 6 +++++- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/src/environs/__init__.py b/src/environs/__init__.py index ae3878a..6f30448 100644 --- a/src/environs/__init__.py +++ b/src/environs/__init__.py @@ -380,15 +380,15 @@ def _deserialize(self, value, *args, **kwargs) -> timedelta: return value if isinstance(value, str): match = _TIMEDELTA_PATTERN.match(value) - if match is not None and match.group(0): # disallow "", allow "0s" + if match is not None and any(groups := match.groups(default=0)): return timedelta( - weeks=int(match.group(1) or 0), - days=int(match.group(2) or 0), - hours=int(match.group(3) or 0), - minutes=int(match.group(4) or 0), - seconds=int(match.group(5) or 0), - milliseconds=int(match.group(6) or 0), - microseconds=int(match.group(7) or 0), + weeks=int(groups[0]), + days=int(groups[1]), + hours=int(groups[2]), + minutes=int(groups[3]), + seconds=int(groups[4]), + milliseconds=int(groups[5]), + microseconds=int(groups[6]), ) return super()._deserialize(value, *args, **kwargs) diff --git a/tests/test_environs.py b/tests/test_environs.py index 458430b..f042563 100644 --- a/tests/test_environs.py +++ b/tests/test_environs.py @@ -250,7 +250,7 @@ def test_timedelta_cast(self, set_env, env): set_env({"TIMEDELTA": "-42s"}) assert env.timedelta("TIMEDELTA") == dt.timedelta(seconds=-42) # whitespaces, units subselection (but descending ordering) - set_env({"TIMEDELTA": " 42 d -42s "}) + set_env({"TIMEDELTA": " 42 d \t -42s "}) assert env.timedelta("TIMEDELTA") == dt.timedelta(days=42, seconds=-42) # unicode µs (in addition to us below) set_env({"TIMEDELTA": "42µs"}) @@ -268,6 +268,10 @@ def test_timedelta_cast(self, set_env, env): ) # empty string not allowed set_env({"TIMEDELTA": ""}) + with pytest.raises(environs.EnvError): + env.timedelta("TIMEDELTA") + # empty string with whitespace not allowed + set_env({"TIMEDELTA": " "}) with pytest.raises(environs.EnvError): env.timedelta("TIMEDELTA") # float not allowed From a46856d897bad342738e0ffbc3aa8b3d71ad4eec Mon Sep 17 00:00:00 2001 From: ddelange <14880945+ddelange@users.noreply.github.com> Date: Fri, 15 Nov 2024 16:02:33 +0100 Subject: [PATCH 3/3] Add integer as string default value test case --- tests/test_environs.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_environs.py b/tests/test_environs.py index f042563..5907db0 100644 --- a/tests/test_environs.py +++ b/tests/test_environs.py @@ -230,6 +230,7 @@ def test_date_cast(self, set_env, env): def test_timedelta_cast(self, set_env, env): # default values + assert env.timedelta("TIMEDELTA", "42") == dt.timedelta(seconds=42) assert env.timedelta("TIMEDELTA", 42) == dt.timedelta(seconds=42) assert env.timedelta("TIMEDELTA", 42.9) == dt.timedelta(seconds=42) # bug? assert env.timedelta("TIMEDELTA", dt.timedelta(seconds=42)) == dt.timedelta(