From f8584c3deded54900018c4ecee36fec8552ba73c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 10 May 2021 08:32:29 -0700 Subject: [PATCH] Use zoneinfo instead of dateutil (#50387) Co-authored-by: J. Nick Koston --- homeassistant/package_constraints.txt | 2 +- homeassistant/util/dt.py | 34 ++++++++++++++++++++++----- requirements.txt | 2 +- setup.py | 2 +- 4 files changed, 31 insertions(+), 9 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 13960edfed5..713c2f1a62f 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -8,6 +8,7 @@ async-upnp-client==0.17.0 async_timeout==3.0.1 attrs==21.2.0 awesomeversion==21.2.3 +backports.zoneinfo;python_version<"3.9" bcrypt==3.1.7 certifi>=2020.12.5 ciso8601==2.1.3 @@ -24,7 +25,6 @@ paho-mqtt==1.5.1 pillow==8.1.2 pip>=8.0.3,<20.3 pyroute2==0.5.18 -python-dateutil==2.8.1 python-slugify==4.0.1 pyyaml==5.4.1 requests==2.25.1 diff --git a/homeassistant/util/dt.py b/homeassistant/util/dt.py index a144918713e..c818c955370 100644 --- a/homeassistant/util/dt.py +++ b/homeassistant/util/dt.py @@ -6,8 +6,12 @@ import datetime as dt import re from typing import Any +try: + import zoneinfo +except ImportError: + from backports import zoneinfo + import ciso8601 -from dateutil import tz from homeassistant.const import MATCH_ALL @@ -43,7 +47,10 @@ def get_time_zone(time_zone_str: str) -> dt.tzinfo | None: Async friendly. """ - return tz.gettz(time_zone_str) + try: + return zoneinfo.ZoneInfo(time_zone_str) # type: ignore + except zoneinfo.ZoneInfoNotFoundError: + return None def utcnow() -> dt.datetime: @@ -311,7 +318,7 @@ def find_next_time_expression_time( if result.tzinfo in (None, UTC): return result - if tz.datetime_ambiguous(result): + if _datetime_ambiguous(result): # This happens when we're leaving daylight saving time and local # clocks are rolled back. In this case, we want to trigger # on both the DST and non-DST time. So when "now" is in the DST @@ -320,7 +327,7 @@ def find_next_time_expression_time( if result.fold != fold: result = result.replace(fold=fold) - if not tz.datetime_exists(result): + if not _datetime_exists(result): # This happens when we're entering daylight saving time and local # clocks are rolled forward, thus there are local times that do # not exist. In this case, we want to trigger on the next time @@ -337,11 +344,26 @@ def find_next_time_expression_time( # For example: if triggering on 2:30 and now is 28.10.2018 2:30 (in DST) # we should trigger next on 28.10.2018 2:30 (out of DST), but our # algorithm above would produce 29.10.2018 2:30 (out of DST) - if tz.datetime_ambiguous(now): + if _datetime_ambiguous(now): check_result = find_next_time_expression_time( now + _dst_offset_diff(now), seconds, minutes, hours ) - if tz.datetime_ambiguous(check_result): + if _datetime_ambiguous(check_result): return check_result return result + + +def _datetime_exists(dattim: dt.datetime) -> bool: + """Check if a datetime exists.""" + assert dattim.tzinfo is not None + original_tzinfo = dattim.tzinfo + # Check if we can round trip to UTC + return dattim == dattim.astimezone(UTC).astimezone(original_tzinfo) + + +def _datetime_ambiguous(dattim: dt.datetime) -> bool: + """Check whether a datetime is ambiguous.""" + assert dattim.tzinfo is not None + opposite_fold = dattim.replace(fold=not dattim.fold) + return _datetime_exists(dattim) and dattim.utcoffset() != opposite_fold.utcoffset() diff --git a/requirements.txt b/requirements.txt index 24e387bed58..39a30934d4c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,6 +6,7 @@ astral==2.2 async_timeout==3.0.1 attrs==21.2.0 awesomeversion==21.2.3 +backports.zoneinfo;python_version<"3.9" bcrypt==3.1.7 certifi>=2020.12.5 ciso8601==2.1.3 @@ -15,7 +16,6 @@ PyJWT==1.7.1 cryptography==3.3.2 pip>=8.0.3,<20.3 python-slugify==4.0.1 -python-dateutil==2.8.1 pyyaml==5.4.1 requests==2.25.1 ruamel.yaml==0.15.100 diff --git a/setup.py b/setup.py index d5b9d6e68b3..43a8107743e 100755 --- a/setup.py +++ b/setup.py @@ -37,6 +37,7 @@ REQUIRES = [ "async_timeout==3.0.1", "attrs==21.2.0", "awesomeversion==21.2.3", + 'backports.zoneinfo;python_version<"3.9"', "bcrypt==3.1.7", "certifi>=2020.12.5", "ciso8601==2.1.3", @@ -47,7 +48,6 @@ REQUIRES = [ "cryptography==3.3.2", "pip>=8.0.3,<20.3", "python-slugify==4.0.1", - "python-dateutil==2.8.1", "pyyaml==5.4.1", "requests==2.25.1", "ruamel.yaml==0.15.100",