Repository: python-telegram-bot/python-telegram-bot

Issue:
Weirdness with Bot._message_wrapper
We have some weirdness and inconsistencies going on with how we use the `_message_wrapper` in `Bot`.
Some `send_*` methods use the `@message` decorator, others, however, use the `_message_wrapper` directly. And the way that they use it seems quite frankly, wrong to me. Take `send_photo` as an example.

``` python
url = '{0}/sendPhoto'.format(self.base_url)

data = {'chat_id': chat_id, 'photo': photo}

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)
```
To me it seems like at least `chat_id`,  `photo`, and `caption` are totally redundant to have as kwargs too, given they are part of the data already.

I propose we figure either use the decorator everywhere (best and cleanest solution in my head) or we scrap it and use the `_message_wrapper` everywhere (and figure out if we really need all those extra kwargs).


Discussion:

Resolving patch:
diff --git a/telegram/bot.py b/telegram/bot.py
--- a/telegram/bot.py
+++ b/telegram/bot.py
@@ -31,10 +31,45 @@
 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 @@ def _validate_token(token):
 
         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 @@ def username(self):
     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 @@ def _message_wrapper(self, url, data, *args, **kwargs):
 
         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 @@ def send_message(self,
         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 @@ def delete_message(self, chat_id, message_id):
                 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 @@ def delete_message(self, chat_id, message_id):
 
         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 @@ def forward_message(self,
         return url, data
 
     @log
+    @message
     def send_photo(self,
                    chat_id,
                    photo,
@@ -356,19 +363,10 @@ def send_photo(self,
         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 @@ def send_audio(self,
         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 @@ def send_document(self,
         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 @@ def send_sticker(self,
         return url, data
 
     @log
+    @message
     def send_video(self,
                    chat_id,
                    video,
@@ -595,20 +571,10 @@ def send_video(self,
         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 @@ def send_voice(self,
         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 @@ def send_video_note(self,
         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 @@ def send_game(self,
                 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 @@ def send_game(self,
             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 @@ def send_game(self,
         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 @@ def send_chat_action(self, chat_id, action, timeout=None, **kwargs):
 
         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 @@ def get_webhook_info(self, timeout=None, **kwargs):
             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 @@ def get_webhook_info(self, timeout=None, **kwargs):
 
         return WebhookInfo.de_json(result, self)
 
+    @log
+    @message
     def set_game_score(self,
                        user_id,
                        score,
@@ -1743,6 +1691,7 @@ def set_game_score(self,
             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 @@ def set_game_score(self,
             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 @@ def get_game_high_scores(self,
             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 @@ def send_invoice(self,
 
         return url, data
 
+    @log
     def answer_shipping_query(self,
                               shipping_query_id,
                               ok,
@@ -1950,6 +1898,9 @@ def answer_shipping_query(self,
                 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:
@@ -1983,6 +1934,7 @@ def answer_shipping_query(self,
 
         return result
 
+    @log
     def answer_pre_checkout_query(self, pre_checkout_query_id, ok,
                                   error_message=None, timeout=None, **kwargs):
         """
@@ -1999,6 +1951,9 @@ def answer_pre_checkout_query(self, pre_checkout_query_id, ok,
                 "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
--- a/telegram/utils/request.py
+++ b/telegram/utils/request.py
@@ -224,7 +224,7 @@ def post(self, url, data, timeout=None):
         """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).


Test patch:
diff --git a/tests/test_bot.py b/tests/test_bot.py
--- a/tests/test_bot.py
+++ b/tests/test_bot.py
@@ -31,6 +31,7 @@
 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 @@ def test_send_contact(self):
         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()