Skip to content

Commit a82c398

Browse files
committed
Initial commit, implement base logic
1 parent 38fa93e commit a82c398

File tree

7 files changed

+1398
-42
lines changed

7 files changed

+1398
-42
lines changed

ptbcontrib/extended_keyboards/README.md

Lines changed: 199 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,22 @@
11
# Extra functionality for PTB keyboards
22

3-
### This module contains extended keyboard classes with extra functionality for PTB keyboards.
3+
These modules contains extended keyboard classes with extra functionality for PTB keyboards.
44

5-
### Methods is self-descriptive.
5+
## Module `base keyboards`
6+
7+
This module provides simple base actions that mostly intended to be used as base class for other extend keyboards.
8+
For example currently `select keyboards` made independently
9+
and occasionally repeats the functionality present in this base module.
610

711
```python
812
class IExtendedInlineKeyboardMarkup(ABC, ):
913
""" Popular keyboard actions """
1014

1115
def to_list(self, ) -> list[list[InlineKeyboardButton]]:
12-
16+
1317
def find_btn_by_cbk(self, cbk: str, ) -> tuple[InlineKeyboardButton, int, int] | None:
1418
""" Returns buttons, row index, column index if found, None otherwise """
15-
19+
1620
def get_buttons(self, ) -> list[InlineKeyboardButton]:
1721
""" Just get flat list of buttons of the keyboard """
1822

@@ -35,10 +39,199 @@ class IExtendedInlineKeyboardMarkup(ABC, ):
3539
"""
3640
```
3741

42+
Popular keyboard actions
43+
```python
44+
45+
46+
def to_list(self, ) -> list[list[InlineKeyboardButton]]:
47+
48+
49+
def find_btn_by_cbk(self, cbk: str, ) -> tuple[InlineKeyboardButton, int, int] | None:
50+
""" Returns buttons, row index, column index if found, None otherwise """
51+
52+
53+
def get_buttons(self, ) -> list[InlineKeyboardButton]:
54+
""" Just get flat list of buttons of the keyboard """
55+
56+
57+
def split(
58+
self,
59+
buttons_in_row: int,
60+
buttons: Sequence[InlineKeyboardButton] | None = None,
61+
update_self: bool = True,
62+
empty_rows_allowed: bool = True,
63+
strict: bool = False,
64+
) -> list[list[InlineKeyboardButton]]:
65+
"""
66+
Split keyboard by N buttons in row.
67+
Last row will contain remainder,
68+
i.e. num of buttons in the last row maybe less than `buttons_in_row` parameter.
69+
70+
Possible enhancement:
71+
keep_empty_rows: bool - keep empty rows in final keyboard if not enough buttons.
72+
# Please create feature issue if you need it.
73+
"""
74+
```
75+
76+
## Module `select keyboards`
77+
78+
This module implement checkbox buttons for PTB inline keyboard
79+
80+
### Class `SelectKeyboard`:
81+
82+
Let's first create the keyboard:
83+
84+
```python
85+
SelectKeyboard(
86+
inline_keyboard=((InlineKeyboardButton(...))),
87+
checkbox_position=0,
88+
checked_symbol='+',
89+
unchecked_symbol='-',
90+
)
91+
```
92+
93+
`SelectKeyboard` inherits from `InlineKeyboardMarkup`and therefore not differ from it
94+
and may be used as drop in replacement.
95+
The one more explicit meaning of keyboard is container (and therefore kind of manager or arbiter) for his buttons.
96+
97+
#### Use cases
98+
99+
You got the incoming callback with inline reply markup (buttons will be also converted if possible):
100+
```python
101+
select_inline_keyboard = SelectKeyboard.from_callback(keyboard=inline_keyboard_markup.inline_keyboard, )
102+
```
103+
104+
Let's go even forward - and directly update the selected button in one line:
105+
```python
106+
select_inline_keyboard = SelectKeyboard.invert_by_callback(keyboard=inline_keyboard_markup.inline_keyboard, )
107+
```
108+
109+
Or if clicked "select all" option:
110+
```python
111+
keyboard = SelectKeyboard.set_all_buttons(keyboard=inline_keyboard, flag=True, )
112+
```
113+
114+
Check is `InlineKeyboardMarkup` can be converted to any known button type of the keyboard
115+
```python
116+
button = SelectKeyboard.is_convertable(button=inline_button, )
117+
```
118+
119+
Check is `InlineKeyboardMarkup` can be converted to any known button type of the keyboard
120+
```python
121+
button = SelectKeyboard.button_from_inline(button=inline_button, )
122+
```
123+
124+
125+
### Class `SelectButtons:`:
126+
127+
```python
128+
select_button = SelectButton(
129+
is_selected=True, # is_selected: Initial state of the button
130+
checkbox_position=0, # checkbox_position: Position of the checkbox in the text
131+
checked_symbol='+', # checked_symbol: Symbol to use when the button is selected
132+
unchecked_symbol='-', # unchecked_symbol: Symbol to use when the button is not selected
133+
text="Hi! I'm the button which will contain selection symbol after init", # text: Button text
134+
callback_data='...',
135+
... # Other `InlineKeyboardButton` regular parameters
136+
)
137+
```
138+
139+
Class `SelectButton` represents checkbox button and provides convenience methods to manage button state.
140+
(note: button state is immutable according to PTB objects management policy, so most of the methods returns new state).
141+
It's also inherits from `InlineKeyboardButton` and may be used interchangeably with it.
142+
After creation the button will contain callback data in format:
143+
`'original callback SELECT_BTN 0 + - 1'`
144+
145+
Which means:
146+
- `'SELECT_BTN'` - key to mark button as select button.
147+
- `'0'` - position of the select symbol in the string.
148+
- `'+'` - symbol to apply on checking.
149+
- `'-'` - symbol to apply on unchecking.
150+
- `'1'` - selected state (0 - deselected).
151+
152+
153+
Invert button state (text and callback_data) from `selected` to `deselected` and vice versa:
154+
```python
155+
inverted_button = select_button.invert()
156+
```
157+
Check the button state is selected:
158+
(Note: that is the property bound to callback)
159+
```python
160+
select_button.is_selected # True or False
161+
```
162+
163+
#### Use cases
164+
165+
Convert `InlineKeyboardButton` to `SelectButton` (To check is button convertable - use `is_convertable` method)
166+
```python
167+
select_button = SelectButton.from_inline_button(button=inline_keyboard_button, )
168+
```
169+
170+
Is button is select button at all? - This will look up for an appropriate callback data
171+
which has `'SELECT_BTN'` pattern
172+
```python
173+
select_button = SelectButton.is_convertable(button=inline_keyboard_button, )
174+
```
175+
176+
### More buttons:
177+
178+
#### class `SelectButtonManager`:
179+
What if `is_selected` parameter depend on the other buttons just as for "select all" button
180+
which are selected when all points selected? That's what `SelectButtonManager` type mean,
181+
every child of it should implement `resolve_is_selected` method.
182+
183+
`SelectAllButton` is child of `SelectButtonManager`
184+
It's similar to `SelectButton` and also inherits from `InlineKeyboardButton`,
185+
so the button itself will decide on her state.
186+
```python
187+
keyboard = SelectAllButton.resolve_is_selected(keyboard=inline_keyboard, ) # True or False
188+
```
189+
190+
Let's update button if `resolve_is_selected` returned opposite state:
191+
```python
192+
updated_select_all_button = select_all_button.update(keyboard=inline_keyboard, ) # just calling `invert` inside
193+
```
194+
195+
#### class`SimpleButton:`:
196+
Specifying the same `checked_symbol`, `unchecked_symbol`,
197+
etc. for every button may be tedious, so there are 2 workarounds:
198+
1. Create common class with overriding `checked_symbol`, `unchecked_symbol`:
199+
`class MySelectButtton: checked_symbol = 'Ha-ha!'`
200+
2. Use `SimpleSelectButton` - this button type will tell to keyboard to use the keyboard parameters of
201+
`checked_symbol`, `unchecked_symbol` rather than the button:
202+
```python
203+
SimpleSelectButton(is_selected=True, cls=SelectButton, ...other button fields)
204+
```
205+
`SimpleSelectButton` has only two purposes: it's a delayed button creation and the indicator.
206+
207+
### Common architecture notes:
208+
1. All types may be divided on 2 parts: creation and parsing or extracting.
209+
`SimpleButton`, `ManagerButton` is about creation, but eventually (see point 2):
210+
2. select button representation expressed via his callback is common or similar for all buttons:
211+
'.* ({SELECT_BTN_S}) (\d+) (\S+) (\S+) ([01])$', so it's sing;e finish point of every button.
212+
3. During initialization, we need a structs mostly, most methods intended to handle already initialized objects.
213+
4. Three rings of responsibility:
214+
1. Select button - target of all modifications.
215+
2. Button manager - strategy | logic of manipulation.
216+
3. Keyboard - applying and providing context to manipulate by manager on the button.
217+
5. Term `keyboard` may mean inline keyboard (nested list) or `InlineKeyboardMarkup`,
218+
this depends on context and will be stick to single meaning when will be more clearing of case usage.
219+
6. May be `SimpleButton` may be dropped and just check is select button fields filled with `None` or not
220+
(need to make them optional in this case).
221+
222+
223+
Future improvements thoughts:
224+
1. Create `Checkbox` class, to allow `SelectButton` able to have separate checkboxes for different states.
225+
2. Make more states rather than just `True` | `False`.
226+
3. Make checkboxes of different sides (actual for 2 column keyboards), similar as point 1 but easiest as keyboard twik
227+
4. Add Some simple set oj emojy checked | unchecked symbols.
228+
5. Increase decoupling between keyboard and buttons, improve architecture.
229+
6. Add `eject` method to divide between
230+
38231
## Requirements
39232

40-
* `python-telegram-bot>=20.0`
233+
* `python-telegram-bot>=20.0`
41234

42235
## Authors
43236

44-
* [David Shiko](https://github.com/david-shiko)
237+
* [David Shiko](https://github.com/david-shiko)

ptbcontrib/extended_keyboards/__init__.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,35 @@
1616
# along with this program. If not, see [http://www.gnu.org/licenses/].
1717
"""This module contains extended keyboard classes with extra functionality for PTB keyboards."""
1818

19-
from .keyboards import ExtendedInlineKeyboardMarkup
19+
from .base_keyboards import ExtendedInlineKeyboardMarkup, IExtendedInlineKeyboardMarkup
20+
from .select_keyboards import (
21+
Exceptions,
22+
IkbStruct,
23+
SelectAllButton,
24+
SelectButton,
25+
SelectButtonBase,
26+
SelectButtonBaseFields,
27+
SelectButtonManager,
28+
SelectButtonStruct,
29+
SelectKeyboard,
30+
SimpleButton,
31+
SimpleButtonBase,
32+
SimpleButtonManager,
33+
)
2034

2135
__all__ = [
36+
"IExtendedInlineKeyboardMarkup",
2237
"ExtendedInlineKeyboardMarkup",
38+
"Exceptions",
39+
"SelectButtonBaseFields",
40+
"IkbStruct",
41+
"SelectButtonStruct",
42+
"SelectButtonBase",
43+
"SelectButton",
44+
"SelectButtonManager",
45+
"SelectAllButton",
46+
"SimpleButtonBase",
47+
"SimpleButton",
48+
"SimpleButtonManager",
49+
"SelectKeyboard",
2350
]

ptbcontrib/extended_keyboards/keyboards.py renamed to ptbcontrib/extended_keyboards/base_keyboards.py

Lines changed: 33 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,37 @@
3030
pass
3131

3232

33+
class Exceptions:
34+
"""Just a data container"""
35+
36+
class NotEnoughButtons(
37+
Exception,
38+
):
39+
"""Exception raised when there are not enough buttons to fill the keyboard."""
40+
41+
def __init__(
42+
self,
43+
inline_keyboard: tuple[tuple[InlineKeyboardButton, ...], ...],
44+
buttons: list[InlineKeyboardButton,],
45+
buttons_in_row: int,
46+
) -> None:
47+
super().__init__(
48+
"Total count of buttons not enough to fill the keyboard by equal rows "
49+
f"({len(buttons)} collected and "
50+
f"{buttons_in_row * len(inline_keyboard, )} required."
51+
)
52+
53+
class EmptyRowsDisallowed(
54+
Exception,
55+
):
56+
"""Exception raised when empty rows are not allowed in the keyboard."""
57+
58+
def __init__(
59+
self,
60+
) -> None:
61+
super().__init__("Result num of rows less that original num of rows in the keyboard.")
62+
63+
3364
class IExtendedInlineKeyboardMarkup(
3465
ABC,
3566
):
@@ -79,38 +110,6 @@ class ExtendedInlineKeyboardMarkup(
79110
):
80111
"""Popular keyboard actions"""
81112

82-
class Exceptions:
83-
"""Just a data container"""
84-
85-
class NotEnoughButtons(
86-
Exception,
87-
):
88-
"""Exception raised when there are not enough buttons to fill the keyboard."""
89-
90-
def __init__(
91-
self,
92-
inline_keyboard: tuple[tuple[InlineKeyboardButton, ...], ...],
93-
buttons: list[InlineKeyboardButton,],
94-
buttons_in_row: int,
95-
) -> None:
96-
super().__init__(
97-
"Total count of buttons not enough to fill the keyboard by equal rows "
98-
f"({len(buttons)} collected and "
99-
f"{buttons_in_row * len(inline_keyboard, )} required."
100-
)
101-
102-
class EmptyRowsDisallowed(
103-
Exception,
104-
):
105-
"""Exception raised when empty rows are not allowed in the keyboard."""
106-
107-
def __init__(
108-
self,
109-
) -> None:
110-
super().__init__(
111-
"Result num of rows less that original num of rows in the keyboard."
112-
)
113-
114113
def to_list(
115114
self,
116115
) -> list[list[InlineKeyboardButton]]:
@@ -172,7 +171,7 @@ def split(
172171
if strict and len(buttons) < buttons_in_row * len(
173172
self.inline_keyboard,
174173
):
175-
raise self.Exceptions.NotEnoughButtons(
174+
raise Exceptions.NotEnoughButtons(
176175
inline_keyboard=self.inline_keyboard,
177176
buttons=buttons,
178177
buttons_in_row=buttons_in_row,
@@ -185,6 +184,6 @@ def split(
185184
if not empty_rows_allowed and len(new_keyboard) < len(
186185
self.inline_keyboard,
187186
):
188-
raise self.Exceptions.EmptyRowsDisallowed
187+
raise Exceptions.EmptyRowsDisallowed
189188

190189
return new_keyboard

0 commit comments

Comments
 (0)