Defaults.tzinfo (#2042)

This commit is contained in:
Bibo-Joshi 2020-09-27 12:59:48 +02:00
parent b07e42ef33
commit 103b115486
12 changed files with 194 additions and 55 deletions

View file

@ -1766,6 +1766,8 @@ class Bot(TelegramObject):
until_date (:obj:`int` | :obj:`datetime.datetime`, optional): Date when the user will 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 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. 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 api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the
Telegram API. Telegram API.
@ -1780,7 +1782,8 @@ class Bot(TelegramObject):
if until_date is not None: if until_date is not None:
if isinstance(until_date, datetime): 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 data['until_date'] = until_date
result = self._post('kickChatMember', data, timeout=timeout, api_kwargs=api_kwargs) 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 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 days or less than 30 seconds from the current time, they are considered to be
restricted forever. 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 (:class:`telegram.ChatPermissions`): A JSON-serialized object for new user
permissions. permissions.
timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as 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 until_date is not None:
if isinstance(until_date, datetime): 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 data['until_date'] = until_date
result = self._post('restrictChatMember', data, timeout=timeout, api_kwargs=api_kwargs) 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 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 more than 600 seconds in the future. Can't be used together with
:attr:`open_period`. :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 is_closed (:obj:`bool`, optional): Pass :obj:`True`, if the poll needs to be
immediately closed. This can be useful for poll preview. immediately closed. This can be useful for poll preview.
disable_notification (:obj:`bool`, optional): Sends the message silently. Users will disable_notification (:obj:`bool`, optional): Sends the message silently. Users will
@ -3682,7 +3690,8 @@ class Bot(TelegramObject):
data['open_period'] = open_period data['open_period'] = open_period
if close_date: if close_date:
if isinstance(close_date, datetime): 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 data['close_date'] = close_date
return self._message('sendPoll', data, timeout=timeout, return self._message('sendPoll', data, timeout=timeout,

View file

@ -17,6 +17,7 @@
# You should have received a copy of the GNU Lesser Public License # You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/]. # 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.""" """This module contains the class Defaults, which allows to pass default values to Updater."""
import pytz
from telegram.utils.helpers import DEFAULT_NONE 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 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 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. 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: Parameters:
parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show 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 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 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. 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, def __init__(self,
parse_mode=None, parse_mode=None,
@ -59,12 +66,14 @@ class Defaults:
# Timeout needs special treatment, since the bot methods have two different # Timeout needs special treatment, since the bot methods have two different
# default values for timeout (None and 20s) # default values for timeout (None and 20s)
timeout=DEFAULT_NONE, timeout=DEFAULT_NONE,
quote=None): quote=None,
tzinfo=pytz.utc):
self._parse_mode = parse_mode self._parse_mode = parse_mode
self._disable_notification = disable_notification self._disable_notification = disable_notification
self._disable_web_page_preview = disable_web_page_preview self._disable_web_page_preview = disable_web_page_preview
self._timeout = timeout self._timeout = timeout
self._quote = quote self._quote = quote
self._tzinfo = tzinfo
@property @property
def parse_mode(self): 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 " raise AttributeError("You can not assign a new value to defaults after because it would "
"not have any effect.") "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): def __hash__(self):
return hash((self._parse_mode, return hash((self._parse_mode,
self._disable_notification, self._disable_notification,
self._disable_web_page_preview, self._disable_web_page_preview,
self._timeout, self._timeout,
self._quote)) self._quote,
self._tzinfo))
def __eq__(self, other): def __eq__(self, other):
if isinstance(other, Defaults): if isinstance(other, Defaults):

View file

@ -89,8 +89,9 @@ class JobQueue:
return self._tz_now() + time return self._tz_now() + time
if isinstance(time, datetime.time): if isinstance(time, datetime.time):
dt = datetime.datetime.combine( dt = datetime.datetime.combine(
datetime.datetime.now(tz=time.tzinfo or self.scheduler.timezone).date(), time, datetime.datetime.now(tz=time.tzinfo or self.scheduler.timezone).date(), time)
tzinfo=time.tzinfo or self.scheduler.timezone) if dt.tzinfo is None:
dt = self.scheduler.timezone.localize(dt)
if shift_day and dt <= datetime.datetime.now(pytz.utc): if shift_day and dt <= datetime.datetime.now(pytz.utc):
dt += datetime.timedelta(days=1) dt += datetime.timedelta(days=1)
return dt return dt
@ -106,6 +107,9 @@ class JobQueue:
""" """
self._dispatcher = dispatcher 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): 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. """Creates a new ``Job`` that runs once and adds it to the queue.
@ -129,13 +133,11 @@ class JobQueue:
job should run. job should run.
* :obj:`datetime.datetime` will be interpreted as a specific date and time at * :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`, 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 * :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, 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. tomorrow. If the timezone (``time.tzinfo``) is :obj:`None`, the
default timezone of the bot will be used.
If ``when`` is :obj:`datetime.datetime` or :obj:`datetime.time` type
then ``when.tzinfo`` will define ``Job.tzinfo``. Otherwise UTC will be assumed.
context (:obj:`object`, optional): Additional data needed for the callback function. context (:obj:`object`, optional): Additional data needed for the callback function.
Can be accessed through ``job.context`` in the callback. Defaults to :obj:`None`. Can be accessed through ``job.context`` in the callback. Defaults to :obj:`None`.
@ -193,13 +195,11 @@ class JobQueue:
job should run. job should run.
* :obj:`datetime.datetime` will be interpreted as a specific date and time at * :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`, 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 * :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, 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. tomorrow. If the timezone (``time.tzinfo``) is :obj:`None`, the
default timezone of the bot will be used.
If ``first`` is :obj:`datetime.datetime` or :obj:`datetime.time` type
then ``first.tzinfo`` will define ``Job.tzinfo``. Otherwise UTC will be assumed.
Defaults to ``interval`` Defaults to ``interval``
last (:obj:`int` | :obj:`float` | :obj:`datetime.timedelta` | \ last (:obj:`int` | :obj:`float` | :obj:`datetime.timedelta` | \
@ -208,7 +208,8 @@ class JobQueue:
depending on its type. See ``first`` for details. depending on its type. See ``first`` for details.
If ``last`` is :obj:`datetime.datetime` or :obj:`datetime.time` type 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`. Defaults to :obj:`None`.
context (:obj:`object`, optional): Additional data needed for the callback function. 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 ``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. 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 (: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 (``when.tzinfo``) is :obj:`None`, the default timezone of the bot will be used.
define ``Job.tzinfo``.
day (:obj:`int`): Defines the day of the month whereby the job would run. It should 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. be within the range of 1 and 31, inclusive.
context (:obj:`object`, optional): Additional data needed for the callback function. 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 ``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. 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 (: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``) is :obj:`None`, the default timezone of the bot will be used.
``time.tzinfo`` will implicitly define ``Job.tzinfo``.
days (Tuple[:obj:`int`], optional): Defines on which days of the week the job should days (Tuple[:obj:`int`], optional): Defines on which days of the week the job should
run. Defaults to ``EVERY_DAY`` run. Defaults to ``EVERY_DAY``
context (:obj:`object`, optional): Additional data needed for the callback function. context (:obj:`object`, optional): Additional data needed for the callback function.

View file

@ -26,6 +26,8 @@ from collections import defaultdict
from html import escape from html import escape
from numbers import Number from numbers import Number
import pytz
try: try:
import ujson as json import ujson as json
except ImportError: except ImportError:
@ -72,8 +74,6 @@ def escape_markdown(text, version=1, entity_type=None):
# -------- date/time related helpers -------- # -------- date/time related helpers --------
# TODO: add generic specification of UTC for naive datetimes to docs
def _datetime_to_float_timestamp(dt_obj): def _datetime_to_float_timestamp(dt_obj):
""" """
Converts a datetime object to a float timestamp (with sub-second precision). 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() 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. Converts a given time object to a float POSIX timestamp.
Used to convert different time specifications to a common format. The time object 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. 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 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`). :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 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 ``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. 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: Returns:
(float | None) The return value depends on the type of argument ``t``. If ``t`` is (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() return reference_timestamp + t.total_seconds()
elif isinstance(t, Number): elif isinstance(t, Number):
return reference_timestamp + t return reference_timestamp + t
elif isinstance(t, dtm.time):
if t.tzinfo is not None: if tzinfo is None:
reference_dt = dtm.datetime.fromtimestamp(reference_timestamp, tz=t.tzinfo) tzinfo = pytz.utc
else:
reference_dt = dtm.datetime.utcfromtimestamp(reference_timestamp) # assume UTC if isinstance(t, dtm.time):
reference_dt = dtm.datetime.fromtimestamp(reference_timestamp, tz=t.tzinfo or tzinfo)
reference_date = reference_dt.date() reference_date = reference_dt.date()
reference_time = reference_dt.timetz() 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) aware_datetime = dtm.datetime.combine(reference_date, t)
return _datetime_to_float_timestamp(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): elif isinstance(t, dtm.datetime):
if t.tzinfo is None:
t = tzinfo.localize(t)
return _datetime_to_float_timestamp(t) return _datetime_to_float_timestamp(t)
raise TypeError('Unable to convert {} object to timestamp'.format(type(t).__name__)) 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 Wrapper over :func:`to_float_timestamp` which returns an integer (the float value truncated
down to the nearest integer). down to the nearest integer).
See the documentation for :func:`to_float_timestamp` for more details. 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. 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`). :obj:`None`s are left alone (i.e. ``from_timestamp(None)`` is :obj:`None`).

View file

@ -69,6 +69,18 @@ def default_bot(request, bot_info):
return default_bot 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') @pytest.fixture(scope='session')
def chat_id(bot_info): def chat_id(bot_info):
return bot_info['chat_id'] return bot_info['chat_id']

View file

@ -29,7 +29,7 @@ from telegram import (Bot, Update, ChatAction, TelegramError, User, InlineKeyboa
InlineQueryResultDocument, Dice, MessageEntity, ParseMode) InlineQueryResultDocument, Dice, MessageEntity, ParseMode)
from telegram.constants import MAX_INLINE_QUERY_RESULTS from telegram.constants import MAX_INLINE_QUERY_RESULTS
from telegram.error import BadRequest, InvalidToken, NetworkError, RetryAfter 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 from tests.conftest import expect_bad_request
BASE_TIME = time.time() BASE_TIME = time.time()
@ -272,6 +272,29 @@ class TestBot:
assert new_message.poll.id == message.poll.id assert new_message.poll.id == message.poll.id
assert new_message.poll.is_closed 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) @flaky(3, 1)
@pytest.mark.timeout(10) @pytest.mark.timeout(10)
@pytest.mark.parametrize('default_bot', [{'parse_mode': 'Markdown'}], indirect=True) @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=until)
assert bot.kick_chat_member(2, 32, until_date=1577887200) 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. # TODO: Needs improvement.
def test_unban_chat_member(self, monkeypatch, bot): def test_unban_chat_member(self, monkeypatch, bot):
def test(url, data, *args, **kwargs): def test(url, data, *args, **kwargs):
@ -951,6 +990,28 @@ class TestBot:
chat_permissions, chat_permissions,
until_date=dtm.datetime.utcnow()) 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) @flaky(3, 1)
@pytest.mark.timeout(10) @pytest.mark.timeout(10)
def test_promote_chat_member(self, bot, channel_id): def test_promote_chat_member(self, bot, channel_id):

View file

@ -37,6 +37,8 @@ class TestDefault:
defaults.timeout = True defaults.timeout = True
with pytest.raises(AttributeError): with pytest.raises(AttributeError):
defaults.quote = True defaults.quote = True
with pytest.raises(AttributeError):
defaults.tzinfo = True
def test_equality(self): def test_equality(self):
a = Defaults(parse_mode='HTML', quote=True) a = Defaults(parse_mode='HTML', quote=True)

View file

@ -190,7 +190,7 @@ class TestFilters:
matches = result['matches'] matches = result['matches']
assert isinstance(matches, list) assert isinstance(matches, list)
assert all([type(res) == SRE_TYPE for res in matches]) assert all([type(res) == SRE_TYPE for res in matches])
update.message.forward_date = False update.message.forward_date = None
result = filter(update) result = filter(update)
assert not result assert not result
update.message.text = 'test it out' update.message.text = 'test it out'
@ -926,7 +926,7 @@ class TestFilters:
update.message.text = 'test' update.message.text = 'test'
update.message.forward_date = datetime.datetime.utcnow() update.message.forward_date = datetime.datetime.utcnow()
assert (Filters.text & (Filters.status_update | Filters.forwarded))(update) 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) assert not (Filters.text & (Filters.forwarded | Filters.status_update))(update)
update.message.pinned_message = True update.message.pinned_message = True
assert (Filters.text & (Filters.forwarded | Filters.status_update)(update)) assert (Filters.text & (Filters.forwarded | Filters.status_update)(update))

View file

@ -25,6 +25,7 @@ from telegram import Sticker
from telegram import Update from telegram import Update
from telegram import User from telegram import User
from telegram import MessageEntity from telegram import MessageEntity
from telegram.ext import Defaults
from telegram.message import Message from telegram.message import Message
from telegram.utils import helpers from telegram.utils import helpers
from telegram.utils.helpers import _datetime_to_float_timestamp from telegram.utils.helpers import _datetime_to_float_timestamp
@ -135,6 +136,10 @@ class TestHelpers:
assert (helpers.to_float_timestamp(time_spec) assert (helpers.to_float_timestamp(time_spec)
== pytest.approx(helpers.to_float_timestamp(time_spec, reference_timestamp=now))) == 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) @pytest.mark.parametrize('time_spec', TIME_SPECS, ids=str)
def test_to_timestamp(self, time_spec): def test_to_timestamp(self, time_spec):
# delegate tests to `to_float_timestamp` # delegate tests to `to_float_timestamp`
@ -144,6 +149,9 @@ class TestHelpers:
# this 'convenience' behaviour has been left left for backwards compatibility # this 'convenience' behaviour has been left left for backwards compatibility
assert helpers.to_timestamp(None) is None 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): def test_from_timestamp_naive(self):
datetime = dtm.datetime(2019, 11, 11, 0, 26, 16, tzinfo=None) datetime = dtm.datetime(2019, 11, 11, 0, 26, 16, tzinfo=None)
assert helpers.from_timestamp(1573431976, tzinfo=None) == datetime assert helpers.from_timestamp(1573431976, tzinfo=None) == datetime

View file

@ -121,7 +121,7 @@ class TestJobQueue:
sleep(0.07) sleep(0.07)
assert self.result == 1 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``""" """Test correct scheduling of job when passing a timezone-aware datetime as ``first``"""
job_queue.run_repeating(self.job_run_once, 0.1, job_queue.run_repeating(self.job_run_once, 0.1,
first=dtm.datetime.now(timezone) + dtm.timedelta(seconds=0.05)) first=dtm.datetime.now(timezone) + dtm.timedelta(seconds=0.05))
@ -135,15 +135,6 @@ class TestJobQueue:
sleep(0.1) sleep(0.1)
assert self.result == 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): def test_run_repeating_last_before_first(self, job_queue):
with pytest.raises(ValueError, match="'last' must not be before 'first'!"): 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) 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) time_of_day = expected_reschedule_time.time().replace(tzinfo=timezone)
day = now.day 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() expected_reschedule_time = expected_reschedule_time.timestamp()
job_queue.run_monthly(self.job_run_once, time_of_day, day) 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() scheduled_time = job_queue.jobs()[0].next_t.timestamp()
assert scheduled_time == pytest.approx(expected_reschedule_time) 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]) @pytest.mark.parametrize('use_context', [True, False])
def test_get_jobs(self, job_queue, use_context): def test_get_jobs(self, job_queue, use_context):
job_queue._dispatcher.use_context = use_context job_queue._dispatcher.use_context = use_context

