Repository: python-telegram-bot/python-telegram-bot
Pull request: add check if keyboard has more than 2 dimensions
Closes #3511 

Problem statement:
Don't allow more than 2 dimensional input for `Inline/ReplyKeyboardMarkup`
### What kind of feature are you missing? Where do you notice a shortcoming of PTB?

When a user passes their list to `Inline/ReplyKeyboardMarkup`, we currently check if their input is valid (exactly 2 dimensional input is valid), i.e. 

* the input is a sequence, not just a single button
* it's not just a simple sequence (sequence of buttons)

However it doesn't check if it's more than 2D, i.e. `[[[KeyboardButton(...)], [...], ]]` is invalid.

### Describe the solution you'd like

Modify `tg._utils.markup.check_keyboard_type` to return `False` if we find another sequence.

### Describe alternatives you've considered

_No response_

### Additional context

_No response_

Hints from issue discussion:

Commit message: remove extra dimension

Patch to review:
diff --git a/telegram/_utils/markup.py b/telegram/_utils/markup.py
index 97068df924d..291c3ed6220 100644
--- a/telegram/_utils/markup.py
+++ b/telegram/_utils/markup.py
@@ -31,7 +31,7 @@ class ``telegram.ReplyMarkup``.
 
 
 def check_keyboard_type(keyboard: object) -> bool:
-    """Checks if the keyboard provided is of the correct type - A list of lists.
+    """Checks if the keyboard provided is of the correct type - A sequence of sequences.
     Implicitly tested in the init-tests of `{Inline, Reply}KeyboardMarkup`
     """
     # string and bytes may actually be used for ReplyKeyboardMarkup in which case each button
@@ -39,6 +39,11 @@ def check_keyboard_type(keyboard: object) -> bool:
     # allow it here.
     if not isinstance(keyboard, Sequence) or isinstance(keyboard, (str, bytes)):
         return False
+
+    # check whether the keyboard has more than two dimensions.
+    if isinstance(keyboard[0][0], Sequence) and not isinstance(keyboard[0][0], (str, bytes)):
+        return False
+
     for row in keyboard:
         if not isinstance(row, Sequence) or isinstance(row, (str, bytes)):
             return False
diff --git a/tests/test_inlinekeyboardmarkup.py b/tests/test_inlinekeyboardmarkup.py
index 911673aadb5..7f0301d219f 100644
--- a/tests/test_inlinekeyboardmarkup.py
+++ b/tests/test_inlinekeyboardmarkup.py
@@ -101,6 +101,8 @@ def test_wrong_keyboard_inputs(self):
             InlineKeyboardMarkup(["strings_are_not_allowed_in_the_rows_either"])
         with pytest.raises(ValueError):
             InlineKeyboardMarkup(InlineKeyboardButton("b1", "1"))
+        with pytest.raises(ValueError):
+            InlineKeyboardMarkup([[[InlineKeyboardButton("only_2d_array_is_allowed", "1")]]])
 
     async def test_expected_values_empty_switch(self, inline_keyboard_markup, bot, monkeypatch):
         async def make_assertion(
diff --git a/tests/test_replykeyboardmarkup.py b/tests/test_replykeyboardmarkup.py
index acfadd46cb3..0f3badfa1d7 100644
--- a/tests/test_replykeyboardmarkup.py
+++ b/tests/test_replykeyboardmarkup.py
@@ -116,6 +116,8 @@ def test_wrong_keyboard_inputs(self):
             ReplyKeyboardMarkup(["strings_are_not_allowed_in_the_rows_either"])
         with pytest.raises(ValueError):
             ReplyKeyboardMarkup(KeyboardButton("button1"))
+        with pytest.raises(ValueError):
+            ReplyKeyboardMarkup([[["button1"]]])
 
     def test_to_dict(self, reply_keyboard_markup):
         reply_keyboard_markup_dict = reply_keyboard_markup.to_dict()


Code review comments:
diff_hunk: @@ -31,14 +31,19 @@ class ``telegram.ReplyMarkup``.
 
 
 def check_keyboard_type(keyboard: object) -> bool:
-    """Checks if the keyboard provided is of the correct type - A list of lists.
+    """Checks if the keyboard provided is of the correct type - A sequence of sequences.
     Implicitly tested in the init-tests of `{Inline, Reply}KeyboardMarkup`
     """
     # string and bytes may actually be used for ReplyKeyboardMarkup in which case each button
     # would contain a single character. But that use case should be discouraged and we don't
     # allow it here.
     if not isinstance(keyboard, Sequence) or isinstance(keyboard, (str, bytes)):
         return False
+
+    # check whether the keyboard has more than two dimensions.
+    if isinstance(keyboard[0][0], Sequence) and not isinstance(keyboard[0][0], (str, bytes)):
path: telegram/_utils/markup.py
text: @user1:
At this point we haven't yet established that the rows are sequences as well. `check_keyboard_type([1])` would raise an exception with this addition IISC …