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

Issue:
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_


Discussion:

Resolving patch:
diff --git a/telegram/_utils/markup.py b/telegram/_utils/markup.py
--- 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,7 +39,11 @@ def check_keyboard_type(keyboard: object) -> bool:
     # allow it here.
     if not isinstance(keyboard, Sequence) or isinstance(keyboard, (str, bytes)):
         return False
+
     for row in keyboard:
         if not isinstance(row, Sequence) or isinstance(row, (str, bytes)):
             return False
+        for inner in row:
+            if isinstance(inner, Sequence) and not isinstance(inner, str):
+                return False
     return True


Test patch:
diff --git a/tests/test_inlinekeyboardmarkup.py b/tests/test_inlinekeyboardmarkup.py
--- 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
--- 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()