From c9a5c29a4a6a5ac1ee530cd1a008adf30eb64b38 Mon Sep 17 00:00:00 2001 From: Fabio Manganiello Date: Sat, 1 Jun 2024 01:34:47 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20A=20proper=20cross-version=20sol?= =?UTF-8?q?ution=20for=20the=20`utcnow()`=20issue.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit No need to maintain two different pieces of logic - a `utcnow()` for Python < 3.11 and `now(datetime.UTC)` for Python >= 3.11. `datetime.timezone.utc` existed long before datetime.UTC and that's what the `utcnow` facade should use. This means that all the `utcnow()` will always have `tzinfo=UTC` regardless of the Python version. There's still a problem with the `utcnow()`-generated timestamps that have been generated by previous versions of Python and stored on the db. Therefore, when the code performs comparisons with timestamps fetched from the db, it should always explicitly do a `.replace(tzinfo=utc)` to ensure that we always compare offset-aware datetime representations. See blog post for technical details: https://manganiello.blog/wheres-my-time-again --- platypush/entities/__init__.py | 8 +++----- platypush/plugins/bluetooth/_ble/_event_handler.py | 5 +++-- platypush/plugins/calendar/ical/__init__.py | 3 +-- platypush/plugins/google/calendar/__init__.py | 2 +- platypush/plugins/sun/__init__.py | 2 +- platypush/user/__init__.py | 10 +++++++--- platypush/utils/__init__.py | 10 +++------- 7 files changed, 19 insertions(+), 21 deletions(-) diff --git a/platypush/entities/__init__.py b/platypush/entities/__init__.py index 80aeff4d16..665ec86ae8 100644 --- a/platypush/entities/__init__.py +++ b/platypush/entities/__init__.py @@ -1,10 +1,8 @@ -from datetime import timedelta import logging from threading import Event +from time import time from typing import Collection, Optional -from platypush.utils import utcnow - from ._base import ( Entity, EntityKey, @@ -47,8 +45,8 @@ def get_entities_engine(timeout: Optional[float] = None) -> EntitiesEngine: :param timeout: Timeout in seconds (default: None). """ - time_start = utcnow() - while not timeout or (utcnow() - time_start < timedelta(seconds=timeout)): + time_start = time() + while not timeout or (time() - time_start < timeout): if _engine: break diff --git a/platypush/plugins/bluetooth/_ble/_event_handler.py b/platypush/plugins/bluetooth/_ble/_event_handler.py index 0712905373..08bd28a418 100644 --- a/platypush/plugins/bluetooth/_ble/_event_handler.py +++ b/platypush/plugins/bluetooth/_ble/_event_handler.py @@ -1,4 +1,4 @@ -from datetime import timedelta +from datetime import timedelta, timezone from logging import getLogger from queue import Queue from typing import Callable, Collection, Dict, Final, List, Optional, Type @@ -99,7 +99,8 @@ event_matchers: Dict[ ) and ( not (old and old.updated_at) - or utcnow() - old.updated_at >= timedelta(seconds=_rssi_update_interval) + or utcnow() - old.updated_at.replace(tzinfo=timezone.utc) + >= timedelta(seconds=_rssi_update_interval) ) ), } diff --git a/platypush/plugins/calendar/ical/__init__.py b/platypush/plugins/calendar/ical/__init__.py index 1d240f62b4..e71325a4e4 100644 --- a/platypush/plugins/calendar/ical/__init__.py +++ b/platypush/plugins/calendar/ical/__init__.py @@ -97,8 +97,7 @@ class CalendarIcalPlugin(Plugin, CalendarInterface): if ( event['status'] != 'cancelled' and event['end'].get('dateTime') - and event['end']['dateTime'] - >= utcnow().replace(tzinfo=datetime.timezone.utc).isoformat() + and event['end']['dateTime'] >= utcnow().isoformat() and ( ( only_participating diff --git a/platypush/plugins/google/calendar/__init__.py b/platypush/plugins/google/calendar/__init__.py index 225e9c4c7f..1aef356da5 100644 --- a/platypush/plugins/google/calendar/__init__.py +++ b/platypush/plugins/google/calendar/__init__.py @@ -71,7 +71,7 @@ class GoogleCalendarPlugin(GooglePlugin, CalendarInterface): :meth:`platypush.plugins.calendar.CalendarPlugin.get_upcoming_events`. """ - now = utcnow().isoformat() + 'Z' + now = utcnow().replace(tzinfo=None).isoformat() + 'Z' service = self.get_service('calendar', 'v3') result = ( service.events() diff --git a/platypush/plugins/sun/__init__.py b/platypush/plugins/sun/__init__.py index 90cfdc723b..6933917df9 100644 --- a/platypush/plugins/sun/__init__.py +++ b/platypush/plugins/sun/__init__.py @@ -68,7 +68,7 @@ class SunPlugin(RunnablePlugin): dt = datetime.datetime.strptime( f'{now.year}-{now.month:02d}-{now.day:02d} {t}', '%Y-%m-%d %I:%M:%S %p', - ).replace(tzinfo=datetime.UTC) + ).replace(tzinfo=datetime.timezone.utc) if dt < now: dt += datetime.timedelta(days=1) diff --git a/platypush/user/__init__.py b/platypush/user/__init__.py index 64aa64cfee..a6db904bd3 100644 --- a/platypush/user/__init__.py +++ b/platypush/user/__init__.py @@ -115,9 +115,13 @@ class UserManager: .first() ) - if not user_session or ( - user_session.expires_at and user_session.expires_at < utcnow() - ): + expires_at = ( + user_session.expires_at.replace(tzinfo=datetime.timezone.utc) + if user_session and user_session.expires_at + else None + ) + + if not user_session or (expires_at and expires_at < utcnow()): return None, None user = session.query(User).filter_by(user_id=user_session.user_id).first() diff --git a/platypush/utils/__init__.py b/platypush/utils/__init__.py index 9fd93d3e5c..f7c9c961ca 100644 --- a/platypush/utils/__init__.py +++ b/platypush/utils/__init__.py @@ -816,14 +816,10 @@ def wait_for_either(*events, timeout: Optional[float] = None, cls: Type = Event) def utcnow(): """ - A workaround util to maintain compatibility both with Python >= 3.12 (which - deprecated datetime.utcnow) and Python < 3.12 (which doesn't have - datetime.UTC). + utcnow() without tears. It always returns a datetime object in UTC + timezone. """ - if hasattr(datetime, 'UTC'): - return datetime.datetime.now(datetime.UTC) - - return datetime.datetime.utcnow() + return datetime.datetime.now(datetime.timezone.utc) # vim:sw=4:ts=4:et: