From 23fe991b85bb2637bf3b82e2298104e8f6b3ffee Mon Sep 17 00:00:00 2001 From: Ambro Date: Fri, 4 Jan 2019 16:29:07 -0300 Subject: [PATCH 1/3] Fix bug: unable to save jobs with timezone aware dates (#1308) * Fix bug on jobs with timezone aware dates * Add Ambro17 as colaborator --- AUTHORS.rst | 1 + telegram/ext/jobqueue.py | 2 +- tests/test_jobqueue.py | 18 ++++++++++++++++++ 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/AUTHORS.rst b/AUTHORS.rst index be534f777..cecf99f84 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -16,6 +16,7 @@ Contributors The following wonderful people contributed directly or indirectly to this project: - `Alateas `_ +- `Ambro17 `_ - `Anton Tagunov `_ - `Avanatiker `_ - `Balduro `_ diff --git a/telegram/ext/jobqueue.py b/telegram/ext/jobqueue.py index e6eb36012..ec9dc12b8 100644 --- a/telegram/ext/jobqueue.py +++ b/telegram/ext/jobqueue.py @@ -62,7 +62,7 @@ class JobQueue(object): raise ValueError('next_t is None') if isinstance(next_t, datetime.datetime): - next_t = (next_t - datetime.datetime.now()).total_seconds() + next_t = (next_t - datetime.datetime.now(next_t.tzinfo)).total_seconds() elif isinstance(next_t, datetime.time): next_datetime = datetime.datetime.combine(datetime.date.today(), next_t) diff --git a/tests/test_jobqueue.py b/tests/test_jobqueue.py index 083a3d194..691fb0a21 100644 --- a/tests/test_jobqueue.py +++ b/tests/test_jobqueue.py @@ -184,6 +184,24 @@ class TestJobQueue(object): sleep(0.06) assert pytest.approx(self.job_time) == expected_time + def test_datetime_with_timezone_job_run_once(self, job_queue): + # Test that run_once jobs work with timezone aware datetimes. + offset = datetime.timedelta(hours=-3) + when = datetime.datetime.now(datetime.timezone(offset)) + + job_queue.run_once(self.job_run_once, when) + sleep(0.01) + assert self.result == 1 + + def test_datetime_with_timezone_job_run_repeating(self, job_queue): + # Test that run_repeating jobs work with timezone aware datetimes. + offset = datetime.timedelta(hours=5) + now_with_offset = datetime.datetime.now(datetime.timezone(offset)) + + job_queue.run_repeating(self.job_run_once, interval=0.01, first=now_with_offset) + sleep(0.015) + assert self.result == 2 + def test_time_unit_dt_time_today(self, job_queue): # Testing running at a specific time today delta = 0.05 From c03160c07f89e30c7023fb12f7a372c3df17d74e Mon Sep 17 00:00:00 2001 From: Tanuj Date: Fri, 4 Jan 2019 20:04:45 +0000 Subject: [PATCH 2/3] Add convenience classmethods for InlineKeyboardMarkup (fixes #1186) (#1260) * Add convenience classmethods for InlineKeyboardMarkup (#1186) * Switch to row and column methods * Also add convenience classmethods for ReplyKeyboardMarkup * Add some simple tests --- telegram/inline/inlinekeyboardmarkup.py | 48 ++++++++++ telegram/replykeyboardmarkup.py | 122 ++++++++++++++++++++++++ tests/test_inlinekeyboardmarkup.py | 21 ++++ tests/test_replykeyboardmarkup.py | 34 +++++++ 4 files changed, 225 insertions(+) diff --git a/telegram/inline/inlinekeyboardmarkup.py b/telegram/inline/inlinekeyboardmarkup.py index 26f77af0b..d212d896c 100644 --- a/telegram/inline/inlinekeyboardmarkup.py +++ b/telegram/inline/inlinekeyboardmarkup.py @@ -48,3 +48,51 @@ class InlineKeyboardMarkup(ReplyMarkup): data['inline_keyboard'].append([x.to_dict() for x in inline_keyboard]) return data + + @classmethod + def from_button(cls, button, **kwargs): + """Shortcut for:: + + InlineKeyboardMarkup([[button]], **kwargs) + + Return an InlineKeyboardMarkup from a single InlineKeyboardButton + + Args: + button (:class:`telegram.InlineKeyboardButton`): The button to use in the markup + **kwargs (:obj:`dict`): Arbitrary keyword arguments. + + """ + return cls([[button]], **kwargs) + + @classmethod + def from_row(cls, button_row, **kwargs): + """Shortcut for:: + + InlineKeyboardMarkup([button_row], **kwargs) + + Return an InlineKeyboardMarkup from a single row of InlineKeyboardButtons + + Args: + button_row (List[:class:`telegram.InlineKeyboardButton`]): The button to use in the + markup + **kwargs (:obj:`dict`): Arbitrary keyword arguments. + + """ + return cls([button_row], **kwargs) + + @classmethod + def from_column(cls, button_column, **kwargs): + """Shortcut for:: + + InlineKeyboardMarkup([[button] for button in button_column], **kwargs) + + Return an InlineKeyboardMarkup from a single column of InlineKeyboardButtons + + Args: + button_column (List[:class:`telegram.InlineKeyboardButton`]): The button to use in the + markup + **kwargs (:obj:`dict`): Arbitrary keyword arguments. + + """ + button_grid = [[button] for button in button_column] + return cls(button_grid, **kwargs) diff --git a/telegram/replykeyboardmarkup.py b/telegram/replykeyboardmarkup.py index d27572a1a..114e93454 100644 --- a/telegram/replykeyboardmarkup.py +++ b/telegram/replykeyboardmarkup.py @@ -85,3 +85,125 @@ class ReplyKeyboardMarkup(ReplyMarkup): r.append(button) # str data['keyboard'].append(r) return data + + @classmethod + def from_button(cls, + button, + resize_keyboard=False, + one_time_keyboard=False, + selective=False, + **kwargs): + """Shortcut for:: + + ReplyKeyboardMarkup([[button]], **kwargs) + + Return an ReplyKeyboardMarkup from a single KeyboardButton + + Args: + button (:class:`telegram.KeyboardButton` | :obj:`str`): The button to use in the markup + resize_keyboard (:obj:`bool`, optional): Requests clients to resize the keyboard vertically + for optimal fit (e.g., make the keyboard smaller if there are just two rows of + buttons). Defaults to false, in which case the custom keyboard is always of the same + height as the app's standard keyboard. Defaults to ``False`` + one_time_keyboard (:obj:`bool`, optional): Requests clients to hide the keyboard as soon as + it's been used. The keyboard will still be available, but clients will automatically + display the usual letter-keyboard in the chat - the user can press a special button in + the input field to see the custom keyboard again. Defaults to ``False``. + selective (:obj:`bool`, optional): Use this parameter if you want to show the keyboard to + specific users only. Targets: + + 1) users that are @mentioned in the text of the Message object + 2) if the bot's message is a reply (has reply_to_message_id), sender of the original + message. + + Defaults to ``False``. + **kwargs (:obj:`dict`): Arbitrary keyword arguments. + """ + return cls([[button]], + resize_keyboard=resize_keyboard, + one_time_keyboard=one_time_keyboard, + selective=selective, + **kwargs) + + @classmethod + def from_row(cls, + button_row, + resize_keyboard=False, + one_time_keyboard=False, + selective=False, + **kwargs): + """Shortcut for:: + + ReplyKeyboardMarkup([button_row], **kwargs) + + Return an ReplyKeyboardMarkup from a single row of KeyboardButtons + + Args: + button_row (List[:class:`telegram.KeyboardButton` | :obj:`str`]): The button to use in the + markup + resize_keyboard (:obj:`bool`, optional): Requests clients to resize the keyboard vertically + for optimal fit (e.g., make the keyboard smaller if there are just two rows of + buttons). Defaults to false, in which case the custom keyboard is always of the same + height as the app's standard keyboard. Defaults to ``False`` + one_time_keyboard (:obj:`bool`, optional): Requests clients to hide the keyboard as soon as + it's been used. The keyboard will still be available, but clients will automatically + display the usual letter-keyboard in the chat - the user can press a special button in + the input field to see the custom keyboard again. Defaults to ``False``. + selective (:obj:`bool`, optional): Use this parameter if you want to show the keyboard to + specific users only. Targets: + + 1) users that are @mentioned in the text of the Message object + 2) if the bot's message is a reply (has reply_to_message_id), sender of the original + message. + + Defaults to ``False``. + **kwargs (:obj:`dict`): Arbitrary keyword arguments. + + """ + return cls([button_row], + resize_keyboard=resize_keyboard, + one_time_keyboard=one_time_keyboard, + selective=selective, + **kwargs) + + @classmethod + def from_column(cls, + button_column, + resize_keyboard=False, + one_time_keyboard=False, + selective=False, + **kwargs): + """Shortcut for:: + + ReplyKeyboardMarkup([[button] for button in button_column], **kwargs) + + Return an ReplyKeyboardMarkup from a single column of KeyboardButtons + + Args: + button_column (List[:class:`telegram.KeyboardButton` | :obj:`str`]): The button to use in the + markup + resize_keyboard (:obj:`bool`, optional): Requests clients to resize the keyboard vertically + for optimal fit (e.g., make the keyboard smaller if there are just two rows of + buttons). Defaults to false, in which case the custom keyboard is always of the same + height as the app's standard keyboard. Defaults to ``False`` + one_time_keyboard (:obj:`bool`, optional): Requests clients to hide the keyboard as soon as + it's been used. The keyboard will still be available, but clients will automatically + display the usual letter-keyboard in the chat - the user can press a special button in + the input field to see the custom keyboard again. Defaults to ``False``. + selective (:obj:`bool`, optional): Use this parameter if you want to show the keyboard to + specific users only. Targets: + + 1) users that are @mentioned in the text of the Message object + 2) if the bot's message is a reply (has reply_to_message_id), sender of the original + message. + + Defaults to ``False``. + **kwargs (:obj:`dict`): Arbitrary keyword arguments. + + """ + button_grid = [[button] for button in button_column] + return cls(button_grid, + resize_keyboard=resize_keyboard, + one_time_keyboard=one_time_keyboard, + selective=selective, + **kwargs) diff --git a/tests/test_inlinekeyboardmarkup.py b/tests/test_inlinekeyboardmarkup.py index 169c6220c..68da77065 100644 --- a/tests/test_inlinekeyboardmarkup.py +++ b/tests/test_inlinekeyboardmarkup.py @@ -44,6 +44,27 @@ class TestInlineKeyboardMarkup(object): assert message.text == 'Testing InlineKeyboardMarkup' + def test_from_button(self): + inline_keyboard_markup = InlineKeyboardMarkup.from_button( + InlineKeyboardButton(text='button1', callback_data='data1')).inline_keyboard + assert len(inline_keyboard_markup) == 1 + assert len(inline_keyboard_markup[0]) == 1 + + def test_from_row(self): + inline_keyboard_markup = InlineKeyboardMarkup.from_row([ + InlineKeyboardButton(text='button1', callback_data='data1'), + InlineKeyboardButton(text='button1', callback_data='data1')]).inline_keyboard + assert len(inline_keyboard_markup) == 1 + assert len(inline_keyboard_markup[0]) == 2 + + def test_from_column(self): + inline_keyboard_markup = InlineKeyboardMarkup.from_column([ + InlineKeyboardButton(text='button1', callback_data='data1'), + InlineKeyboardButton(text='button1', callback_data='data1')]).inline_keyboard + assert len(inline_keyboard_markup) == 2 + assert len(inline_keyboard_markup[0]) == 1 + assert len(inline_keyboard_markup[1]) == 1 + def test_expected_values(self, inline_keyboard_markup): assert inline_keyboard_markup.inline_keyboard == self.inline_keyboard diff --git a/tests/test_replykeyboardmarkup.py b/tests/test_replykeyboardmarkup.py index cd0cf3684..91f997f7f 100644 --- a/tests/test_replykeyboardmarkup.py +++ b/tests/test_replykeyboardmarkup.py @@ -51,6 +51,40 @@ class TestReplyKeyboardMarkup(object): assert message.text == 'text 2' + def test_from_button(self): + reply_keyboard_markup = ReplyKeyboardMarkup.from_button( + KeyboardButton(text='button1')).keyboard + assert len(reply_keyboard_markup) == 1 + assert len(reply_keyboard_markup[0]) == 1 + + reply_keyboard_markup = ReplyKeyboardMarkup.from_button('button1').keyboard + assert len(reply_keyboard_markup) == 1 + assert len(reply_keyboard_markup[0]) == 1 + + def test_from_row(self): + reply_keyboard_markup = ReplyKeyboardMarkup.from_row([ + KeyboardButton(text='button1'), + KeyboardButton(text='button2')]).keyboard + assert len(reply_keyboard_markup) == 1 + assert len(reply_keyboard_markup[0]) == 2 + + reply_keyboard_markup = ReplyKeyboardMarkup.from_row(['button1', 'button2']).keyboard + assert len(reply_keyboard_markup) == 1 + assert len(reply_keyboard_markup[0]) == 2 + + def test_from_column(self): + reply_keyboard_markup = ReplyKeyboardMarkup.from_column([ + KeyboardButton(text='button1'), + KeyboardButton(text='button2')]).keyboard + assert len(reply_keyboard_markup) == 2 + assert len(reply_keyboard_markup[0]) == 1 + assert len(reply_keyboard_markup[1]) == 1 + + reply_keyboard_markup = ReplyKeyboardMarkup.from_column(['button1', 'button2']).keyboard + assert len(reply_keyboard_markup) == 2 + assert len(reply_keyboard_markup[0]) == 1 + assert len(reply_keyboard_markup[1]) == 1 + def test_expected_values(self, reply_keyboard_markup): assert isinstance(reply_keyboard_markup.keyboard, list) assert isinstance(reply_keyboard_markup.keyboard[0][0], KeyboardButton) From 3e8d71582dbdf2e8a7f44692d49eb1e9330f0fb8 Mon Sep 17 00:00:00 2001 From: Gregory Petukhov Date: Wed, 30 Jan 2019 21:38:15 +0300 Subject: [PATCH 3/3] Fix #1328: custom timeout argument does not work (#1330) * Fix #1328: custom timeout argument does not work * Remove unused import --- telegram/utils/request.py | 3 ++- tests/test_bot.py | 21 ++++++++++++++++----- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/telegram/utils/request.py b/telegram/utils/request.py index db948fb30..ca3a7731e 100644 --- a/telegram/utils/request.py +++ b/telegram/utils/request.py @@ -323,7 +323,8 @@ class Request(object): else: result = self._request_wrapper('POST', url, body=json.dumps(data).encode('utf-8'), - headers={'Content-Type': 'application/json'}) + headers={'Content-Type': 'application/json'}, + **urlopen_kwargs) return self._parse(result) diff --git a/tests/test_bot.py b/tests/test_bot.py index fe3b38736..a84d52abf 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -618,16 +618,27 @@ class TestBot(object): # test_sticker module. def test_timeout_propagation(self, monkeypatch, bot, chat_id): + + from telegram.vendor.ptb_urllib3.urllib3.util.timeout import Timeout + class OkException(Exception): pass - timeout = 500 + TIMEOUT = 500 - def post(*args, **kwargs): - if kwargs.get('timeout') == 500: + def request_wrapper(*args, **kwargs): + obj = kwargs.get('timeout') + if isinstance(obj, Timeout) and obj._read == TIMEOUT: raise OkException - monkeypatch.setattr('telegram.utils.request.Request.post', post) + return b'{"ok": true, "result": []}' + monkeypatch.setattr('telegram.utils.request.Request._request_wrapper', request_wrapper) + + # Test file uploading with pytest.raises(OkException): - bot.send_photo(chat_id, open('tests/data/telegram.jpg', 'rb'), timeout=timeout) + bot.send_photo(chat_id, open('tests/data/telegram.jpg', 'rb'), timeout=TIMEOUT) + + # Test JSON submition + with pytest.raises(OkException): + bot.get_chat_administrators(chat_id, timeout=TIMEOUT)