View file

@ -169,13 +169,13 @@ class TestMessage:
MessageEntity(**e) for e in test_entities_v2 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) new = Message.de_json(message_params.to_dict(), bot)
assert new.to_dict() == message_params.to_dict() assert new.to_dict() == message_params.to_dict()
def test_dict_approach(self, message): 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['chat_id'] == message.chat_id
assert message['no_key'] is None assert message['no_key'] is None

View file

@ -20,6 +20,8 @@
import pytest import pytest
from datetime import datetime from datetime import datetime
from telegram import Poll, PollOption, PollAnswer, User, MessageEntity from telegram import Poll, PollOption, PollAnswer, User, MessageEntity
from telegram.utils.helpers import to_timestamp from telegram.utils.helpers import to_timestamp
@ -154,7 +156,7 @@ class TestPoll:
open_period = 42 open_period = 42
close_date = datetime.utcnow() close_date = datetime.utcnow()
def test_de_json(self): def test_de_json(self, bot):
json_dict = { json_dict = {
'id': self.id_, 'id': self.id_,
'question': self.question, 'question': self.question,
@ -169,7 +171,7 @@ class TestPoll:
'open_period': self.open_period, 'open_period': self.open_period,
'close_date': to_timestamp(self.close_date) '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.id == self.id_
assert poll.question == self.question assert poll.question == self.question