mirror of
https://github.com/Rapptz/discord.py.git
synced 2025-06-07 20:28:38 +00:00
[tasks] Handle imaginary or ambiguous times due to DST transitions
This commit is contained in:
parent
f63070c071
commit
f2586e9fe7
@ -64,6 +64,48 @@ FT = TypeVar('FT', bound=_func)
|
|||||||
ET = TypeVar('ET', bound=Callable[[Any, BaseException], Awaitable[Any]])
|
ET = TypeVar('ET', bound=Callable[[Any, BaseException], Awaitable[Any]])
|
||||||
|
|
||||||
|
|
||||||
|
def is_ambiguous(dt: datetime.datetime) -> bool:
|
||||||
|
if dt.tzinfo is None or isinstance(dt.tzinfo, datetime.timezone):
|
||||||
|
# Naive or fixed timezones are never ambiguous
|
||||||
|
return False
|
||||||
|
|
||||||
|
before = dt.replace(fold=0)
|
||||||
|
after = dt.replace(fold=1)
|
||||||
|
|
||||||
|
same_offset = before.utcoffset() == after.utcoffset()
|
||||||
|
same_dst = before.dst() == after.dst()
|
||||||
|
return not (same_offset and same_dst)
|
||||||
|
|
||||||
|
|
||||||
|
def is_imaginary(dt: datetime.datetime) -> bool:
|
||||||
|
if dt.tzinfo is None or isinstance(dt.tzinfo, datetime.timezone):
|
||||||
|
# Naive or fixed timezones are never imaginary
|
||||||
|
return False
|
||||||
|
|
||||||
|
tz = dt.tzinfo
|
||||||
|
dt = dt.replace(tzinfo=None)
|
||||||
|
roundtrip = dt.replace(tzinfo=tz).astimezone(datetime.timezone.utc).astimezone(tz).replace(tzinfo=None)
|
||||||
|
return dt != roundtrip
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_datetime(dt: datetime.datetime) -> datetime.datetime:
|
||||||
|
if dt.tzinfo is None or isinstance(dt.tzinfo, datetime.timezone):
|
||||||
|
# Naive or fixed requires no resolution
|
||||||
|
return dt
|
||||||
|
|
||||||
|
if is_imaginary(dt):
|
||||||
|
# Largest gap is probably 24 hours
|
||||||
|
tomorrow = dt + datetime.timedelta(days=1)
|
||||||
|
yesterday = dt - datetime.timedelta(days=1)
|
||||||
|
# utcoffset shouldn't return None since these are aware instances
|
||||||
|
# If it returns None then the timezone implementation was broken from the get go
|
||||||
|
return dt + (tomorrow.utcoffset() - yesterday.utcoffset()) # type: ignore
|
||||||
|
elif is_ambiguous(dt):
|
||||||
|
return dt.replace(fold=1)
|
||||||
|
else:
|
||||||
|
return dt
|
||||||
|
|
||||||
|
|
||||||
class SleepHandle:
|
class SleepHandle:
|
||||||
__slots__ = ('future', 'loop', 'handle')
|
__slots__ = ('future', 'loop', 'handle')
|
||||||
|
|
||||||
@ -596,7 +638,7 @@ class Loop(Generic[LF]):
|
|||||||
date = now.date()
|
date = now.date()
|
||||||
time = self._time[index]
|
time = self._time[index]
|
||||||
|
|
||||||
return datetime.datetime.combine(date, time, tzinfo=time.tzinfo or datetime.timezone.utc)
|
return resolve_datetime(datetime.datetime.combine(date, time, tzinfo=time.tzinfo or datetime.timezone.utc))
|
||||||
|
|
||||||
def _start_time_relative_to(self, now: datetime.datetime) -> Optional[int]:
|
def _start_time_relative_to(self, now: datetime.datetime) -> Optional[int]:
|
||||||
# now kwarg should be a datetime.datetime representing the time "now"
|
# now kwarg should be a datetime.datetime representing the time "now"
|
||||||
|
@ -10,6 +10,7 @@ import asyncio
|
|||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
import sys
|
||||||
|
|
||||||
from discord import utils
|
from discord import utils
|
||||||
from discord.ext import tasks
|
from discord.ext import tasks
|
||||||
@ -95,3 +96,53 @@ def test_task_regression_issue7659():
|
|||||||
|
|
||||||
assert loop._get_next_sleep_time(before_midnight) == expected_before_midnight
|
assert loop._get_next_sleep_time(before_midnight) == expected_before_midnight
|
||||||
assert loop._get_next_sleep_time(after_midnight) == expected_after_midnight
|
assert loop._get_next_sleep_time(after_midnight) == expected_after_midnight
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif(sys.version_info < (3, 9), reason="zoneinfo requires 3.9")
|
||||||
|
def test_task_is_imaginary():
|
||||||
|
import zoneinfo
|
||||||
|
|
||||||
|
tz = zoneinfo.ZoneInfo('America/New_York')
|
||||||
|
|
||||||
|
# 2:30 AM was skipped
|
||||||
|
dt = datetime.datetime(2022, 3, 13, 2, 30, tzinfo=tz)
|
||||||
|
assert tasks.is_imaginary(dt)
|
||||||
|
|
||||||
|
now = utils.utcnow()
|
||||||
|
# UTC time is never imaginary or ambiguous
|
||||||
|
assert not tasks.is_imaginary(now)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif(sys.version_info < (3, 9), reason="zoneinfo requires 3.9")
|
||||||
|
def test_task_is_ambiguous():
|
||||||
|
import zoneinfo
|
||||||
|
|
||||||
|
tz = zoneinfo.ZoneInfo('America/New_York')
|
||||||
|
|
||||||
|
# 1:30 AM happened twice
|
||||||
|
dt = datetime.datetime(2022, 11, 6, 1, 30, tzinfo=tz)
|
||||||
|
assert tasks.is_ambiguous(dt)
|
||||||
|
|
||||||
|
now = utils.utcnow()
|
||||||
|
# UTC time is never imaginary or ambiguous
|
||||||
|
assert not tasks.is_imaginary(now)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif(sys.version_info < (3, 9), reason="zoneinfo requires 3.9")
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
('dt', 'key', 'expected'),
|
||||||
|
[
|
||||||
|
(datetime.datetime(2022, 11, 6, 1, 30), 'America/New_York', datetime.datetime(2022, 11, 6, 1, 30, fold=1)),
|
||||||
|
(datetime.datetime(2022, 3, 13, 2, 30), 'America/New_York', datetime.datetime(2022, 3, 13, 3, 30)),
|
||||||
|
(datetime.datetime(2022, 4, 8, 2, 30), 'America/New_York', datetime.datetime(2022, 4, 8, 2, 30)),
|
||||||
|
(datetime.datetime(2023, 1, 7, 12, 30), 'UTC', datetime.datetime(2023, 1, 7, 12, 30)),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_task_date_resolve(dt, key, expected):
|
||||||
|
import zoneinfo
|
||||||
|
|
||||||
|
tz = zoneinfo.ZoneInfo(key)
|
||||||
|
|
||||||
|
actual = tasks.resolve_datetime(dt.replace(tzinfo=tz))
|
||||||
|
expected = expected.replace(tzinfo=tz)
|
||||||
|
assert actual == expected
|
||||||
|
Loading…
x
Reference in New Issue
Block a user