From 103b115486fd075f3f7eac9780d1e5dc538f4176 Mon Sep 17 00:00:00 2001 From: Bibo-Joshi Date: Sun, 27 Sep 2020 12:59:48 +0200 Subject: [PATCH] Defaults.tzinfo (#2042) --- telegram/bot.py | 15 ++++++++-- telegram/ext/defaults.py | 23 ++++++++++++-- telegram/ext/jobqueue.py | 33 ++++++++++---------- telegram/utils/helpers.py | 43 ++++++++++++++++---------- tests/conftest.py | 12 ++++++++ tests/test_bot.py | 63 ++++++++++++++++++++++++++++++++++++++- tests/test_defaults.py | 2 ++ tests/test_filters.py | 4 +-- tests/test_helpers.py | 8 +++++ tests/test_jobqueue.py | 36 +++++++++++++++------- tests/test_message.py | 4 +-- tests/test_poll.py | 6 ++-- 12 files changed, 194 insertions(+), 55 deletions(-) diff --git a/telegram/bot.py b/telegram/bot.py index c9069716c..54a251458 100644 --- a/telegram/bot.py +++ b/telegram/bot.py @@ -1766,6 +1766,8 @@ class Bot(TelegramObject): until_date (:obj:`int` | :obj:`datetime.datetime`, optional): Date when the user will be unbanned, unix time. If user is banned for more than 366 days or less than 30 seconds from the current time they are considered to be banned forever. + For timezone naive :obj:`datetime.datetime` objects, the default timezone of the + bot will be used. api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the Telegram API. @@ -1780,7 +1782,8 @@ class Bot(TelegramObject): if until_date is not None: if isinstance(until_date, datetime): - until_date = to_timestamp(until_date) + until_date = to_timestamp(until_date, + tzinfo=self.defaults.tzinfo if self.defaults else None) data['until_date'] = until_date result = self._post('kickChatMember', data, timeout=timeout, api_kwargs=api_kwargs) @@ -2866,6 +2869,8 @@ class Bot(TelegramObject): will be lifted for the user, unix time. If user is restricted for more than 366 days or less than 30 seconds from the current time, they are considered to be restricted forever. + For timezone naive :obj:`datetime.datetime` objects, the default timezone of the + bot will be used. permissions (:class:`telegram.ChatPermissions`): A JSON-serialized object for new user permissions. timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as @@ -2884,7 +2889,8 @@ class Bot(TelegramObject): if until_date is not None: if isinstance(until_date, datetime): - until_date = to_timestamp(until_date) + until_date = to_timestamp(until_date, + tzinfo=self.defaults.tzinfo if self.defaults else None) data['until_date'] = until_date result = self._post('restrictChatMember', data, timeout=timeout, api_kwargs=api_kwargs) @@ -3630,6 +3636,8 @@ class Bot(TelegramObject): timestamp) when the poll will be automatically closed. Must be at least 5 and no more than 600 seconds in the future. Can't be used together with :attr:`open_period`. + For timezone naive :obj:`datetime.datetime` objects, the default timezone of the + bot will be used. is_closed (:obj:`bool`, optional): Pass :obj:`True`, if the poll needs to be immediately closed. This can be useful for poll preview. disable_notification (:obj:`bool`, optional): Sends the message silently. Users will @@ -3682,7 +3690,8 @@ class Bot(TelegramObject): data['open_period'] = open_period if close_date: if isinstance(close_date, datetime): - close_date = to_timestamp(close_date) + close_date = to_timestamp(close_date, + tzinfo=self.defaults.tzinfo if self.defaults else None) data['close_date'] = close_date return self._message('sendPoll', data, timeout=timeout, diff --git a/telegram/ext/defaults.py b/telegram/ext/defaults.py index 06d32dfed..0bdcac18a 100644 --- a/telegram/ext/defaults.py +++ b/telegram/ext/defaults.py @@ -17,6 +17,7 @@ # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains the class Defaults, which allows to pass default values to Updater.""" +import pytz from telegram.utils.helpers import DEFAULT_NONE @@ -37,6 +38,8 @@ class Defaults: quote (:obj:`bool`): Optional. If set to :obj:`True`, the reply is sent as an actual reply to the message. If ``reply_to_message_id`` is passed in ``kwargs``, this parameter will be ignored. Default: :obj:`True` in group chats and :obj:`False` in private chats. + tzinfo (:obj:`tzinfo`): A timezone to be used for all date(time) objects appearing + throughout PTB. Parameters: parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show @@ -51,6 +54,10 @@ class Defaults: quote (:obj:`bool`, optional): If set to :obj:`True`, the reply is sent as an actual reply to the message. If ``reply_to_message_id`` is passed in ``kwargs``, this parameter will be ignored. Default: :obj:`True` in group chats and :obj:`False` in private chats. + tzinfo (:obj:`tzinfo`, optional): A timezone to be used for all date(time) inputs + appearing throughout PTB, i.e. if a timezone naive date(time) object is passed + somewhere, it will be assumed to be in ``tzinfo``. Must be a timezone provided by the + ``pytz`` module. Defaults to UTC. """ def __init__(self, parse_mode=None, @@ -59,12 +66,14 @@ class Defaults: # Timeout needs special treatment, since the bot methods have two different # default values for timeout (None and 20s) timeout=DEFAULT_NONE, - quote=None): + quote=None, + tzinfo=pytz.utc): self._parse_mode = parse_mode self._disable_notification = disable_notification self._disable_web_page_preview = disable_web_page_preview self._timeout = timeout self._quote = quote + self._tzinfo = tzinfo @property def parse_mode(self): @@ -111,12 +120,22 @@ class Defaults: raise AttributeError("You can not assign a new value to defaults after because it would " "not have any effect.") + @property + def tzinfo(self): + return self._tzinfo + + @tzinfo.setter + def tzinfo(self, value): + raise AttributeError("You can not assign a new value to defaults after because it would " + "not have any effect.") + def __hash__(self): return hash((self._parse_mode, self._disable_notification, self._disable_web_page_preview, self._timeout, - self._quote)) + self._quote, + self._tzinfo)) def __eq__(self, other): if isinstance(other, Defaults): diff --git a/telegram/ext/jobqueue.py b/telegram/ext/jobqueue.py index 4093b557e..d8332bb3e 100644 --- a/telegram/ext/jobqueue.py +++ b/telegram/ext/jobqueue.py @@ -89,8 +89,9 @@ class JobQueue: return self._tz_now() + time if isinstance(time, datetime.time): dt = datetime.datetime.combine( - datetime.datetime.now(tz=time.tzinfo or self.scheduler.timezone).date(), time, - tzinfo=time.tzinfo or self.scheduler.timezone) + datetime.datetime.now(tz=time.tzinfo or self.scheduler.timezone).date(), time) + if dt.tzinfo is None: + dt = self.scheduler.timezone.localize(dt) if shift_day and dt <= datetime.datetime.now(pytz.utc): dt += datetime.timedelta(days=1) return dt @@ -106,6 +107,9 @@ class JobQueue: """ self._dispatcher = dispatcher + if dispatcher.bot.defaults: + if dispatcher.bot.defaults: + self.scheduler.configure(timezone=dispatcher.bot.defaults.tzinfo or pytz.utc) def run_once(self, callback, when, context=None, name=None, job_kwargs=None): """Creates a new ``Job`` that runs once and adds it to the queue. @@ -129,13 +133,11 @@ class JobQueue: job should run. * :obj:`datetime.datetime` will be interpreted as a specific date and time at which the job should run. If the timezone (``datetime.tzinfo``) is :obj:`None`, - UTC will be assumed. + the default timezone of the bot will be used. * :obj:`datetime.time` will be interpreted as a specific time of day at which the job should run. This could be either today or, if the time has already passed, - tomorrow. If the timezone (``time.tzinfo``) is :obj:`None`, UTC will be assumed. - - If ``when`` is :obj:`datetime.datetime` or :obj:`datetime.time` type - then ``when.tzinfo`` will define ``Job.tzinfo``. Otherwise UTC will be assumed. + tomorrow. If the timezone (``time.tzinfo``) is :obj:`None`, the + default timezone of the bot will be used. context (:obj:`object`, optional): Additional data needed for the callback function. Can be accessed through ``job.context`` in the callback. Defaults to :obj:`None`. @@ -193,13 +195,11 @@ class JobQueue: job should run. * :obj:`datetime.datetime` will be interpreted as a specific date and time at which the job should run. If the timezone (``datetime.tzinfo``) is :obj:`None`, - UTC will be assumed. + the default timezone of the bot will be used. * :obj:`datetime.time` will be interpreted as a specific time of day at which the job should run. This could be either today or, if the time has already passed, - tomorrow. If the timezone (``time.tzinfo``) is :obj:`None`, UTC will be assumed. - - If ``first`` is :obj:`datetime.datetime` or :obj:`datetime.time` type - then ``first.tzinfo`` will define ``Job.tzinfo``. Otherwise UTC will be assumed. + tomorrow. If the timezone (``time.tzinfo``) is :obj:`None`, the + default timezone of the bot will be used. Defaults to ``interval`` last (:obj:`int` | :obj:`float` | :obj:`datetime.timedelta` | \ @@ -208,7 +208,8 @@ class JobQueue: depending on its type. See ``first`` for details. If ``last`` is :obj:`datetime.datetime` or :obj:`datetime.time` type - and ``last.tzinfo`` is :obj:`None`, UTC will be assumed. + and ``last.tzinfo`` is :obj:`None`, the default timezone of the bot will be + assumed. Defaults to :obj:`None`. context (:obj:`object`, optional): Additional data needed for the callback function. @@ -268,8 +269,7 @@ class JobQueue: ``context.job`` is the :class:`telegram.ext.Job` instance. It can be used to access its ``job.context`` or change it to a repeating job. when (:obj:`datetime.time`): Time of day at which the job should run. If the timezone - (``when.tzinfo``) is :obj:`None`, UTC will be assumed. This will also implicitly - define ``Job.tzinfo``. + (``when.tzinfo``) is :obj:`None`, the default timezone of the bot will be used. day (:obj:`int`): Defines the day of the month whereby the job would run. It should be within the range of 1 and 31, inclusive. context (:obj:`object`, optional): Additional data needed for the callback function. @@ -338,8 +338,7 @@ class JobQueue: ``context.job`` is the :class:`telegram.ext.Job` instance. It can be used to access its ``job.context`` or change it to a repeating job. time (:obj:`datetime.time`): Time of day at which the job should run. If the timezone - (``time.tzinfo``) is :obj:`None`, UTC will be assumed. - ``time.tzinfo`` will implicitly define ``Job.tzinfo``. + (``time.tzinfo``) is :obj:`None`, the default timezone of the bot will be used. days (Tuple[:obj:`int`], optional): Defines on which days of the week the job should run. Defaults to ``EVERY_DAY`` context (:obj:`object`, optional): Additional data needed for the callback function. diff --git a/telegram/utils/helpers.py b/telegram/utils/helpers.py index 21f750b64..19287b0f7 100644 --- a/telegram/utils/helpers.py +++ b/telegram/utils/helpers.py @@ -26,6 +26,8 @@ from collections import defaultdict from html import escape from numbers import Number +import pytz + try: import ujson as json except ImportError: @@ -72,8 +74,6 @@ def escape_markdown(text, version=1, entity_type=None): # -------- date/time related helpers -------- -# TODO: add generic specification of UTC for naive datetimes to docs - def _datetime_to_float_timestamp(dt_obj): """ Converts a datetime object to a float timestamp (with sub-second precision). @@ -85,13 +85,13 @@ def _datetime_to_float_timestamp(dt_obj): return dt_obj.timestamp() -def to_float_timestamp(t, reference_timestamp=None): +def to_float_timestamp(t, reference_timestamp=None, tzinfo=None): """ Converts a given time object to a float POSIX timestamp. Used to convert different time specifications to a common format. The time object can be relative (i.e. indicate a time increment, or a time of day) or absolute. Any objects from the :class:`datetime` module that are timezone-naive will be assumed - to be in UTC. + to be in UTC, if ``bot`` is not passed or ``bot.defaults`` is :obj:`None`. :obj:`None` s are left alone (i.e. ``to_float_timestamp(None)`` is :obj:`None`). @@ -113,6 +113,9 @@ def to_float_timestamp(t, reference_timestamp=None): If ``t`` is given as an absolute representation of date & time (i.e. a ``datetime.datetime`` object), ``reference_timestamp`` is not relevant and so its value should be :obj:`None`. If this is not the case, a ``ValueError`` will be raised. + tzinfo (:obj:`datetime.tzinfo`, optional): If ``t`` is a naive object from the + :class:`datetime` module, it will be interpreted as this timezone. Defaults to + ``pytz.utc``. Returns: (float | None) The return value depends on the type of argument ``t``. If ``t`` is @@ -138,33 +141,43 @@ def to_float_timestamp(t, reference_timestamp=None): return reference_timestamp + t.total_seconds() elif isinstance(t, Number): return reference_timestamp + t - elif isinstance(t, dtm.time): - if t.tzinfo is not None: - reference_dt = dtm.datetime.fromtimestamp(reference_timestamp, tz=t.tzinfo) - else: - reference_dt = dtm.datetime.utcfromtimestamp(reference_timestamp) # assume UTC + + if tzinfo is None: + tzinfo = pytz.utc + + if isinstance(t, dtm.time): + reference_dt = dtm.datetime.fromtimestamp(reference_timestamp, tz=t.tzinfo or tzinfo) reference_date = reference_dt.date() reference_time = reference_dt.timetz() - if reference_time > t: # if the time of day has passed today, use tomorrow - reference_date += dtm.timedelta(days=1) - return _datetime_to_float_timestamp(dtm.datetime.combine(reference_date, t)) + + aware_datetime = dtm.datetime.combine(reference_date, t) + if aware_datetime.tzinfo is None: + aware_datetime = tzinfo.localize(aware_datetime) + + # if the time of day has passed today, use tomorrow + if reference_time > aware_datetime.timetz(): + aware_datetime += dtm.timedelta(days=1) + return _datetime_to_float_timestamp(aware_datetime) elif isinstance(t, dtm.datetime): + if t.tzinfo is None: + t = tzinfo.localize(t) return _datetime_to_float_timestamp(t) raise TypeError('Unable to convert {} object to timestamp'.format(type(t).__name__)) -def to_timestamp(dt_obj, reference_timestamp=None): +def to_timestamp(dt_obj, reference_timestamp=None, tzinfo=pytz.utc): """ Wrapper over :func:`to_float_timestamp` which returns an integer (the float value truncated down to the nearest integer). See the documentation for :func:`to_float_timestamp` for more details. """ - return int(to_float_timestamp(dt_obj, reference_timestamp)) if dt_obj is not None else None + return (int(to_float_timestamp(dt_obj, reference_timestamp, tzinfo)) + if dt_obj is not None else None) -def from_timestamp(unixtime, tzinfo=dtm.timezone.utc): +def from_timestamp(unixtime, tzinfo=pytz.utc): """ Converts an (integer) unix timestamp to a timezone aware datetime object. :obj:`None`s are left alone (i.e. ``from_timestamp(None)`` is :obj:`None`). diff --git a/tests/conftest.py b/tests/conftest.py index 97b470258..f6d5c1569 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -69,6 +69,18 @@ def default_bot(request, bot_info): return default_bot +@pytest.fixture(scope='function') +def tz_bot(timezone, bot_info): + defaults = Defaults(tzinfo=timezone) + default_bot = DEFAULT_BOTS.get(defaults) + if default_bot: + return default_bot + else: + default_bot = make_bot(bot_info, **{'defaults': defaults}) + DEFAULT_BOTS[defaults] = default_bot + return default_bot + + @pytest.fixture(scope='session') def chat_id(bot_info): return bot_info['chat_id'] diff --git a/tests/test_bot.py b/tests/test_bot.py index 1beeb79d3..aa78ead13 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -29,7 +29,7 @@ from telegram import (Bot, Update, ChatAction, TelegramError, User, InlineKeyboa InlineQueryResultDocument, Dice, MessageEntity, ParseMode) from telegram.constants import MAX_INLINE_QUERY_RESULTS from telegram.error import BadRequest, InvalidToken, NetworkError, RetryAfter -from telegram.utils.helpers import from_timestamp, escape_markdown +from telegram.utils.helpers import from_timestamp, escape_markdown, to_timestamp from tests.conftest import expect_bad_request BASE_TIME = time.time() @@ -272,6 +272,29 @@ class TestBot: assert new_message.poll.id == message.poll.id assert new_message.poll.is_closed + @flaky(5, 1) + @pytest.mark.timeout(10) + def test_send_close_date_default_tz(self, tz_bot, super_group_id): + question = 'Is this a test?' + answers = ['Yes', 'No', 'Maybe'] + reply_markup = InlineKeyboardMarkup.from_button( + InlineKeyboardButton(text='text', callback_data='data')) + + aware_close_date = dtm.datetime.now(tz=tz_bot.defaults.tzinfo) + dtm.timedelta(seconds=5) + close_date = aware_close_date.replace(tzinfo=None) + + message = tz_bot.send_poll(chat_id=super_group_id, question=question, options=answers, + close_date=close_date, timeout=60) + assert message.poll.close_date == aware_close_date.replace(microsecond=0) + + time.sleep(5.1) + + new_message = tz_bot.edit_message_reply_markup(chat_id=super_group_id, + message_id=message.message_id, + reply_markup=reply_markup, timeout=60) + assert new_message.poll.id == message.poll.id + assert new_message.poll.is_closed + @flaky(3, 1) @pytest.mark.timeout(10) @pytest.mark.parametrize('default_bot', [{'parse_mode': 'Markdown'}], indirect=True) @@ -518,6 +541,22 @@ class TestBot: assert bot.kick_chat_member(2, 32, until_date=until) assert bot.kick_chat_member(2, 32, until_date=1577887200) + def test_kick_chat_member_default_tz(self, monkeypatch, tz_bot): + until = dtm.datetime(2020, 1, 11, 16, 13) + until_timestamp = to_timestamp(until, tzinfo=tz_bot.defaults.tzinfo) + + def test(url, data, *args, **kwargs): + chat_id = data['chat_id'] == 2 + user_id = data['user_id'] == 32 + until_date = data.get('until_date', until_timestamp) == until_timestamp + return chat_id and user_id and until_date + + monkeypatch.setattr(tz_bot.request, 'post', test) + + assert tz_bot.kick_chat_member(2, 32) + assert tz_bot.kick_chat_member(2, 32, until_date=until) + assert tz_bot.kick_chat_member(2, 32, until_date=until_timestamp) + # TODO: Needs improvement. def test_unban_chat_member(self, monkeypatch, bot): def test(url, data, *args, **kwargs): @@ -951,6 +990,28 @@ class TestBot: chat_permissions, until_date=dtm.datetime.utcnow()) + def test_restrict_chat_member_default_tz(self, monkeypatch, tz_bot, channel_id, + chat_permissions): + until = dtm.datetime(2020, 1, 11, 16, 13) + until_timestamp = to_timestamp(until, tzinfo=tz_bot.defaults.tzinfo) + + def test(url, data, *args, **kwargs): + return data.get('until_date', until_timestamp) == until_timestamp + + monkeypatch.setattr(tz_bot.request, 'post', test) + + assert tz_bot.restrict_chat_member(channel_id, + 95205500, + chat_permissions) + assert tz_bot.restrict_chat_member(channel_id, + 95205500, + chat_permissions, + until_date=until) + assert tz_bot.restrict_chat_member(channel_id, + 95205500, + chat_permissions, + until_date=until_timestamp) + @flaky(3, 1) @pytest.mark.timeout(10) def test_promote_chat_member(self, bot, channel_id): diff --git a/tests/test_defaults.py b/tests/test_defaults.py index d67cfc6b8..5344f538d 100644 --- a/tests/test_defaults.py +++ b/tests/test_defaults.py @@ -37,6 +37,8 @@ class TestDefault: defaults.timeout = True with pytest.raises(AttributeError): defaults.quote = True + with pytest.raises(AttributeError): + defaults.tzinfo = True def test_equality(self): a = Defaults(parse_mode='HTML', quote=True) diff --git a/tests/test_filters.py b/tests/test_filters.py index fad30709d..d45a2441b 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -190,7 +190,7 @@ class TestFilters: matches = result['matches'] assert isinstance(matches, list) assert all([type(res) == SRE_TYPE for res in matches]) - update.message.forward_date = False + update.message.forward_date = None result = filter(update) assert not result update.message.text = 'test it out' @@ -926,7 +926,7 @@ class TestFilters: update.message.text = 'test' update.message.forward_date = datetime.datetime.utcnow() assert (Filters.text & (Filters.status_update | Filters.forwarded))(update) - update.message.forward_date = False + update.message.forward_date = None assert not (Filters.text & (Filters.forwarded | Filters.status_update))(update) update.message.pinned_message = True assert (Filters.text & (Filters.forwarded | Filters.status_update)(update)) diff --git a/tests/test_helpers.py b/tests/test_helpers.py index 7aa62f9b3..e1ff5fc3b 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -25,6 +25,7 @@ from telegram import Sticker from telegram import Update from telegram import User from telegram import MessageEntity +from telegram.ext import Defaults from telegram.message import Message from telegram.utils import helpers from telegram.utils.helpers import _datetime_to_float_timestamp @@ -135,6 +136,10 @@ class TestHelpers: assert (helpers.to_float_timestamp(time_spec) == pytest.approx(helpers.to_float_timestamp(time_spec, reference_timestamp=now))) + def test_to_float_timestamp_error(self): + with pytest.raises(TypeError, match='Defaults'): + helpers.to_float_timestamp(Defaults()) + @pytest.mark.parametrize('time_spec', TIME_SPECS, ids=str) def test_to_timestamp(self, time_spec): # delegate tests to `to_float_timestamp` @@ -144,6 +149,9 @@ class TestHelpers: # this 'convenience' behaviour has been left left for backwards compatibility assert helpers.to_timestamp(None) is None + def test_from_timestamp_none(self): + assert helpers.from_timestamp(None) is None + 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 diff --git a/tests/test_jobqueue.py b/tests/test_jobqueue.py index f1e307e52..5919f8544 100644 --- a/tests/test_jobqueue.py +++ b/tests/test_jobqueue.py @@ -121,7 +121,7 @@ class TestJobQueue: sleep(0.07) assert self.result == 1 - def test_run_repeating_first_timezone(self, job_queue, timezone): + def test_run_repeating_last_timezone(self, job_queue, timezone): """Test correct scheduling of job when passing a timezone-aware datetime as ``first``""" job_queue.run_repeating(self.job_run_once, 0.1, first=dtm.datetime.now(timezone) + dtm.timedelta(seconds=0.05)) @@ -135,15 +135,6 @@ class TestJobQueue: sleep(0.1) assert self.result == 1 - def test_run_repeating_last_timezone(self, job_queue, timezone): - """Test correct scheduling of job when passing a timezone-aware datetime as ``first``""" - job_queue.run_repeating(self.job_run_once, 0.05, - last=dtm.datetime.now(timezone) + dtm.timedelta(seconds=0.06)) - sleep(0.1) - assert self.result == 1 - sleep(0.1) - assert self.result == 1 - def test_run_repeating_last_before_first(self, job_queue): with pytest.raises(ValueError, match="'last' must not be before 'first'!"): job_queue.run_repeating(self.job_run_once, 0.05, first=1, last=0.5) @@ -300,7 +291,11 @@ class TestJobQueue: time_of_day = expected_reschedule_time.time().replace(tzinfo=timezone) day = now.day - expected_reschedule_time += dtm.timedelta(calendar.monthrange(now.year, now.month)[1]) + expected_reschedule_time = timezone.normalize( + expected_reschedule_time + dtm.timedelta(calendar.monthrange(now.year, now.month)[1])) + # Adjust the hour for the special case that between now and next month a DST switch happens + expected_reschedule_time += dtm.timedelta( + hours=time_of_day.hour - expected_reschedule_time.hour) expected_reschedule_time = expected_reschedule_time.timestamp() job_queue.run_monthly(self.job_run_once, time_of_day, day) @@ -326,6 +321,25 @@ class TestJobQueue: scheduled_time = job_queue.jobs()[0].next_t.timestamp() assert scheduled_time == pytest.approx(expected_reschedule_time) + def test_default_tzinfo(self, _dp, tz_bot): + # 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 + jq = JobQueue() + original_bot = _dp.bot + _dp.bot = tz_bot + jq.set_dispatcher(_dp) + try: + jq.start() + + when = dtm.datetime.now(tz_bot.defaults.tzinfo) + dtm.timedelta(seconds=0.0005) + jq.run_once(self.job_run_once, when.time()) + sleep(0.001) + assert self.result == 1 + + jq.stop() + finally: + _dp.bot = original_bot + @pytest.mark.parametrize('use_context', [True, False]) def test_get_jobs(self, job_queue, use_context): job_queue._dispatcher.use_context = use_context diff --git a/tests/test_message.py b/tests/test_message.py index 46563a517..d8a994388 100644 --- a/tests/test_message.py +++ b/tests/test_message.py @@ -169,13 +169,13 @@ class TestMessage: MessageEntity(**e) for e in test_entities_v2 ]) - def test_all_posibilities_de_json_and_to_dict(self, bot, message_params): + def test_all_possibilities_de_json_and_to_dict(self, bot, message_params): new = Message.de_json(message_params.to_dict(), bot) assert new.to_dict() == message_params.to_dict() def test_dict_approach(self, message): - assert message['date'] == message.date + assert message['text'] == message.text assert message['chat_id'] == message.chat_id assert message['no_key'] is None diff --git a/tests/test_poll.py b/tests/test_poll.py index 0dbcd182e..7327cee11 100644 --- a/tests/test_poll.py +++ b/tests/test_poll.py @@ -20,6 +20,8 @@ import pytest from datetime import datetime + + from telegram import Poll, PollOption, PollAnswer, User, MessageEntity from telegram.utils.helpers import to_timestamp @@ -154,7 +156,7 @@ class TestPoll: open_period = 42 close_date = datetime.utcnow() - def test_de_json(self): + def test_de_json(self, bot): json_dict = { 'id': self.id_, 'question': self.question, @@ -169,7 +171,7 @@ class TestPoll: 'open_period': self.open_period, 'close_date': to_timestamp(self.close_date) } - poll = Poll.de_json(json_dict, None) + poll = Poll.de_json(json_dict, bot) assert poll.id == self.id_ assert poll.question == self.question