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

Issue:
Using InlineKeyboardMarkup with one button without 2D list
<!--
Thanks for reporting issues of python-telegram-bot!

Use this template to notify us if you found a bug, or if you want to request a new feature.
If you're looking for help with programming your bot using our library, feel free to ask your
questions in out telegram group at: https://t.me/pythontelegrambotgroup

To make it easier for us to help you please enter detailed information below.

Please note, we only support the latest version of python-telegram-bot and
master branch. Please make sure to upgrade & recreate the issue on the latest
version prior to opening an issue.
-->
### Steps to reproduce
```
reply_markup = InlineKeyboardMarkup(InlineKeyboardButton(text="TEST_BUTTON", url="t.me/TEST"))
bot.send_message(chat_id=chat_id, text="TEST_MESSAGE", reply_markup=reply_markup)
```

### Expected behaviour
A message appears with the TEST_MESSAGE text and the TEST_BUTTON button
![image](https://user-images.githubusercontent.com/8740496/44001093-f87eaf7c-9e33-11e8-83b4-12e31357df8a.png)

### Actual behaviour
`KeyError: 0` because the markup needs a compulsory of a list within a list (even for one button).

### Configuration
**Operating System:** Win10 1803
**Version of Python, python-telegram-bot & dependencies:**
```
python-telegram-bot 10.1.0
certifi 2018.01.18
future 0.16.0
Python 3.6.2
```
### Logs
```
2018-08-12 12:42:19,317 - telegram.ext.dispatcher - ERROR - An uncaught error was raised while processing the update
Traceback (most recent call last):
  File "E:\python362\lib\site-packages\telegram\ext\dispatcher.py", line 279, in process_update
    handler.handle_update(update, self)
  File "E:\python362\lib\site-packages\telegram\ext\commandhandler.py", line 173, in handle_update
    return self.callback(dispatcher.bot, update, **optional_args)
  File "E:\bobermod\bot.py", line 17, in start
    bot.send_message(chat_id=update.message.chat_id, text="TEST_MESSAGE", reply_markup=reply_markup)
  File "E:\python362\lib\site-packages\telegram\bot.py", line 60, in decorator
    result = func(self, *args, **kwargs)
  File "E:\python362\lib\site-packages\telegram\bot.py", line 81, in decorator
    data['reply_markup'] = reply_markup.to_json()
  File "E:\python362\lib\site-packages\telegram\base.py", line 57, in to_json
    return json.dumps(self.to_dict())
  File "E:\python362\lib\site-packages\telegram\inline\inlinekeyboardmarkup.py", line 47, in to_dict
    for inline_keyboard in self.inline_keyboard:
  File "E:\python362\lib\site-packages\telegram\base.py", line 39, in __getitem__
    return self.__dict__[item]
KeyError: 0
```
### Improvement
It can be fixed by using list within a list:
`reply_markup = InlineKeyboardMarkup([[InlineKeyboardButton(text="TEST_BUTTON", url="t.me/TEST")]])`
**But it would be nice if it could be used with a single button without extra lists.**


Discussion:
@python-telegram-bot/maintainers Do we want this?
It should support:
```
List[List[Button]]
List[Button]
Button
```
then. I personally think it;s not needed.
technically we don't mandate a list today, it can also be a generator if i recall correctly.
if i'm right - i vote to keep this functionality.
if i'm wrong - i like the 
```
List[List[Button]]
List[Button]
Button
```
suggestion

another approach would be:
```
InlineKeyboard.from_button(button)
InlineKeyboard.from_single_line(line: List[Button])
```
both will be class methods which will call `__init__()` with proper parameters.

Resolving patch:
diff --git a/telegram/inline/inlinekeyboardmarkup.py b/telegram/inline/inlinekeyboardmarkup.py
--- a/telegram/inline/inlinekeyboardmarkup.py
+++ b/telegram/inline/inlinekeyboardmarkup.py
@@ -48,3 +48,51 @@ def to_dict(self):
             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
--- a/telegram/replykeyboardmarkup.py
+++ b/telegram/replykeyboardmarkup.py
@@ -85,3 +85,125 @@ def to_dict(self):
                     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)


Test patch:
diff --git a/tests/test_inlinekeyboardmarkup.py b/tests/test_inlinekeyboardmarkup.py
--- a/tests/test_inlinekeyboardmarkup.py
+++ b/tests/test_inlinekeyboardmarkup.py
@@ -44,6 +44,27 @@ def test_send_message_with_inline_keyboard_markup(self, bot, chat_id, inline_key
 
         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
--- a/tests/test_replykeyboardmarkup.py
+++ b/tests/test_replykeyboardmarkup.py
@@ -51,6 +51,40 @@ def test_send_message_with_data_markup(self, bot, chat_id):
 
         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)