DiscordPYComponents 1.3.1__tar.gz → 1.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (22) hide show
  1. {discordpycomponents-1.3.1 → discordpycomponents-1.4.0}/DiscordPYComponents.egg-info/PKG-INFO +184 -28
  2. {discordpycomponents-1.3.1 → discordpycomponents-1.4.0}/DiscordPYComponents.egg-info/SOURCES.txt +1 -0
  3. {discordpycomponents-1.3.1 → discordpycomponents-1.4.0}/PKG-INFO +184 -28
  4. {discordpycomponents-1.3.1 → discordpycomponents-1.4.0}/README.md +183 -27
  5. {discordpycomponents-1.3.1 → discordpycomponents-1.4.0}/py_components/__init__.py +43 -2
  6. discordpycomponents-1.4.0/py_components/components.py +1161 -0
  7. discordpycomponents-1.4.0/py_components/enums.py +115 -0
  8. {discordpycomponents-1.3.1 → discordpycomponents-1.4.0}/py_components/ids.py +29 -5
  9. discordpycomponents-1.4.0/py_components/interactions.py +167 -0
  10. discordpycomponents-1.4.0/py_components/modal.py +76 -0
  11. {discordpycomponents-1.3.1 → discordpycomponents-1.4.0}/py_components/registry.py +11 -0
  12. {discordpycomponents-1.3.1 → discordpycomponents-1.4.0}/py_components/ui.py +84 -18
  13. {discordpycomponents-1.3.1 → discordpycomponents-1.4.0}/py_components/view.py +13 -2
  14. {discordpycomponents-1.3.1 → discordpycomponents-1.4.0}/pyproject.toml +1 -1
  15. discordpycomponents-1.3.1/py_components/components.py +0 -839
  16. discordpycomponents-1.3.1/py_components/enums.py +0 -53
  17. discordpycomponents-1.3.1/py_components/modal.py +0 -46
  18. {discordpycomponents-1.3.1 → discordpycomponents-1.4.0}/DiscordPYComponents.egg-info/dependency_links.txt +0 -0
  19. {discordpycomponents-1.3.1 → discordpycomponents-1.4.0}/DiscordPYComponents.egg-info/requires.txt +0 -0
  20. {discordpycomponents-1.3.1 → discordpycomponents-1.4.0}/DiscordPYComponents.egg-info/top_level.txt +0 -0
  21. {discordpycomponents-1.3.1 → discordpycomponents-1.4.0}/py_components/http.py +0 -0
  22. {discordpycomponents-1.3.1 → discordpycomponents-1.4.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: DiscordPYComponents
3
- Version: 1.3.1
3
+ Version: 1.4.0
4
4
  Summary: Discord Components V2 for discord.py
5
5
  Author: 2gm49
6
6
  License: MIT
@@ -82,6 +82,9 @@ await ui.send(ctx)
82
82
  | `await ui.send(target, ephemeral=False, files=None)` | channel or interaction |
83
83
  | `await ui.reply(interaction, ...)` | same as send on an interaction |
84
84
  | `await ui.edit(target, message=None)` | edit interaction original / a Message |
85
+ | `await ui.update(interaction)` | edit the message a button/select was used on |
86
+ | `ui.count()` | total components, nested ones included |
87
+ | `UI.from_payload(data)` | rebuild a UI from a raw payload or components list |
85
88
 
86
89
  `send` / `reply` try discord.py first (`ctx.send`, `interaction.response.send_message`, `followup.send`). Falls back to raw HTTP if that fails.
87
90
 
@@ -93,9 +96,21 @@ await ui.edit(interaction)
93
96
  await ui.edit(message)
94
97
  ```
95
98
 
96
- Also: `await send_ui(target, ui)` — same as `ui.send(target)`.
99
+ Also: `await send_ui(target, ui)`, same as `ui.send(target)`.
97
100
 
98
- Loose `Button`s are auto-wrapped into `ActionRow`s (5 per row). Put selects in an `ActionRow` yourself.
101
+ Loose `Button`s are auto-wrapped into `ActionRow`s (5 per row). Loose selects get their own `ActionRow`.
102
+
103
+ `to_payload()` raises `ValueError` if the message would go over Discord's 40 component limit (nested components count too).
104
+
105
+ ### Send options
106
+
107
+ ```python
108
+ await ui.send(ctx, silent=True) # no push notification
109
+ await ui.send(ctx, allowed_mentions=discord.AllowedMentions.none())
110
+ await ui.reply(interaction, ephemeral=True)
111
+ ```
112
+
113
+ Pings inside `TextDisplay` follow `allowed_mentions`.
99
114
 
100
115
  ---
101
116
 
@@ -129,6 +144,8 @@ Thumbnail("https://cdn.discordapp.com/...", description="icon", spoiler=False)
129
144
  | `spoiler` | blur it |
130
145
  | `id` | optional |
131
146
 
147
+ `url` can be `attachment://name.png` to use a file sent with the message.
148
+
132
149
  ### MediaGallery / MediaGalleryItem
133
150
 
134
151
  1–10 media items.
@@ -145,7 +162,12 @@ MediaGallery(
145
162
  | `*items` | urls and/or `MediaGalleryItem` |
146
163
  | `id` | optional |
147
164
 
148
- `MediaGalleryItem(url, description=None, spoiler=False)`
165
+ `MediaGalleryItem(url, description=None, spoiler=False)`. Description max 1024.
166
+
167
+ ```python
168
+ gallery = MediaGallery("https://example.com/a.png")
169
+ gallery.add("https://example.com/b.png")
170
+ ```
149
171
 
150
172
  ### File
151
173
 
@@ -162,13 +184,15 @@ File("attachment://report.pdf", spoiler=True)
162
184
  | `spoiler` | blur it |
163
185
  | `id` | optional |
164
186
 
187
+ When you read a File back from a message (`UI.from_payload`), `.name` and `.size` are filled in from Discord.
188
+
165
189
  ---
166
190
 
167
191
  ## Layout components
168
192
 
169
193
  ### ActionRow
170
194
 
171
- Holds up to 5 buttons **or** one select.
195
+ Holds up to 5 buttons **or** one select. Mixing them raises `ValueError`.
172
196
 
173
197
  ```python
174
198
  ActionRow(
@@ -180,7 +204,7 @@ ActionRow(StringSelect("pick", options=[...]))
180
204
 
181
205
  ### Section
182
206
 
183
- 1–3 TextDisplays + optional accessory (Button or Thumbnail).
207
+ 1–3 TextDisplays + optional accessory (Button or Thumbnail). Plain strings are turned into TextDisplays.
184
208
 
185
209
  ```python
186
210
  Section(
@@ -219,11 +243,13 @@ Container(
219
243
  )
220
244
  ```
221
245
 
222
- Children can be ActionRow, TextDisplay, Section, MediaGallery, Separator, File.
246
+ Children can be ActionRow, TextDisplay, Section, MediaGallery, Separator, File. Anything else raises `ValueError`.
247
+
248
+ `accent_color` takes an int (`0x000000`–`0xFFFFFF`) or a `discord.Colour`. `accent_colour=` works too.
223
249
 
224
250
  ```python
225
- c = Container(TextDisplay("x"))
226
- c.add(Separator())
251
+ c = Container(TextDisplay("x"), accent_color=discord.Colour.blurple())
252
+ c.add(Separator(), TextDisplay("more"))
227
253
  ```
228
254
 
229
255
  ---
@@ -250,7 +276,7 @@ Button(
250
276
  | `LINK` | 5 | url= |
251
277
  | `PREMIUM` | 6 | sku_id= |
252
278
 
253
- Lowercase aliases work too: `ButtonStyle.primary`, etc.
279
+ Lowercase aliases work too: `ButtonStyle.primary`, and the discord.py names `blurple`, `grey`, `green`, `red`, `url`.
254
280
 
255
281
  ```python
256
282
  Button("docs", style=ButtonStyle.LINK, url="https://discord.com/developers/docs")
@@ -260,7 +286,11 @@ Button("nope", custom_id="x", disabled=True)
260
286
 
261
287
  Must sit in an ActionRow or as a Section accessory. UI auto-wraps loose buttons.
262
288
 
263
- Label max 80 chars. custom_id 1–100 chars.
289
+ Rules (checked when you create the button):
290
+ - needs a `label` or an `emoji` (except PREMIUM)
291
+ - label max 80, custom_id 1–100, url max 512
292
+ - LINK: `url`, no `custom_id`
293
+ - PREMIUM: `sku_id` only, no label, emoji, url or custom_id
264
294
 
265
295
  ---
266
296
 
@@ -268,6 +298,8 @@ Label max 80 chars. custom_id 1–100 chars.
268
298
 
269
299
  All selects need a `custom_id`. In messages they go inside an ActionRow (one select per row). In modals they go inside a Label.
270
300
 
301
+ Shared rules: `placeholder` max 150, `min_values` 0–25, `max_values` 1–25, `min_values <= max_values`. If you set `required=True`, `min_values` must be at least 1.
302
+
271
303
  ### SelectOption (StringSelect only)
272
304
 
273
305
  ```python
@@ -278,8 +310,12 @@ SelectOption(
278
310
  emoji="🔴",
279
311
  default=False,
280
312
  )
313
+
314
+ SelectOption("Red") # value defaults to the label
281
315
  ```
282
316
 
317
+ Label, value and description max 100.
318
+
283
319
  ### StringSelect
284
320
 
285
321
  ```python
@@ -297,7 +333,7 @@ StringSelect(
297
333
  )
298
334
  ```
299
335
 
300
- 1–25 options.
336
+ 1–25 options with unique values. `max_values` can't be more than the number of options. Plain strings work as options: `StringSelect("colour", ["red", "blue"])`.
301
337
 
302
338
  ### UserSelect / RoleSelect / MentionableSelect / ChannelSelect
303
339
 
@@ -322,8 +358,23 @@ ChannelSelect(
322
358
  )
323
359
  ```
324
360
 
325
- `default_values` entries: `{"id": "<snowflake>", "type": "user"|"role"|"channel"}`.
361
+ `default_values` takes any of:
362
+
363
+ ```python
364
+ from py_components import SelectDefaultValue, ChannelType
326
365
 
366
+ UserSelect("u", default_values=[member, 1234567890]) # objects or ids
367
+ RoleSelect("r", default_values=[SelectDefaultValue.role(role)])
368
+ MentionableSelect("m", default_values=[
369
+ SelectDefaultValue.user(111),
370
+ SelectDefaultValue.role(222),
371
+ ])
372
+ ChannelSelect("c", channel_types=[ChannelType.text, ChannelType.voice],
373
+ default_values=[channel])
374
+ {"id": "<snowflake>", "type": "user"} # raw dicts still work
375
+ ```
376
+
377
+ The number of defaults has to fit between `min_values` and `max_values`, and the type has to match the select (MentionableSelect needs `SelectDefaultValue.user/role` for bare ids).
327
378
  `required` only matters in modals. `disabled` only in messages (modals error if disabled).
328
379
 
329
380
  ---
@@ -337,6 +388,8 @@ make_emoji("🔥")
337
388
  make_emoji("pepe", id=123456789012345678, animated=True)
338
389
  ```
339
390
 
391
+ Anywhere an emoji is accepted you can pass a unicode string, a `<:name:id>` / `<a:name:id>` string, a dict, or a discord.py `Emoji` / `PartialEmoji`.
392
+
340
393
  Buttons and SelectOptions also take a plain unicode string:
341
394
 
342
395
  ```python
@@ -370,6 +423,19 @@ replace_by_id(payload["components"], 10, TextDisplay("x", id=10).to_dict())
370
423
 
371
424
  Use this when editing a message and only changing one piece.
372
425
 
426
+ Ids are only added to components, never to media gallery items.
427
+
428
+ ### Reading components back
429
+
430
+ ```python
431
+ from py_components import UI, component_from_dict
432
+
433
+ ui = UI.from_payload(raw_message_dict) # or a components list
434
+ first = component_from_dict(raw_message_dict["components"][0])
435
+ ```
436
+
437
+ Every component class builds from its raw dict. Component types this library doesn't know yet come back as `UnknownComponent`, which keeps the raw data and sends it back unchanged.
438
+
373
439
  ---
374
440
 
375
441
  ## Modals
@@ -377,7 +443,7 @@ Use this when editing a message and only changing one piece.
377
443
  ```python
378
444
  from py_components import (
379
445
  Modal, Label, TextInput, TextInputStyle,
380
- RadioGroup, RadioGroupOption, CheckboxGroup, Checkbox,
446
+ RadioGroup, RadioGroupOption, CheckboxGroup, GroupOption, Checkbox,
381
447
  FileUpload, StringSelect, SelectOption,
382
448
  )
383
449
 
@@ -415,7 +481,9 @@ async def report(interaction):
415
481
  await modal.open(interaction)
416
482
  ```
417
483
 
418
- Max **5** top-level components. Top-level should be `Label` (or `TextDisplay`). Interactive controls go **inside** Label.
484
+ Max **5** top-level components. Top-level must be `Label` or `TextDisplay`. Interactive controls go **inside** a Label. Components inside a modal can't be `disabled`. All of this is checked when you build the modal.
485
+
486
+ `modal.open(interaction)` must be the first response to the interaction. It raises if you've already responded.
419
487
 
420
488
  ### Modal
421
489
 
@@ -435,7 +503,7 @@ Title max 45 chars. custom_id 1–100.
435
503
  Label("visible label", child_component, description="optional helper text")
436
504
  ```
437
505
 
438
- Label text max 45. Description max 100. One child.
506
+ Label text max 45. Description max 100. One child: TextInput, any select, FileUpload, RadioGroup, CheckboxGroup or Checkbox.
439
507
 
440
508
  ### TextInput
441
509
 
@@ -451,6 +519,10 @@ TextInput(
451
519
  )
452
520
  ```
453
521
 
522
+ min_length 0–4000, max_length 1–4000, value max 4000, placeholder max 100.
523
+
524
+ `TextInput(..., label="...")` still works for old ActionRow modals but gives a `DeprecationWarning`. Use `Label` instead.
525
+
454
526
  ### FileUpload
455
527
 
456
528
  ```python
@@ -463,6 +535,8 @@ FileUpload(
463
535
  )
464
536
  ```
465
537
 
538
+ Up to 10 files, up to 10 `file_types` (`image`, `video`, `audio` or a `.ext`). File size follows the user's own upload limit.
539
+
466
540
  ### RadioGroup / CheckboxGroup / Checkbox
467
541
 
468
542
  ```python
@@ -478,10 +552,12 @@ CheckboxGroup(
478
552
  required=True,
479
553
  )
480
554
 
481
- # single yes/no — label comes from the Label wrapper
555
+ # single yes/no, label comes from the Label wrapper
482
556
  Checkbox("agree", default=False)
483
557
  ```
484
558
 
559
+ A Checkbox can't be required. For a "must tick" box, use a CheckboxGroup with one option and `required=True`. RadioGroup allows one `default` option.
560
+
485
561
  `GroupOption` / `RadioGroupOption` / `CheckboxGroupOption` are the same thing:
486
562
 
487
563
  ```python
@@ -490,7 +566,39 @@ GroupOption("label", "value", description="...", default=False)
490
566
 
491
567
  Selects also work inside Label in modals (StringSelect, UserSelect, etc.).
492
568
 
493
- On submit you get a `MODAL_SUBMIT` interaction. Values are under `interaction.data["components"]` (each Label wraps its filled child).
569
+ ### Reading a modal submit
570
+
571
+ ```python
572
+ from py_components import ModalSubmit
573
+
574
+ @bot.event
575
+ async def on_interaction(interaction):
576
+ if interaction.type.name == "modal_submit" and interaction.data["custom_id"] == "apply":
577
+ form = ModalSubmit(interaction)
578
+
579
+ why = form.text("why") # TextInput -> str
580
+ level = form.get("exp", "none") # RadioGroup -> str or None
581
+ teams = form.selected("teams") # CheckboxGroup / selects -> list[str]
582
+ agreed = form.checked("rules") # Checkbox -> bool
583
+ files = form.attachments("shots") # FileUpload -> attachment dicts
584
+ refs = form.users("ref") # UserSelect -> resolved user dicts
585
+
586
+ await interaction.response.send_message(f"thanks, got {len(files)} files", ephemeral=True)
587
+ ```
588
+
589
+ | method | returns |
590
+ |--------|---------|
591
+ | `form["id"]` | raw value |
592
+ | `form.get("id", default)` | value, or default if empty |
593
+ | `form.text("id")` | str |
594
+ | `form.checked("id")` | bool |
595
+ | `form.selected("id")` | list of str |
596
+ | `form.attachments("id")` | uploaded attachment dicts |
597
+ | `form.users / members / roles / channels("id")` | resolved objects for selects |
598
+ | `form.values()` | dict of every custom_id and value |
599
+ | `form.fields` | `SubmittedField` objects (custom_id, type, id, value, raw) |
600
+
601
+ Works with a discord.py `Interaction` or a raw interaction dict. `parse_modal(x)` is the same as `ModalSubmit(x)`.
494
602
 
495
603
  ---
496
604
 
@@ -553,9 +661,29 @@ Wire it up in your bot:
553
661
  ```python
554
662
  @bot.event
555
663
  async def on_interaction(interaction):
556
- if interaction.type.name == "component":
557
- cid = interaction.data.get("custom_id")
558
- await default_registry.dispatch(cid, interaction)
664
+ if interaction.type.name in ("component", "modal_submit"):
665
+ await default_registry.dispatch_interaction(interaction)
666
+ ```
667
+
668
+ `dispatch_interaction` reads the custom_id for you and returns False if nothing matched. Views have the same method, plus `view.add_select(select, callback=...)`.
669
+
670
+ ### Reading a button or select
671
+
672
+ ```python
673
+ from py_components import ComponentClick
674
+
675
+ click = ComponentClick(interaction)
676
+ click.custom_id # "range"
677
+ click.values # ["7d"] for selects, [] for buttons
678
+ click.value # first value or None
679
+ click.is_select # True
680
+ click.resolved # users / roles / channels for entity selects
681
+ ```
682
+
683
+ Answer by editing the message in place:
684
+
685
+ ```python
686
+ await new_ui.update(interaction)
559
687
  ```
560
688
 
561
689
  ---
@@ -583,11 +711,18 @@ API base: `https://discord.com/api/v10`.
583
711
 
584
712
  ```python
585
713
  from py_components import (
586
- ComponentType, # ACTION_ROW=1, BUTTON=2, ... CHECKBOX=23
587
- ButtonStyle, # PRIMARY..PREMIUM
588
- TextInputStyle, # SHORT=1, PARAGRAPH=2
589
- SeparatorSpacing, # SMALL=1, LARGE=2
590
- IS_COMPONENTS_V2, # 1 << 15
714
+ ComponentType, # ACTION_ROW=1, BUTTON=2, ... CHECKBOX=23
715
+ ButtonStyle, # PRIMARY..PREMIUM
716
+ TextInputStyle, # SHORT=1, PARAGRAPH=2
717
+ SeparatorSpacing, # SMALL=1, LARGE=2
718
+ ChannelType, # GUILD_TEXT=0, GUILD_VOICE=2, ... (for ChannelSelect)
719
+ InteractionResponseType, # UPDATE_MESSAGE=7, MODAL=9, ...
720
+ MessageFlags, # EPHEMERAL, SUPPRESS_NOTIFICATIONS, IS_COMPONENTS_V2
721
+ IS_COMPONENTS_V2, # 1 << 15
722
+ EPHEMERAL, # 1 << 6
723
+ SUPPRESS_NOTIFICATIONS, # 1 << 12
724
+ MAX_MESSAGE_COMPONENTS, # 40
725
+ MAX_MODAL_COMPONENTS, # 5
591
726
  )
592
727
  ```
593
728
 
@@ -685,9 +820,30 @@ await modal.open(interaction)
685
820
  - 40 components max per message
686
821
  - buttons: ActionRow or Section accessory
687
822
  - one select per ActionRow
688
- - modals: max 5 top-level components
823
+ - modals: max 5 top-level components, title ≤45
689
824
  - Label label ≤45, description ≤100
690
- - Button label ≤80, custom_id ≤100
825
+ - Button label ≤80, custom_id ≤100, url ≤512
826
+ - select placeholder ≤150, 0–25 values, 25 options
827
+ - TextDisplay ≤4000, thumbnail/gallery descriptions ≤1024
828
+ - component `id` is a 32-bit int, unique in the message
829
+
830
+ The library checks these when you build components, so mistakes show up as a `ValueError` in your code instead of a 400 from Discord.
831
+
832
+ ## Changelog
833
+
834
+ ### 1.4.0
835
+ - Read components back: `UI.from_payload`, `component_from_dict`, `UnknownComponent` for future types
836
+ - `ModalSubmit` / `parse_modal` to read modal answers, including file uploads and resolved users, roles and channels
837
+ - `ComponentClick` / `parse_component` for buttons and selects
838
+ - `Registry.dispatch_interaction` and `View.dispatch_interaction` for components and modals
839
+ - `SelectDefaultValue` and object/id support for select `default_values`
840
+ - `ChannelType`, `InteractionResponseType`, `MessageFlags` enums
841
+ - `ui.send(silent=True, allowed_mentions=...)`, `ui.update()`, `ui.count()`
842
+ - Loose selects in a `UI` or `View` are wrapped in an ActionRow automatically
843
+ - Validation for every Discord limit: 40 component cap, placeholder, option counts, min/max values, file types, button rules, container and section children, modal children, accent colour range
844
+ - Emoji accepts `<:name:id>` strings and discord.py emoji objects; Container accepts `discord.Colour`
845
+ - `TextInput(label=...)` kept for legacy modals with a deprecation warning
846
+ - Fixed: ids were being added to media gallery items
691
847
 
692
848
  ## License
693
849
 
@@ -10,6 +10,7 @@ py_components/components.py
10
10
  py_components/enums.py
11
11
  py_components/http.py
12
12
  py_components/ids.py
13
+ py_components/interactions.py
13
14
  py_components/modal.py
14
15
  py_components/registry.py
15
16
  py_components/ui.py