Files
dpy_paginator/README.md
T
dsql 8addecd64f fix: cache button renders on single page; cache_button edit swallows deleted/expired message (v0.1.7)
update_buttons gated the cache button behind the nav row, so a single-page
paginator with a non-empty cache never showed it despite the README promising
otherwise; the cache button now renders whenever self.cache is truthy,
independent of nav, and start() keeps the view alive for that case too.

cache_button's post-sleep message.edit was unguarded, so a message deleted or
expired during cache_sleep raised out of the callback; it now mirrors
on_timeout's NotFound/Forbidden swallow and HTTPException log.

also compresses the module and method docstrings (no behavior change).

Signed-off-by: disqualifier <dev@disqualifier.me>
2026-07-02 23:23:26 -04:00

5.9 KiB

dpy_paginator

Button-navigated paginator for discord.py. A discord.ui.View that paginates mixed content (strings, embeds, files, attachments, or dicts of mixed content with custom buttons) behind previous / jump / next navigation, with an optional cache button.

Install

requirements.txt:

dpy_paginator @ git+ssh://git@git.rethinkstudios.io/rethink-public/dpy_paginator.git@v0.1.7

Direct:

pip install "dpy_paginator @ git+ssh://git@git.rethinkstudios.io/rethink-public/dpy_paginator.git@v0.1.7"

Requires discord.py (pulled transitively).

Drop the @v0.1.7 suffix from the line above to install the latest unpinned.

Basic usage

The paginator class is DPYPaginator. It is also exported as ButtonPaginator (a back-compat alias) — both names refer to the same class, so either import works.

Plain pages — just navigation:

from dpy_paginator import DPYPaginator      # or: from dpy_paginator import ButtonPaginator

pages = [discord.Embed(title=f"Page {i}") for i in range(5)]
await DPYPaginator(pages, author_id=ctx.author.id).start(ctx)

start() accepts an Interaction or any Messageable (a Context, channel, etc.).

Emojis

Navigation uses plain Unicode by default — no setup, no emoji upload required. Pass emojis= to override with custom application/guild emojis the bot can use:

DPYPaginator(pages, emojis={
    "previous": "<:icon_back:123...>",
    "next": "<:icon_next:123...>",
    "cache": "<:icon_cache:123...>",
})

Unset keys fall back to the Unicode defaults.

Page types

A page may be a str, discord.Embed, discord.File/Attachment, a sequence of those, or a dict. A dict page can carry content, embed/embeds, file/files, and a buttons list of custom button configs. Both file and files accept a discord.Attachment and convert it via to_file() automatically — you never need to convert an attachment before passing it in.

File pages are safe to navigate back to. discord.py closes a discord.File's underlying handle after every send/edit, so the paginator never resends your File object directly — it rebuilds a fresh discord.File from the same source (path or buffer) on every render, so pages can hold a discord.File once and be paged back and forth indefinitely.

Custom per-page buttons

Each page can declare its own buttons. They render alongside the navigation row and are rebuilt from the page dict on every render — so mutating a page's button config and re-rendering updates the button live.

async def callback_reorder(interaction, button, data, paginator):
    await interaction.response.send_message(f"Reordering {data['email']}", ephemeral=True)

pages = []
for session in sessions:
    pages.append({
        "content": session["_id"],
        "embed": build_embed(session),
        "buttons": [
            {
                "label": "Reorder",
                "style": discord.ButtonStyle.green,
                "emoji": some_emoji,            # optional, your own emoji
                "disabled": False,
                "data": {"email": session["_id"]},
                "callback": callback_reorder,
            }
        ],
    })

paginator = DPYPaginator(
    pages, cache=None, timeout=900, delete_message_after=True,
    mentions_allowed=discord.AllowedMentions.none(), ephemeral=True,
    page_text="Session {} of {}",
)
await paginator.start(interaction)

Button config keys: label, style, emoji, disabled, data (arbitrary, passed to the callback), callback. The callback signature is async def cb(interaction, button, data, paginator). With no callback, the button echoes its data.

Cache button (mention priming)

Discord clients render <@id> as a raw id until the user object is cached locally. Omit cache (or pass None / []) and the button never appears; a non-empty cache must have one entry per page or construction raises. The cache button fixes that: pass cache=[...] with one entry per page, where each entry is a string of the user mentions on that page ("<@111> <@222> <@333>").

When clicked, the button posts the page's mentions in a throwaway ephemeral message (rendering the <@id> tags primes the viewer's client — allowed_mentions is none() so no actual ping fires), waits cache_sleep seconds (default 1.0) for the client to resolve them, then re-renders the current page — so the mentions now display as names without the user having to flip pages manually.

pages, cache = [], []
for group in groups:
    pages.append(build_embed(group))
    cache.append(" ".join(f"<@{uid}>" for uid in group["user_ids"]))

await DPYPaginator(pages, cache=cache, cache_sleep=1.0).start(ctx)

Omit cache or pass None/[] and the button never appears. When set, cache must have one entry per rendered page — a shorter cache raises ValueError at construction rather than failing with an IndexError mid-navigation.

The cache button renders whenever cache is truthy, even on a single page (no navigation row) — it doesn't depend on there being more than one page to flip through.

Constructor options

  • pages — sequence of page content
  • cache — optional per-page cache strings (enables the cache button)
  • author_id — restrict interaction to this user
  • timeout — seconds before the view stops (default 180)
  • delete_message_after — delete the message on timeout
  • per_page — entries per page (default 1)
  • mentions_allowedAllowedMentions for sends (default .all())
  • ephemeral — send/edit ephemerally
  • page_text — format string for the jump button label
  • emojis — override navigation emojis

Subclassing

Override format_page(page) to transform pages before rendering (e.g. wrap raw data in an embed). It may be sync or async.

Versioning

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.