4 Commits
Author SHA1 Message Date
dsql de572af675 docs: pin install line to release, note unpinned-latest option
Signed-off-by: disqualifier <dev@disqualifier.me>
2026-06-29 18:13:51 -04:00
dsql 234f663f04 docs: show unpinned install line; note tag-pinning for reproducibility
Signed-off-by: disqualifier <dev@disqualifier.me>
2026-06-29 18:07:36 -04:00
dsql 8c3bacb2f2 fix: reject wrong-typed embed/file page keys; author_id is-None check (v0.1.3)
- a dict page with a wrong-typed embed/embeds/file/files key now raises a clear
  paginator-side ValueError instead of forwarding it to discord.py as a conflicting
  kwarg (opaque TypeError) (L12)
- interaction_check uses 'author_id is None' so an author_id of 0 still restricts (nit).

Signed-off-by: disqualifier <dev@disqualifier.me>
2026-06-29 17:58:09 -04:00
dsql 20ea4f9291 fix: single page keeps custom buttons instead of dropping the view (v0.1.2)
a single-page result (max_pages < 2) suppressed the navigation row by dropping the
whole view, which also discarded the consumer's custom per-page buttons. now: if the
page carries custom buttons, keep the view and rebuild with update_buttons(nav=False)
— nav items suppressed, custom buttons kept, and stop() NOT called so their callbacks
still fire. a page with no custom buttons keeps the original drop-the-view behavior.

verified by execution against real discord.py: single page + custom button -> start()
-> callback FIRES on click (view kept, stop() not called); negative control on the old
code drops the button entirely; the no-button single-page case is unregressed.

Signed-off-by: disqualifier <dev@disqualifier.me>
2026-06-29 17:26:04 -04:00
3 changed files with 43 additions and 20 deletions
+5 -3
View File
@@ -9,17 +9,19 @@ buttons) behind previous / jump / next navigation, with an optional cache button
`requirements.txt`: `requirements.txt`:
``` ```
dpy_paginator @ git+ssh://git@git.rethinkstudios.io/rethink-public/dpy_paginator.git@v0.1.1 dpy_paginator @ git+ssh://git@git.rethinkstudios.io/rethink-public/dpy_paginator.git@v0.1.3
``` ```
Direct: Direct:
```bash ```bash
pip install "dpy_paginator @ git+ssh://git@git.rethinkstudios.io/rethink-public/dpy_paginator.git@v0.1.1" pip install "dpy_paginator @ git+ssh://git@git.rethinkstudios.io/rethink-public/dpy_paginator.git@v0.1.3"
``` ```
Requires `discord.py` (pulled transitively). Requires `discord.py` (pulled transitively).
Drop the `@v0.1.3` suffix from the line above to install the latest unpinned.
## Basic usage ## Basic usage
Plain pages — just navigation: Plain pages — just navigation:
@@ -139,4 +141,4 @@ in an embed). It may be sync or async.
## Versioning ## Versioning
Tagged `vX.Y.Z`. Pin the tag in `requirements.txt`. Releases are tagged `vX.Y.Z`. The install line above pins a release; drop the `@vX.Y.Z` suffix to install the latest unpinned. Pin deliberately for reproducible installs.
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project] [project]
name = "dpy_paginator" name = "dpy_paginator"
version = "0.1.1" version = "0.1.3"
description = "Button-navigated paginator for discord.py — config-free, injectable emojis, installable." description = "Button-navigated paginator for discord.py — config-free, injectable emojis, installable."
requires-python = ">=3.10" requires-python = ">=3.10"
dependencies = [ dependencies = [
+37 -16
View File
@@ -200,7 +200,7 @@ class ButtonPaginator(Generic[PageT_co], discord.ui.View):
async def interaction_check(self, interaction: Interaction) -> bool: async def interaction_check(self, interaction: Interaction) -> bool:
"""restrict interaction to author_id when set""" """restrict interaction to author_id when set"""
if not self.author_id or self.author_id == interaction.user.id: if self.author_id is None or self.author_id == interaction.user.id:
return True return True
await interaction.response.send_message("You cannot interact with this menu.", ephemeral=True) await interaction.response.send_message("You cannot interact with this menu.", ephemeral=True)
return False return False
@@ -259,6 +259,14 @@ class ButtonPaginator(Generic[PageT_co], discord.ui.View):
if isinstance(value, discord.Attachment): if isinstance(value, discord.Attachment):
value = await value.to_file() value = await value.to_file()
self._page_kwargs["files"].append(value) self._page_kwargs["files"].append(value)
elif key in ("embed", "embeds", "file", "files"):
# a wrong-typed embed/file would otherwise fall to the catch-all and
# be forwarded verbatim, colliding with the base embeds=[]/files=[]
# and raising an opaque TypeError from inside discord.py — reject it
# here with a clear, paginator-side message
raise ValueError(
f"page key {key!r} has unexpected type {type(value).__name__}"
)
else: else:
self._page_kwargs[key] = value self._page_kwargs[key] = value
elif isinstance(formatted_page, str): elif isinstance(formatted_page, str):
@@ -280,27 +288,34 @@ class ButtonPaginator(Generic[PageT_co], discord.ui.View):
return self._page_kwargs return self._page_kwargs
def update_buttons(self) -> None: def update_buttons(self, nav: bool = True) -> None:
"""rebuild the action row for the current page state""" """rebuild the action row for the current page state
nav=False rebuilds with only the page's custom buttons and no navigation
items (prev/jump/cache/next) — a single-page result that still carries
custom buttons keeps them (and their live callbacks) without a nav row.
"""
self.clear_items() self.clear_items()
self.previous_page.emoji = self.emojis["previous"] if nav:
self.previous_page.disabled = self.current_page <= 0 self.previous_page.emoji = self.emojis["previous"]
self.add_item(self.previous_page) self.previous_page.disabled = self.current_page <= 0
self.add_item(self.previous_page)
self.jump_button.label = self.page_text.format(self.current_page + 1, self.max_pages) self.jump_button.label = self.page_text.format(self.current_page + 1, self.max_pages)
self.add_item(self.jump_button) self.add_item(self.jump_button)
for button in self.current_page_buttons: for button in self.current_page_buttons:
self.add_item(button) self.add_item(button)
if self.cache: if nav:
self.cache_button.emoji = self.emojis["cache"] if self.cache:
self.add_item(self.cache_button) self.cache_button.emoji = self.emojis["cache"]
self.add_item(self.cache_button)
self.next_page.emoji = self.emojis["next"] self.next_page.emoji = self.emojis["next"]
self.next_page.disabled = self.current_page >= self.max_pages - 1 self.next_page.disabled = self.current_page >= self.max_pages - 1
self.add_item(self.next_page) self.add_item(self.next_page)
async def _build_render_kwargs(self) -> Dict[str, Any]: async def _build_render_kwargs(self) -> Dict[str, Any]:
"""build the edit-ready kwargs for the current page (buttons + attachments)""" """build the edit-ready kwargs for the current page (buttons + attachments)"""
@@ -370,8 +385,14 @@ class ButtonPaginator(Generic[PageT_co], discord.ui.View):
self.update_buttons() self.update_buttons()
if self.max_pages < 2: if self.max_pages < 2:
self.stop() if self.current_page_buttons:
kwargs.pop("view", None) # single page WITH custom buttons: keep the view live so the
# buttons' callbacks still fire; strip only the navigation row
self.update_buttons(nav=False)
else:
# single page, no custom buttons: no interactive row at all
self.stop()
kwargs.pop("view", None)
self.reset_files(kwargs) self.reset_files(kwargs)