🐛 A proper cross-version solution for the utcnow() issue.

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
This commit is contained in:
Fabio Manganiello 2024-06-01 01:34:47 +02:00
parent 1067ab04d9
commit c9a5c29a4a
Signed by: blacklight
GPG key ID: D90FBA7F76362774
7 changed files with 19 additions and 21 deletions

View file

@ -1,10 +1,8 @@
from datetime import timedelta
import logging import logging
from threading import Event from threading import Event
from time import time
from typing import Collection, Optional from typing import Collection, Optional
from platypush.utils import utcnow
from ._base import ( from ._base import (
Entity, Entity,
EntityKey, EntityKey,
@ -47,8 +45,8 @@ def get_entities_engine(timeout: Optional[float] = None) -> EntitiesEngine:
:param timeout: Timeout in seconds (default: None). :param timeout: Timeout in seconds (default: None).
""" """
time_start = utcnow() time_start = time()
while not timeout or (utcnow() - time_start < timedelta(seconds=timeout)): while not timeout or (time() - time_start < timeout):
if _engine: if _engine:
break break

View file

@ -1,4 +1,4 @@
from datetime import timedelta from datetime import timedelta, timezone
from logging import getLogger from logging import getLogger
from queue import Queue from queue import Queue
from typing import Callable, Collection, Dict, Final, List, Optional, Type from typing import Callable, Collection, Dict, Final, List, Optional, Type
@ -99,7 +99,8 @@ event_matchers: Dict[
) )
and ( and (
not (old and old.updated_at) 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)
) )
), ),
} }

View file

@ -97,8 +97,7 @@ class CalendarIcalPlugin(Plugin, CalendarInterface):
if ( if (
event['status'] != 'cancelled' event['status'] != 'cancelled'
and event['end'].get('dateTime') and event['end'].get('dateTime')
and event['end']['dateTime'] and event['end']['dateTime'] >= utcnow().isoformat()
>= utcnow().replace(tzinfo=datetime.timezone.utc).isoformat()
and ( and (
( (
only_participating only_participating

View file

@ -71,7 +71,7 @@ class GoogleCalendarPlugin(GooglePlugin, CalendarInterface):
:meth:`platypush.plugins.calendar.CalendarPlugin.get_upcoming_events`. :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') service = self.get_service('calendar', 'v3')
result = ( result = (
service.events() service.events()

View file

@ -68,7 +68,7 @@ class SunPlugin(RunnablePlugin):
dt = datetime.datetime.strptime( dt = datetime.datetime.strptime(
f'{now.year}-{now.month:02d}-{now.day:02d} {t}', f'{now.year}-{now.month:02d}-{now.day:02d} {t}',
'%Y-%m-%d %I:%M:%S %p', '%Y-%m-%d %I:%M:%S %p',
).replace(tzinfo=datetime.UTC) ).replace(tzinfo=datetime.timezone.utc)
if dt < now: if dt < now:
dt += datetime.timedelta(days=1) dt += datetime.timedelta(days=1)

View file

@ -115,9 +115,13 @@ class UserManager:
.first() .first()
) )
if not user_session or ( expires_at = (
user_session.expires_at and user_session.expires_at < utcnow() 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 return None, None
user = session.query(User).filter_by(user_id=user_session.user_id).first() user = session.query(User).filter_by(user_id=user_session.user_id).first()

View file

@ -816,14 +816,10 @@ def wait_for_either(*events, timeout: Optional[float] = None, cls: Type = Event)
def utcnow(): def utcnow():
""" """
A workaround util to maintain compatibility both with Python >= 3.12 (which utcnow() without tears. It always returns a datetime object in UTC
deprecated datetime.utcnow) and Python < 3.12 (which doesn't have timezone.
datetime.UTC).
""" """
if hasattr(datetime, 'UTC'): return datetime.datetime.now(datetime.timezone.utc)
return datetime.datetime.now(datetime.UTC)
return datetime.datetime.utcnow()
# vim:sw=4:ts=4:et: # vim:sw=4:ts=4:et: