88from discord .ui import ActionRow , LayoutView , Select , View
99from discord .ui .item import ContainedItemCallbackType
1010from discord .ui .select import SelectCallbackDecorator
11+ from githubkit import Response
1112
13+ from ghutils .utils .github import is_last_page
1214from ghutils .utils .types import AsyncCallable
1315
1416logger = logging .getLogger (__name__ )
1719PREVIOUS_PAGE_VALUE = "e4215656-23ba-4a3d-8386-795778944b4b"
1820NEXT_PAGE_VALUE = "1e1f5d4c-e908-42cf-8d6c-8b66aadf0998"
1921
20- MAX_PAGE_LENGTH = 23
22+ MAX_PER_PAGE = 23
2123
2224
2325type PageGetter [V : View | LayoutView ] = AsyncCallable [
@@ -30,6 +32,7 @@ class PaginatedSelect[V: View | LayoutView](Select[V]):
3032 _inner_callback : ContainedItemCallbackType [V | ActionRow [Any ], Self ] | None
3133 _page_getter : PageGetter [V ] | None
3234 _page_cache : dict [int , list [SelectOption ]]
35+ _placeholder : str | None
3336
3437 _page : int
3538 _selected_page : int | None
@@ -39,18 +42,22 @@ def __init__(
3942 self ,
4043 * ,
4144 custom_id : str = MISSING ,
42- placeholder : None = None ,
45+ placeholder : str | None = None ,
4346 min_values : Literal [0 , 1 ] = 1 ,
4447 max_values : Literal [0 , 1 ] = 1 ,
4548 options : list [SelectOption ] = MISSING ,
4649 disabled : bool = False ,
47- required : bool = True ,
50+ required : bool = MISSING ,
4851 row : int | None = None ,
4952 id : int | None = None ,
5053 inner_callback : ContainedItemCallbackType [V | ActionRow [Any ], Self ]
5154 | None = None ,
5255 page_getter : PageGetter [V ] | None = None ,
5356 ) -> None :
57+ # https://github.com/Rapptz/discord.py/issues/10291
58+ if required is MISSING :
59+ required = min_values > 0
60+
5461 super ().__init__ (
5562 custom_id = custom_id ,
5663 placeholder = placeholder ,
@@ -70,6 +77,7 @@ def __init__(
7077 self ._inner_callback = inner_callback
7178 self ._page_getter = page_getter
7279 self ._page_cache = {}
80+ self ._placeholder = placeholder
7381 self ._page = 1
7482 self ._selected_page = None
7583 self ._selected_index = None
@@ -83,6 +91,21 @@ async def fetch_first_page(self, interaction: Interaction):
8391 self ._page_cache .pop (1 , None )
8492 await self ._switch_to_page (interaction , 1 )
8593
94+ def set_last_page (self , page : int , response : Response [Any ] | None = None ) -> bool :
95+ """Set the final page. The page after this one is assumed to be empty.
96+
97+ Page getters can use this if they're returning a page with less than 23 options
98+ that they know in advance is the last page, to prevent the select menu from
99+ adding a spurious "next page" option to that page.
100+
101+ If a GitHub response is given, the pagination headers are checked
102+ to see if this is the last page or not.
103+ """
104+ if response is not None and not is_last_page (response ):
105+ return False
106+ self ._page_cache [page + 1 ] = []
107+ return True
108+
86109 def clear_cached_pages (self ):
87110 self ._page_cache .clear ()
88111
@@ -117,9 +140,9 @@ def options(self) -> list[SelectOption]:
117140 @options .setter
118141 @override
119142 def options (self , value : list [SelectOption ]):
120- if len (value ) > MAX_PAGE_LENGTH :
143+ if len (value ) > MAX_PER_PAGE :
121144 raise ValueError (
122- f"Pages must not contain more than { MAX_PAGE_LENGTH } options (got { len (value )} )"
145+ f"Pages must not contain more than { MAX_PER_PAGE } options (got { len (value )} )"
123146 )
124147
125148 assert Select .options .fset is not None
@@ -138,14 +161,14 @@ def options(self, value: list[SelectOption]):
138161 if self .options and (
139162 self .options [0 ].value == PREVIOUS_PAGE_VALUE
140163 or self .options [- 1 ].value == NEXT_PAGE_VALUE
141- or len (self .options ) > MAX_PAGE_LENGTH
164+ or len (self .options ) > MAX_PER_PAGE
142165 ):
143166 return
144167
145168 # NOTE: we need to check this *before* mutating self.options
146169 if (
147170 # only allow going to the next page if the current page is full
148- len (self .options ) == MAX_PAGE_LENGTH
171+ len (self .options ) == MAX_PER_PAGE
149172 # and either the next page has values or we haven't fetched it yet
150173 and self ._page_cache .get (self ._page + 1 , True )
151174 ):
@@ -254,9 +277,9 @@ async def _switch_to_page(self, interaction: Interaction, page: int):
254277 assert self ._page_getter
255278 assert self .view
256279 options = await self ._page_getter (self .view , interaction , self , page )
257- if len (options ) > MAX_PAGE_LENGTH :
280+ if len (options ) > MAX_PER_PAGE :
258281 raise ValueError (
259- f"Pages must not contain more than { MAX_PAGE_LENGTH } options (got { len (options )} )"
282+ f"Pages must not contain more than { MAX_PER_PAGE } options (got { len (options )} )"
260283 )
261284 self ._page_cache [page ] = options
262285
@@ -279,10 +302,10 @@ async def _switch_to_page(self, interaction: Interaction, page: int):
279302 0 if self ._selected_page < page else - 1
280303 ].description = f"(selected on page { self ._selected_page } )"
281304 else :
282- self .placeholder = None
305+ self .placeholder = self . _placeholder
283306
284307 def _clear_remote_page_selection (self ):
285- self .placeholder = None
308+ self .placeholder = self . _placeholder
286309
287310 if self .options [0 ].value == PREVIOUS_PAGE_VALUE :
288311 self .options [0 ].description = None
@@ -298,6 +321,7 @@ def paginated_select[
298321 * ,
299322 options : list [SelectOption ] = MISSING ,
300323 custom_id : str = MISSING ,
324+ placeholder : str | None = None ,
301325 min_values : Literal [0 , 1 ] = 1 ,
302326 max_values : Literal [0 , 1 ] = 1 ,
303327 disabled : bool = False ,
@@ -308,13 +332,21 @@ def decorator(inner_callback: ContainedItemCallbackType[Any, Any]) -> SelectT:
308332 select = PaginatedSelect [Any ](
309333 options = options ,
310334 custom_id = custom_id ,
335+ placeholder = placeholder ,
311336 min_values = min_values ,
312337 max_values = max_values ,
313338 disabled = disabled ,
314339 row = row ,
315340 id = id ,
316341 inner_callback = inner_callback ,
317342 )
343+
344+ # hack: View only adds items as children if __discord_ui_model_type__ is set
345+ def model_type (* args : Any , ** kwargs : Any ):
346+ raise AssertionError ("This should never be called" )
347+
348+ setattr (select , "__discord_ui_model_type__" , model_type )
349+
318350 return select # pyright: ignore[reportReturnType]
319351
320352 return decorator
0 commit comments