From 9b5e014a0aa69e9e1a6e32aeb406fa03ac871883 Mon Sep 17 00:00:00 2001 From: saschalalala Date: Sun, 18 Jun 2017 12:09:32 +0200 Subject: [PATCH 1/4] Simplification of boolean checks (#662) * Simplification of boolean checks * Cast ok to bool for Telegram API json encoding --- telegram/bot.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/telegram/bot.py b/telegram/bot.py index 05b617405..0776b66d7 100644 --- a/telegram/bot.py +++ b/telegram/bot.py @@ -1960,12 +1960,14 @@ class Bot(TelegramObject): """ - if ok is True and (shipping_options is None or error_message is not None): + ok = bool(ok) + + if ok and (shipping_options is None or error_message is not None): raise TelegramError( 'answerShippingQuery: If ok is True, shipping_options ' 'should not be empty and there should not be error_message') - if ok is False and (shipping_options is not None or error_message is None): + if not ok and (shipping_options is not None or error_message is None): raise TelegramError( 'answerShippingQuery: If ok is False, error_message ' 'should not be empty and there should not be shipping_options') @@ -1974,7 +1976,7 @@ class Bot(TelegramObject): data = {'shipping_query_id': shipping_query_id, 'ok': ok} - if ok is True: + if ok: data['shipping_options'] = [option.to_dict() for option in shipping_options] if error_message is not None: data['error_message'] = error_message @@ -2009,6 +2011,8 @@ class Bot(TelegramObject): """ + ok = bool(ok) + if not (ok ^ (error_message is not None)): raise TelegramError( 'answerPreCheckoutQuery: If ok is True, there should ' From faddb923957f09debc6031e7744f35c95c439112 Mon Sep 17 00:00:00 2001 From: Jacob Bom Date: Sun, 18 Jun 2017 12:14:24 +0200 Subject: [PATCH 2/4] Clean up Bot code a bit (#673) * Clean up Bot code a bit - Move decorators to module. It really wasn't clear how decorators inside classes work, and why they didn't have a self parameter, but still wasn't static. This also makes them effectively private without having to underscore them, which I think we should have done long time ago atm. Note that this might break backwards compatibility slightly (only if people are daft enough to have used the decorators themselves) - Don't call _message_wrapper directly. Ever. Instead always use the message decorator, since it's what it's there for. Closes #627 - Don't use the message decorator if the method isn't supposed to return a message. The decorator could handle values like True (which is often the return value), but to someone reading the code, it seems like it's a message returning method even when it wasn't. - Always document timeout and **kwargs - Log all methods * Add test to make sure timeout propagates properly despite decorators --- telegram/bot.py | 195 +++++++++++++++----------------------- telegram/utils/request.py | 2 +- tests/test_bot.py | 19 ++++ 3 files changed, 95 insertions(+), 121 deletions(-) diff --git a/telegram/bot.py b/telegram/bot.py index 0776b66d7..1814abd28 100644 --- a/telegram/bot.py +++ b/telegram/bot.py @@ -31,10 +31,45 @@ from telegram.utils.request import Request logging.getLogger(__name__).addHandler(logging.NullHandler()) +def info(func): + @functools.wraps(func) + def decorator(self, *args, **kwargs): + if not self.bot: + self.get_me() + + result = func(self, *args, **kwargs) + return result + + return decorator + + +def log(func): + logger = logging.getLogger(func.__module__) + + @functools.wraps(func) + def decorator(self, *args, **kwargs): + logger.debug('Entering: %s', func.__name__) + result = func(self, *args, **kwargs) + logger.debug(result) + logger.debug('Exiting: %s', func.__name__) + return result + + return decorator + + +def message(func): + @functools.wraps(func) + def decorator(self, *args, **kwargs): + url, data = func(self, *args, **kwargs) + return self._message_wrapper(url, data, *args, **kwargs) + + return decorator + + class Bot(TelegramObject): """This object represents a Telegram Bot. - Attributes: + Properties: id (int): Unique identifier for this bot. first_name (str): Bot's first name. last_name (str): Bot's last name. @@ -80,18 +115,6 @@ class Bot(TelegramObject): return token - def info(func): - - @functools.wraps(func) - def decorator(self, *args, **kwargs): - if not self.bot: - self.get_me() - - result = func(self, *args, **kwargs) - return result - - return decorator - @property @info def id(self): @@ -116,19 +139,6 @@ class Bot(TelegramObject): def name(self): return '@{0}'.format(self.username) - def log(func): - logger = logging.getLogger(func.__module__) - - @functools.wraps(func) - def decorator(self, *args, **kwargs): - logger.debug('Entering: %s', func.__name__) - result = func(self, *args, **kwargs) - logger.debug(result) - logger.debug('Exiting: %s', func.__name__) - return result - - return decorator - def _message_wrapper(self, url, data, *args, **kwargs): if kwargs.get('reply_to_message_id'): data['reply_to_message_id'] = kwargs.get('reply_to_message_id') @@ -150,15 +160,6 @@ class Bot(TelegramObject): return Message.de_json(result, self) - def message(func): - - @functools.wraps(func) - def decorator(self, *args, **kwargs): - url, data = func(self, *args, **kwargs) - return Bot._message_wrapper(self, url, data, *args, **kwargs) - - return decorator - @log def get_me(self, timeout=None, **kwargs): """A simple method for testing your bot's auth token. @@ -242,8 +243,7 @@ class Bot(TelegramObject): return url, data @log - @message - def delete_message(self, chat_id, message_id): + def delete_message(self, chat_id, message_id, timeout=None, **kwargs): """Use this method to delete a message. A message can only be deleted if it was sent less than 48 hours ago. Any such recently sent outgoing message may be deleted. Additionally, if the bot is an administrator in a group chat, it can delete any message. If the bot is @@ -257,6 +257,10 @@ class Bot(TelegramObject): username of the target channel (in the format @channelusername). message_id (int): Unique message identifier. + timeout (Optional[int|float]): If this value is specified, use it as the read timeout + from the server (instead of the one specified during creation of the connection + pool). + **kwargs (dict): Arbitrary keyword arguments. Returns: bool: On success, `True` is returned. @@ -269,7 +273,9 @@ class Bot(TelegramObject): data = {'chat_id': chat_id, 'message_id': message_id} - return url, data + result = self._request.post(url, data, timeout=timeout) + + return result @log @message @@ -315,6 +321,7 @@ class Bot(TelegramObject): return url, data @log + @message def send_photo(self, chat_id, photo, @@ -356,19 +363,10 @@ class Bot(TelegramObject): if caption: data['caption'] = caption - return self._message_wrapper( - url, - data, - chat_id=chat_id, - photo=photo, - caption=caption, - disable_notification=disable_notification, - reply_to_message_id=reply_to_message_id, - reply_markup=reply_markup, - timeout=timeout, - **kwargs) + return url, data @log + @message def send_audio(self, chat_id, audio, @@ -431,22 +429,10 @@ class Bot(TelegramObject): if caption: data['caption'] = caption - return self._message_wrapper( - url, - data, - chat_id=chat_id, - audio=audio, - duration=duration, - performer=performer, - title=title, - caption=caption, - disable_notification=disable_notification, - reply_to_message_id=reply_to_message_id, - reply_markup=reply_markup, - timeout=timeout, - **kwargs) + return url, data @log + @message def send_document(self, chat_id, document, @@ -493,18 +479,7 @@ class Bot(TelegramObject): if caption: data['caption'] = caption - return self._message_wrapper( - url, - data, - chat_id=chat_id, - document=document, - filename=filename, - caption=caption, - disable_notification=disable_notification, - reply_to_message_id=reply_to_message_id, - reply_markup=reply_markup, - timeout=timeout, - **kwargs) + return url, data @log @message @@ -549,6 +524,7 @@ class Bot(TelegramObject): return url, data @log + @message def send_video(self, chat_id, video, @@ -595,20 +571,10 @@ class Bot(TelegramObject): if caption: data['caption'] = caption - return self._message_wrapper( - url, - data, - chat_id=chat_id, - video=video, - duration=duration, - caption=caption, - disable_notification=disable_notification, - reply_to_message_id=reply_to_message_id, - reply_markup=reply_markup, - timeout=timeout, - **kwargs) + return url, data @log + @message def send_voice(self, chat_id, voice, @@ -658,20 +624,10 @@ class Bot(TelegramObject): if caption: data['caption'] = caption - return self._message_wrapper( - url, - data, - chat_id=chat_id, - voice=voice, - duration=duration, - caption=caption, - disable_notification=disable_notification, - reply_to_message_id=reply_to_message_id, - reply_markup=reply_markup, - timeout=timeout, - **kwargs) + return url, data @log + @message def send_video_note(self, chat_id, video_note, @@ -718,18 +674,7 @@ class Bot(TelegramObject): if length is not None: data['length'] = length - return self._message_wrapper( - url, - data, - chat_id=chat_id, - video_note=video_note, - duration=duration, - length=length, - disable_notification=disable_notification, - reply_to_message_id=reply_to_message_id, - reply_markup=reply_markup, - timeout=timeout, - **kwargs) + return url, data @log @message @@ -898,8 +843,6 @@ class Bot(TelegramObject): channel (in the format @channelusername). game_short_name (str): Short name of the game, serves as the unique identifier for the game. - - Keyword Args: disable_notification (Optional[bool]): Sends the message silently. iOS users will not receive a notification, Android users will receive a notification with no sound. reply_to_message_id (Optional[int]): If the message is a reply, @@ -910,6 +853,7 @@ class Bot(TelegramObject): timeout (Optional[int|float]): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). + **kwargs (dict): Arbitrary keyword arguments. Returns: :class:`telegram.Message`: On success, the sent message is returned. @@ -925,7 +869,6 @@ class Bot(TelegramObject): return url, data @log - @message def send_chat_action(self, chat_id, action, timeout=None, **kwargs): """Use this method when you need to tell the user that something is happening on the bot's side. The status is set for 5 seconds or less (when a message arrives from your bot, @@ -952,7 +895,9 @@ class Bot(TelegramObject): data = {'chat_id': chat_id, 'action': action} - return url, data + result = self._request.post(url, data, timeout=timeout) + + return result @log def answer_inline_query(self, @@ -1698,6 +1643,7 @@ class Bot(TelegramObject): timeout (Optional[int|float]): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). + **kwargs (dict): Arbitrary keyword arguments. Returns: :class: `telegram.WebhookInfo` @@ -1711,6 +1657,8 @@ class Bot(TelegramObject): return WebhookInfo.de_json(result, self) + @log + @message def set_game_score(self, user_id, score, @@ -1743,6 +1691,7 @@ class Bot(TelegramObject): timeout (Optional[int|float]): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). + **kwargs (dict): Arbitrary keyword arguments. Returns: :class:`telegram.Message` or True: The edited message, or if the @@ -1770,12 +1719,9 @@ class Bot(TelegramObject): else: warnings.warn('edit_message is ignored when disable_edit_message is used') - result = self._request.post(url, data, timeout=timeout) - if result is True: - return result - else: - return Message.de_json(result, self) + return url, data + @log def get_game_high_scores(self, user_id, chat_id=None, @@ -1797,6 +1743,7 @@ class Bot(TelegramObject): timeout (Optional[int|float]): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). + **kwargs (dict): Arbitrary keyword arguments. Returns: list[:class:`telegram.GameHighScore`]: Scores of the specified user and several of his @@ -1927,6 +1874,7 @@ class Bot(TelegramObject): return url, data + @log def answer_shipping_query(self, shipping_query_id, ok, @@ -1950,6 +1898,9 @@ class Bot(TelegramObject): form that explains why it is impossible to complete the order (e.g. "Sorry, delivery to your desired address is unavailable'). Telegram will display this message to the user. + timeout (Optional[int|float]): If this value is specified, use it as the read timeout + from the server (instead of the one specified during creation of the connection + pool). **kwargs (dict): Arbitrary keyword arguments. Returns: @@ -1985,6 +1936,7 @@ class Bot(TelegramObject): return result + @log def answer_pre_checkout_query(self, pre_checkout_query_id, ok, error_message=None, timeout=None, **kwargs): """ @@ -2001,6 +1953,9 @@ class Bot(TelegramObject): "Sorry, somebody just bought the last of our amazing black T-shirts while you were busy filling out your payment details. Please choose a different color or garment!"). Telegram will display this message to the user. + timeout (Optional[int|float]): If this value is specified, use it as the read timeout + from the server (instead of the one specified during creation of the connection + pool). **kwargs (dict): Arbitrary keyword arguments. Returns: diff --git a/telegram/utils/request.py b/telegram/utils/request.py index 19531f528..5bd8043ce 100644 --- a/telegram/utils/request.py +++ b/telegram/utils/request.py @@ -224,7 +224,7 @@ class Request(object): """Request an URL. Args: url (str): The web location we want to retrieve. - data (dict[str, str]): A dict of key/value pairs. Note: On py2.7 value is unicode. + data (dict[str, str|int]): A dict of key/value pairs. Note: On py2.7 value is unicode. timeout (Optional[int|float]): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). diff --git a/tests/test_bot.py b/tests/test_bot.py index 700253a61..169fa5cf5 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -31,6 +31,7 @@ from flaky import flaky sys.path.append('.') import telegram +from telegram.utils.request import Request from telegram.error import BadRequest from tests.base import BaseTest, timeout @@ -467,6 +468,24 @@ class BotTest(BaseTest, unittest.TestCase): self.assertEqual(name, message.contact.first_name) self.assertEqual(last, message.contact.last_name) + def test_timeout_propagation(self): + class OkException(Exception): + pass + + class MockRequest(Request): + def post(self, url, data, timeout=None): + raise OkException(timeout) + + _request = self._bot._request + self._bot._request = MockRequest() + + timeout = 500 + + with self.assertRaises(OkException) as ok: + self._bot.send_photo(self._chat_id, open('tests/data/telegram.jpg'), timeout=timeout) + self.assertEqual(ok.exception.args[0], timeout) + + self._bot._request = _request if __name__ == '__main__': unittest.main() From d5583190b87ceed76dca38d39d82d95ca9224b8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jannes=20H=C3=B6ke?= Date: Sun, 18 Jun 2017 12:35:16 +0200 Subject: [PATCH 3/4] Bump version to v6.1.0 --- CHANGES.rst | 8 ++++++++ README.rst | 8 +------- docs/source/conf.py | 4 ++-- telegram/version.py | 2 +- 4 files changed, 12 insertions(+), 10 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 978ee720c..abdad2ab6 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -2,6 +2,14 @@ Changes ======= +**2017-06-18** + +*Released 6.1.0* + +- Fully support Bot API 3.0 +- Add more fine-grained filters for status updates +- Bug fixes and other improvements + **2017-05-29** *Released 6.0.3* diff --git a/README.rst b/README.rst index 8c7a8a6af..2dd6e4cee 100644 --- a/README.rst +++ b/README.rst @@ -84,13 +84,7 @@ make the development of bots easy and straightforward. These classes are contain Telegram API support ==================== -As of **21. May 2017**, all types and methods of the Telegram Bot API 2.3.1 are supported. Additionally, the ``deleteMessage`` API function and the field ``User.language_code`` are supported. - -Also, version 6.1 beta 0 is available, offering full but experimental Bot API 3.0 coverage: - -.. code:: shell - - $ pip install python-telegram-bot==6.1b0 +As of **18. June 2017**, all types and methods of the Telegram Bot API 3.0 are supported. ========== Installing diff --git a/docs/source/conf.py b/docs/source/conf.py index a41e4a998..75b99841b 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -58,9 +58,9 @@ author = u'Leandro Toledo' # built documents. # # The short X.Y version. -version = '6.0' # telegram.__version__[:3] +version = '6.1' # telegram.__version__[:3] # The full version, including alpha/beta/rc tags. -release = '6.0.3' # telegram.__version__ +release = '6.1.0' # telegram.__version__ # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/telegram/version.py b/telegram/version.py index ffd7b3ea0..33ba74459 100644 --- a/telegram/version.py +++ b/telegram/version.py @@ -17,4 +17,4 @@ # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. -__version__ = '6.0.3' +__version__ = '6.1.0' From eee0f78b1559d189d8ef58bcb94fc00d1ff0bf03 Mon Sep 17 00:00:00 2001 From: Eldinnie Date: Tue, 20 Jun 2017 21:45:49 +0200 Subject: [PATCH 4/4] Add appveyor.yml (#660) [ci skip] --- appveyor.yml | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 appveyor.yml diff --git a/appveyor.yml b/appveyor.yml new file mode 100644 index 000000000..f0d71990e --- /dev/null +++ b/appveyor.yml @@ -0,0 +1,29 @@ +environment: + + matrix: + # For Python versions available on Appveyor, see + # http://www.appveyor.com/docs/installed-software#python + # The list here is complete (excluding Python 2.6, which + # isn't covered by this document) at the time of writing. + + - PYTHON: "C:\\Python27" + - PYTHON: "C:\\Python33" + - PYTHON: "C:\\Python34" + - PYTHON: "C:\\Python35" + - PYTHON: "C:\\Python36" + +install: + # We need wheel installed to build wheels + - "git submodule update --init --recursive" + - "%PYTHON%\\python.exe -m pip install wheel" + - "%PYTHON%\\python.exe -m pip install -r requirements.txt" + - "%PYTHON%\\python.exe -m pip install -r requirements-dev.txt" + +build: off + +test_script: + - "%python%\\Scripts\\nosetests -v --with-flaky --no-flaky-report tests" + +after_test: + # This step builds your wheels. + - "%PYTHON%\\python.exe setup.py bdist_wheel"