Button-paginated embed module, written for d.py
Go to file
disqualifier 18bbde19b6 docs: note cache=None/[] disables the cache button
Signed-off-by: disqualifier <dev@disqualifier.me>
2026-06-29 21:41:37 -04:00
src/dpy_paginator fix: F3 guard per_page>=1; swallow on_timeout HTTPException 2026-06-29 21:35:01 -04:00
.gitignore init: button paginator for discord.py 2026-06-24 15:10:18 -04:00
pyproject.toml fix: cache=[] disables the button (not raises); nav row stays suppressed on re-render (v0.1.4) 2026-06-29 20:47:43 -04:00
README.md docs: note cache=None/[] disables the cache button 2026-06-29 21:41:37 -04:00

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.4

Direct:

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

Requires discord.py (pulled transitively).

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

Basic usage

Plain pages — just navigation:

from dpy_paginator import ButtonPaginator

pages = [discord.Embed(title=f"Page {i}") for i in range(5)]
await ButtonPaginator(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:

ButtonPaginator(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.

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 = ButtonPaginator(
    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 ButtonPaginator(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.

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.