Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions telegram/ext/jobqueue.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@

from telegram.ext.callbackcontext import CallbackContext
from telegram.utils.deprecate import TelegramDeprecationWarning
from telegram.utils.helpers import to_float_timestamp, _UTC
from telegram.utils.helpers import to_float_timestamp


class Days(object):
Expand Down Expand Up @@ -436,7 +436,7 @@ def __init__(self,

self._days = None
self.days = days
self.tzinfo = tzinfo or _UTC
self.tzinfo = tzinfo or datetime.timezone.utc

self._job_queue = weakref.proxy(job_queue) if job_queue is not None else None

Expand Down Expand Up @@ -519,7 +519,7 @@ def next_t(self):
def _set_next_t(self, next_t):
if isinstance(next_t, datetime.datetime):
# Set timezone to UTC in case datetime is in local timezone.
next_t = next_t.astimezone(_UTC)
next_t = next_t.astimezone(datetime.timezone.utc)
next_t = to_float_timestamp(next_t)
elif not (isinstance(next_t, Number) or next_t is None):
raise TypeError("The 'next_t' argument should be one of the following types: "
Expand Down
57 changes: 14 additions & 43 deletions telegram/utils/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,46 +75,12 @@ def escape_markdown(text, version=1, entity_type=None):
# -------- date/time related helpers --------
# TODO: add generic specification of UTC for naive datetimes to docs

if hasattr(dtm, 'timezone'):
# Python 3.3+
def _datetime_to_float_timestamp(dt_obj):
if dt_obj.tzinfo is None:
dt_obj = dt_obj.replace(tzinfo=_UTC)
return dt_obj.timestamp()

_UtcOffsetTimezone = dtm.timezone
_UTC = dtm.timezone.utc
else:
# Python < 3.3 (incl 2.7)

# hardcoded timezone class (`datetime.timezone` isn't available in py2)
class _UtcOffsetTimezone(dtm.tzinfo):
def __init__(self, offset):
self.offset = offset

def tzname(self, dt):
return 'UTC +{}'.format(self.offset)

def utcoffset(self, dt):
return self.offset

def dst(self, dt):
return dtm.timedelta(0)

_UTC = _UtcOffsetTimezone(dtm.timedelta(0))
__EPOCH_DT = dtm.datetime.fromtimestamp(0, tz=_UTC)
__NAIVE_EPOCH_DT = __EPOCH_DT.replace(tzinfo=None)

# _datetime_to_float_timestamp
# Not using future.backports.datetime here as datetime value might be an input from the user,
# making every isinstace() call more delicate. So we just use our own compat layer.
def _datetime_to_float_timestamp(dt_obj):
epoch_dt = __EPOCH_DT if dt_obj.tzinfo is not None else __NAIVE_EPOCH_DT
return (dt_obj - epoch_dt).total_seconds()

_datetime_to_float_timestamp.__doc__ = \
def _datetime_to_float_timestamp(dt_obj):
"""Converts a datetime object to a float timestamp (with sub-second precision).
If the datetime object is timezone-naive, it is assumed to be in UTC."""
If the datetime object is timezone-naive, it is assumed to be in UTC."""
if dt_obj.tzinfo is None:
dt_obj = dt_obj.replace(tzinfo=dtm.timezone.utc)
return dt_obj.timestamp()


def to_float_timestamp(t, reference_timestamp=None):
Expand Down Expand Up @@ -196,22 +162,27 @@ def to_timestamp(dt_obj, reference_timestamp=None):
return int(to_float_timestamp(dt_obj, reference_timestamp)) if dt_obj is not None else None


def from_timestamp(unixtime):
def from_timestamp(unixtime, tzinfo=dtm.timezone.utc):
"""
Converts an (integer) unix timestamp to a naive datetime object in UTC.
Converts an (integer) unix timestamp to a timezone aware datetime object.
``None`` s are left alone (i.e. ``from_timestamp(None)`` is ``None``).

Args:
unixtime (int): integer POSIX timestamp
tzinfo (:obj:`datetime.tzinfo`, optional): The timezone, the timestamp is to be converted
to. Defaults to UTC.

Returns:
equivalent :obj:`datetime.datetime` value in naive UTC if ``timestamp`` is not
timezone aware equivalent :obj:`datetime.datetime` value if ``timestamp`` is not
``None``; else ``None``
"""
if unixtime is None:
return None

return dtm.datetime.utcfromtimestamp(unixtime)
if tzinfo is not None:
return dtm.datetime.fromtimestamp(unixtime, tz=tzinfo)
else:
return dtm.datetime.utcfromtimestamp(unixtime)

# -------- end --------

Expand Down
3 changes: 1 addition & 2 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@
InlineQuery, CallbackQuery, ShippingQuery, PreCheckoutQuery,
ChosenInlineResult)
from telegram.ext import Dispatcher, JobQueue, Updater, BaseFilter, Defaults
from telegram.utils.helpers import _UtcOffsetTimezone
from telegram.error import BadRequest
from tests.bots import get_bot

Expand Down Expand Up @@ -281,7 +280,7 @@ def utc_offset(request):

@pytest.fixture()
def timezone(utc_offset):
return _UtcOffsetTimezone(utc_offset)
return datetime.timezone(utc_offset)


def expect_bad_request(func, message, reason):
Expand Down
18 changes: 13 additions & 5 deletions tests/test_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,14 @@
from telegram import MessageEntity
from telegram.message import Message
from telegram.utils import helpers
from telegram.utils.helpers import _UtcOffsetTimezone, _datetime_to_float_timestamp
from telegram.utils.helpers import _datetime_to_float_timestamp


# sample time specification values categorised into absolute / delta / time-of-day
ABSOLUTE_TIME_SPECS = [dtm.datetime.now(tz=_UtcOffsetTimezone(dtm.timedelta(hours=-7))),
ABSOLUTE_TIME_SPECS = [dtm.datetime.now(tz=dtm.timezone(dtm.timedelta(hours=-7))),
dtm.datetime.utcnow()]
DELTA_TIME_SPECS = [dtm.timedelta(hours=3, seconds=42, milliseconds=2), 30, 7.5]
TIME_OF_DAY_TIME_SPECS = [dtm.time(12, 42, tzinfo=_UtcOffsetTimezone(dtm.timedelta(hours=-7))),
TIME_OF_DAY_TIME_SPECS = [dtm.time(12, 42, tzinfo=dtm.timezone(dtm.timedelta(hours=-7))),
dtm.time(12, 42)]
RELATIVE_TIME_SPECS = DELTA_TIME_SPECS + TIME_OF_DAY_TIME_SPECS
TIME_SPECS = ABSOLUTE_TIME_SPECS + RELATIVE_TIME_SPECS
Expand Down Expand Up @@ -142,8 +142,16 @@ def test_to_timestamp_none(self):
# this 'convenience' behaviour has been left left for backwards compatibility
assert helpers.to_timestamp(None) is None

def test_from_timestamp(self):
assert helpers.from_timestamp(1573431976) == dtm.datetime(2019, 11, 11, 0, 26, 16)
def test_from_timestamp_naive(self):
datetime = dtm.datetime(2019, 11, 11, 0, 26, 16, tzinfo=None)
assert helpers.from_timestamp(1573431976, tzinfo=None) == datetime

def test_from_timestamp_aware(self, timezone):
# we're parametrizing this with two different UTC offsets to exclude the possibility
# of an xpass when the test is run in a timezone with the same UTC offset
datetime = dtm.datetime(2019, 11, 11, 0, 26, 16, 10**5, tzinfo=timezone)
assert (helpers.from_timestamp(1573431976.1 - timezone.utcoffset(None).total_seconds())
== datetime)

def test_create_deep_linked_url(self):
username = 'JamesTheMock'
Expand Down
29 changes: 14 additions & 15 deletions tests/test_jobqueue.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@

from telegram.ext import JobQueue, Updater, Job, CallbackContext
from telegram.utils.deprecate import TelegramDeprecationWarning
from telegram.utils.helpers import _UtcOffsetTimezone, _UTC


@pytest.fixture(scope='function')
Expand Down Expand Up @@ -277,7 +276,7 @@ def test_run_daily_with_timezone(self, job_queue):
# must subtract one minute because the UTC offset has to be strictly less than 24h
# thus this test will xpass if run in the interval [00:00, 00:01) UTC time
# (because target time will be 23:59 UTC, so local and target weekday will be the same)
target_tzinfo = _UtcOffsetTimezone(dtm.timedelta(days=1, minutes=-1))
target_tzinfo = dtm.timezone(dtm.timedelta(days=1, minutes=-1))
target_datetime = (utcnow + dtm.timedelta(days=1, minutes=-1, seconds=delta)).replace(
tzinfo=target_tzinfo)
target_time = target_datetime.timetz()
Expand Down Expand Up @@ -344,7 +343,7 @@ def test_job_default_tzinfo(self, job_queue):
jobs = [job_1, job_2, job_3]

for job in jobs:
assert job.tzinfo == _UTC
assert job.tzinfo == dtm.timezone.utc

def test_job_next_t_property(self, job_queue):
# Testing:
Expand Down Expand Up @@ -379,31 +378,31 @@ def test_job_set_next_t(self, job_queue):

job = job_queue.run_once(self.job_run_once, 0.05)

t = dtm.datetime.now(tz=_UtcOffsetTimezone(dtm.timedelta(hours=12)))
t = dtm.datetime.now(tz=dtm.timezone(dtm.timedelta(hours=12)))
job._set_next_t(t)
job.tzinfo = _UtcOffsetTimezone(dtm.timedelta(hours=5))
job.tzinfo = dtm.timezone(dtm.timedelta(hours=5))
assert job.next_t == t.astimezone(job.tzinfo)

def test_passing_tzinfo_to_job(self, job_queue):
"""Test that tzinfo is correctly passed to job with run_once, run_daily
and run_repeating methods"""

when_dt_tz_specific = dtm.datetime.now(
tz=_UtcOffsetTimezone(dtm.timedelta(hours=12))
tz=dtm.timezone(dtm.timedelta(hours=12))
) + dtm.timedelta(seconds=2)
when_dt_tz_utc = dtm.datetime.now() + dtm.timedelta(seconds=2)
job_once1 = job_queue.run_once(self.job_run_once, when_dt_tz_specific)
job_once2 = job_queue.run_once(self.job_run_once, when_dt_tz_utc)

when_time_tz_specific = (dtm.datetime.now(
tz=_UtcOffsetTimezone(dtm.timedelta(hours=12))
tz=dtm.timezone(dtm.timedelta(hours=12))
) + dtm.timedelta(seconds=2)).timetz()
when_time_tz_utc = (dtm.datetime.now() + dtm.timedelta(seconds=2)).timetz()
job_once3 = job_queue.run_once(self.job_run_once, when_time_tz_specific)
job_once4 = job_queue.run_once(self.job_run_once, when_time_tz_utc)

first_dt_tz_specific = dtm.datetime.now(
tz=_UtcOffsetTimezone(dtm.timedelta(hours=12))
tz=dtm.timezone(dtm.timedelta(hours=12))
) + dtm.timedelta(seconds=2)
first_dt_tz_utc = dtm.datetime.now() + dtm.timedelta(seconds=2)
job_repeating1 = job_queue.run_repeating(
Expand All @@ -412,7 +411,7 @@ def test_passing_tzinfo_to_job(self, job_queue):
self.job_run_once, 2, first=first_dt_tz_utc)

first_time_tz_specific = (dtm.datetime.now(
tz=_UtcOffsetTimezone(dtm.timedelta(hours=12))
tz=dtm.timezone(dtm.timedelta(hours=12))
) + dtm.timedelta(seconds=2)).timetz()
first_time_tz_utc = (dtm.datetime.now() + dtm.timedelta(seconds=2)).timetz()
job_repeating3 = job_queue.run_repeating(
Expand All @@ -421,19 +420,19 @@ def test_passing_tzinfo_to_job(self, job_queue):
self.job_run_once, 2, first=first_time_tz_utc)

time_tz_specific = (dtm.datetime.now(
tz=_UtcOffsetTimezone(dtm.timedelta(hours=12))
tz=dtm.timezone(dtm.timedelta(hours=12))
) + dtm.timedelta(seconds=2)).timetz()
time_tz_utc = (dtm.datetime.now() + dtm.timedelta(seconds=2)).timetz()
job_daily1 = job_queue.run_daily(self.job_run_once, time_tz_specific)
job_daily2 = job_queue.run_daily(self.job_run_once, time_tz_utc)

assert job_once1.tzinfo == when_dt_tz_specific.tzinfo
assert job_once2.tzinfo == _UTC
assert job_once2.tzinfo == dtm.timezone.utc
assert job_once3.tzinfo == when_time_tz_specific.tzinfo
assert job_once4.tzinfo == _UTC
assert job_once4.tzinfo == dtm.timezone.utc
assert job_repeating1.tzinfo == first_dt_tz_specific.tzinfo
assert job_repeating2.tzinfo == _UTC
assert job_repeating2.tzinfo == dtm.timezone.utc
assert job_repeating3.tzinfo == first_time_tz_specific.tzinfo
assert job_repeating4.tzinfo == _UTC
assert job_repeating4.tzinfo == dtm.timezone.utc
assert job_daily1.tzinfo == time_tz_specific.tzinfo
assert job_daily2.tzinfo == _UTC
assert job_daily2.tzinfo == dtm.timezone.utc