pycascadeui 3.3.4__tar.gz → 3.4.1__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.
- {pycascadeui-3.3.4/pycascadeui.egg-info → pycascadeui-3.4.1}/PKG-INFO +9 -3
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/README.md +8 -2
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/cascadeui/__init__.py +18 -3
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/cascadeui/components/inputs.py +71 -7
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/cascadeui/components/patterns/__init__.py +11 -0
- pycascadeui-3.4.1/cascadeui/components/patterns/v2.py +2301 -0
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/cascadeui/components/types.py +9 -0
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/cascadeui/components/wrappers.py +19 -6
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/cascadeui/devtools.py +28 -5
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/cascadeui/persistence/backends/memory.py +12 -0
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/cascadeui/persistence/backends/postgres.py +63 -15
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/cascadeui/persistence/backends/sqlite.py +57 -17
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/cascadeui/persistence/manager.py +251 -58
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/cascadeui/persistence/protocols.py +18 -0
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/cascadeui/state/actions.py +9 -6
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/cascadeui/state/middleware/logging.py +12 -6
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/cascadeui/state/middleware/persistence.py +105 -12
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/cascadeui/state/middleware/undo.py +36 -13
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/cascadeui/state/store.py +1 -1
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/cascadeui/theming/context.py +19 -0
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/cascadeui/utils/__init__.py +2 -1
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/cascadeui/utils/logging.py +66 -11
- pycascadeui-3.4.1/cascadeui/utils/strings.py +72 -0
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/cascadeui/utils/tasks.py +29 -30
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/cascadeui/validation.py +19 -0
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/cascadeui/views/_interaction.py +48 -3
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/cascadeui/views/_navigation.py +204 -69
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/cascadeui/views/_placement.py +59 -0
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/cascadeui/views/base.py +399 -40
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/cascadeui/views/layout.py +122 -39
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/cascadeui/views/patterns/form.py +2 -5
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/cascadeui/views/patterns/leaderboard.py +287 -52
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/cascadeui/views/patterns/menu.py +48 -15
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/cascadeui/views/patterns/paginated.py +71 -29
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/cascadeui/views/patterns/roles.py +31 -23
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/cascadeui/views/patterns/tabs.py +15 -2
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/cascadeui/views/patterns/wizard.py +2 -5
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/cascadeui/views/persistent.py +71 -4
- {pycascadeui-3.3.4 → pycascadeui-3.4.1/pycascadeui.egg-info}/PKG-INFO +9 -3
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/pycascadeui.egg-info/SOURCES.txt +2 -0
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/pyproject.toml +1 -1
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/tests/test_auto_defer.py +35 -0
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/tests/test_backends.py +83 -0
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/tests/test_batching.py +17 -17
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/tests/test_devtools.py +70 -0
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/tests/test_ephemeral_refresh.py +75 -0
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/tests/test_inputs.py +172 -9
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/tests/test_instance_limit.py +23 -2
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/tests/test_layout_view.py +636 -16
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/tests/test_leaderboard_patterns.py +459 -11
- pycascadeui-3.4.1/tests/test_logging_middleware.py +160 -0
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/tests/test_menu_patterns.py +59 -9
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/tests/test_message_cleanup.py +45 -4
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/tests/test_nav_version.py +2 -2
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/tests/test_navigation.py +390 -0
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/tests/test_navigation_artifact_restore.py +21 -0
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/tests/test_paginated_patterns.py +76 -0
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/tests/test_persistence.py +825 -4
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/tests/test_persistence_middleware.py +77 -0
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/tests/test_persistent_views.py +14 -0
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/tests/test_placement.py +107 -1
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/tests/test_rebuild_callback.py +3 -3
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/tests/test_roles_patterns.py +14 -0
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/tests/test_state_slots.py +6 -5
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/tests/test_tab_patterns.py +45 -0
- pycascadeui-3.4.1/tests/test_task_manager.py +204 -0
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/tests/test_theming.py +111 -0
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/tests/test_undo.py +72 -0
- pycascadeui-3.4.1/tests/test_v2_helpers.py +1814 -0
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/tests/test_validation.py +72 -0
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/tests/test_view_init.py +91 -0
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/tests/test_wrappers.py +42 -0
- pycascadeui-3.3.4/cascadeui/components/patterns/v2.py +0 -1256
- pycascadeui-3.3.4/cascadeui/utils/strings.py +0 -20
- pycascadeui-3.3.4/tests/test_v2_helpers.py +0 -672
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/LICENSE +0 -0
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/cascadeui/__main__.py +0 -0
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/cascadeui/components/__init__.py +0 -0
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/cascadeui/components/base.py +0 -0
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/cascadeui/components/buttons.py +0 -0
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/cascadeui/components/patterns/v1.py +0 -0
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/cascadeui/components/selects.py +0 -0
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/cascadeui/components/v1_composition.py +0 -0
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/cascadeui/exceptions.py +0 -0
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/cascadeui/persistence/__init__.py +0 -0
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/cascadeui/persistence/backends/__init__.py +0 -0
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/cascadeui/persistence/config.py +0 -0
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/cascadeui/persistence/migrations.py +0 -0
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/cascadeui/persistence/schema.py +0 -0
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/cascadeui/persistence/schema_postgres.py +0 -0
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/cascadeui/py.typed +0 -0
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/cascadeui/setup.py +0 -0
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/cascadeui/state/__init__.py +0 -0
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/cascadeui/state/computed.py +0 -0
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/cascadeui/state/middleware/__init__.py +0 -0
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/cascadeui/state/reducers.py +0 -0
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/cascadeui/state/singleton.py +0 -0
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/cascadeui/state/slots.py +0 -0
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/cascadeui/state/types.py +0 -0
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/cascadeui/theming/__init__.py +0 -0
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/cascadeui/theming/core.py +0 -0
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/cascadeui/theming/themes.py +0 -0
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/cascadeui/tracing.py +0 -0
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/cascadeui/utils/coercion.py +0 -0
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/cascadeui/utils/decorators.py +0 -0
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/cascadeui/utils/errors.py +0 -0
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/cascadeui/utils/fetch.py +0 -0
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/cascadeui/views/__init__.py +0 -0
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/cascadeui/views/patterns/__init__.py +0 -0
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/cascadeui/views/patterns/types.py +0 -0
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/cascadeui/views/view.py +0 -0
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/pycascadeui.egg-info/dependency_links.txt +0 -0
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/pycascadeui.egg-info/requires.txt +0 -0
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/pycascadeui.egg-info/top_level.txt +0 -0
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/setup.cfg +0 -0
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/tests/test_backends_postgres.py +0 -0
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/tests/test_backends_raw_sql.py +0 -0
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/tests/test_button_grid.py +0 -0
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/tests/test_components.py +0 -0
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/tests/test_computed.py +0 -0
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/tests/test_dynamic_persistent_button.py +0 -0
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/tests/test_emoji_grid.py +0 -0
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/tests/test_fetch.py +0 -0
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/tests/test_five_pillars.py +0 -0
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/tests/test_form_patterns.py +0 -0
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/tests/test_hooks.py +0 -0
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/tests/test_input_coercion.py +0 -0
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/tests/test_layout_pagination.py +0 -0
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/tests/test_layout_patterns.py +0 -0
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/tests/test_layout_persistent.py +0 -0
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/tests/test_middleware.py +0 -0
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/tests/test_owner_only.py +0 -0
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/tests/test_pagination.py +0 -0
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/tests/test_pillar_isolation.py +0 -0
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/tests/test_reducers.py +0 -0
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/tests/test_scoping.py +0 -0
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/tests/test_selects.py +0 -0
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/tests/test_state_store.py +0 -0
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/tests/test_update_coalescing.py +0 -0
- {pycascadeui-3.3.4 → pycascadeui-3.4.1}/tests/test_wizard_patterns.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pycascadeui
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.4.1
|
|
4
4
|
Summary: Redux-inspired UI framework for discord.py
|
|
5
5
|
Author-email: HollowTheSilver <hollow@users.noreply.github.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -179,6 +179,10 @@ class CounterView(StatefulLayoutView):
|
|
|
179
179
|
subscribed_actions = {"SCOPED_UPDATE"}
|
|
180
180
|
state_scope = "user"
|
|
181
181
|
|
|
182
|
+
def __init__(self, *args, **kwargs):
|
|
183
|
+
super().__init__(*args, **kwargs)
|
|
184
|
+
self.build_ui() # First render: send() needs components to ship
|
|
185
|
+
|
|
182
186
|
def build_ui(self):
|
|
183
187
|
self.clear_items()
|
|
184
188
|
count = self.scoped_state.get("count", 0)
|
|
@@ -422,7 +426,7 @@ class ConfigHub(MenuLayoutView):
|
|
|
422
426
|
]
|
|
423
427
|
super().__init__(*args, categories=categories, **kwargs)
|
|
424
428
|
|
|
425
|
-
def
|
|
429
|
+
def build_header(self):
|
|
426
430
|
return [card("## Server Config", key_value(self._summary()))]
|
|
427
431
|
```
|
|
428
432
|
|
|
@@ -650,8 +654,10 @@ for row in rows:
|
|
|
650
654
|
|
|
651
655
|
### Components
|
|
652
656
|
- Stateful buttons, selects, and modals with state integration
|
|
657
|
+
- Modal inputs beyond text: `Checkbox`, `CheckboxGroup`, `RadioGroup`, `FileUpload`, sharing one validator + custom_id contract with `TextInput`
|
|
653
658
|
- Select callbacks can opt into a `values` second parameter
|
|
654
|
-
- V2 builders: `card()`, `stats_card()`, `action_section()`, `toggle_section()`, `image_section()`, `link_section()`, `confirm_section()`, `button_row()`, `cycle_button()`, `toggle_button()`, `tab_nav()`, `key_value()`, `alert()`, `progress_bar()`, `divider()`, `gap()`, `gallery()`, `file_attachment()`
|
|
659
|
+
- V2 builders: `card()`, `stats_card()`, `action_section()`, `toggle_section()`, `image_section()`, `link_section()`, `confirm_section()`, `button_row()`, `cycle_button()`, `toggle_button()`, `tab_nav()`, `choice_row()`, `key_value()`, `alert()`, `progress_bar()`, `divider()`, `gap()`, `gallery()`, `file_attachment()`
|
|
660
|
+
- V2 stateful composites: `PaginatedRegion` (per-section pager), `Collapsible` (inline disclosure)
|
|
655
661
|
- Grid helpers: `emoji_grid()` and `button_grid()`
|
|
656
662
|
- Typed modal fields (`text`, `integer`, `float`, `date`) with per-field validation
|
|
657
663
|
- Declarative `FormSchema` and `WizardSchema` base classes
|
|
@@ -136,6 +136,10 @@ class CounterView(StatefulLayoutView):
|
|
|
136
136
|
subscribed_actions = {"SCOPED_UPDATE"}
|
|
137
137
|
state_scope = "user"
|
|
138
138
|
|
|
139
|
+
def __init__(self, *args, **kwargs):
|
|
140
|
+
super().__init__(*args, **kwargs)
|
|
141
|
+
self.build_ui() # First render: send() needs components to ship
|
|
142
|
+
|
|
139
143
|
def build_ui(self):
|
|
140
144
|
self.clear_items()
|
|
141
145
|
count = self.scoped_state.get("count", 0)
|
|
@@ -379,7 +383,7 @@ class ConfigHub(MenuLayoutView):
|
|
|
379
383
|
]
|
|
380
384
|
super().__init__(*args, categories=categories, **kwargs)
|
|
381
385
|
|
|
382
|
-
def
|
|
386
|
+
def build_header(self):
|
|
383
387
|
return [card("## Server Config", key_value(self._summary()))]
|
|
384
388
|
```
|
|
385
389
|
|
|
@@ -607,8 +611,10 @@ for row in rows:
|
|
|
607
611
|
|
|
608
612
|
### Components
|
|
609
613
|
- Stateful buttons, selects, and modals with state integration
|
|
614
|
+
- Modal inputs beyond text: `Checkbox`, `CheckboxGroup`, `RadioGroup`, `FileUpload`, sharing one validator + custom_id contract with `TextInput`
|
|
610
615
|
- Select callbacks can opt into a `values` second parameter
|
|
611
|
-
- V2 builders: `card()`, `stats_card()`, `action_section()`, `toggle_section()`, `image_section()`, `link_section()`, `confirm_section()`, `button_row()`, `cycle_button()`, `toggle_button()`, `tab_nav()`, `key_value()`, `alert()`, `progress_bar()`, `divider()`, `gap()`, `gallery()`, `file_attachment()`
|
|
616
|
+
- V2 builders: `card()`, `stats_card()`, `action_section()`, `toggle_section()`, `image_section()`, `link_section()`, `confirm_section()`, `button_row()`, `cycle_button()`, `toggle_button()`, `tab_nav()`, `choice_row()`, `key_value()`, `alert()`, `progress_bar()`, `divider()`, `gap()`, `gallery()`, `file_attachment()`
|
|
617
|
+
- V2 stateful composites: `PaginatedRegion` (per-section pager), `Collapsible` (inline disclosure)
|
|
612
618
|
- Grid helpers: `emoji_grid()` and `button_grid()`
|
|
613
619
|
- Typed modal fields (`text`, `integer`, `float`, `date`) with per-field validation
|
|
614
620
|
- Declarative `FormSchema` and `WizardSchema` base classes
|
|
@@ -22,8 +22,11 @@ from .components.buttons import (
|
|
|
22
22
|
)
|
|
23
23
|
from .components.inputs import Checkbox, CheckboxGroup, FileUpload, Modal, RadioGroup, TextInput
|
|
24
24
|
from .components.patterns import (
|
|
25
|
+
Choice,
|
|
26
|
+
Collapsible,
|
|
25
27
|
ConfirmationButtons,
|
|
26
28
|
EmojiGrid,
|
|
29
|
+
PaginatedRegion,
|
|
27
30
|
PaginationControls,
|
|
28
31
|
ProgressBar,
|
|
29
32
|
ToggleGroup,
|
|
@@ -32,6 +35,7 @@ from .components.patterns import (
|
|
|
32
35
|
button_grid,
|
|
33
36
|
button_row,
|
|
34
37
|
card,
|
|
38
|
+
choice_row,
|
|
35
39
|
confirm_section,
|
|
36
40
|
cycle_button,
|
|
37
41
|
divider,
|
|
@@ -55,7 +59,7 @@ from .components.selects import (
|
|
|
55
59
|
RoleSelect,
|
|
56
60
|
UserSelect,
|
|
57
61
|
)
|
|
58
|
-
from .components.types import EmojiInput, MediaInput
|
|
62
|
+
from .components.types import MAX_SELECT_OPTIONS, EmojiInput, MediaInput
|
|
59
63
|
from .components.v1_composition import CompositeComponent, get_component, register_component
|
|
60
64
|
from .components.wrappers import with_confirmation, with_cooldown, with_loading_state
|
|
61
65
|
from .devtools import DevToolsCog, InspectorView
|
|
@@ -100,11 +104,12 @@ from .utils.decorators import cascade_component, cascade_reducer
|
|
|
100
104
|
from .utils.errors import safe_execute, with_error_boundary, with_retry
|
|
101
105
|
from .utils.fetch import fetch_as_file
|
|
102
106
|
from .utils.logging import setup_logging
|
|
103
|
-
from .utils.strings import slugify
|
|
107
|
+
from .utils.strings import is_emoji, slugify
|
|
104
108
|
from .utils.tasks import get_task_manager
|
|
105
109
|
from .validation import (
|
|
106
110
|
ValidationResult,
|
|
107
111
|
choices,
|
|
112
|
+
emoji,
|
|
108
113
|
max_length,
|
|
109
114
|
max_value,
|
|
110
115
|
min_length,
|
|
@@ -147,7 +152,7 @@ if "SQLiteBackend" in _persistence_all:
|
|
|
147
152
|
# // ========================================( Script )======================================== // #
|
|
148
153
|
|
|
149
154
|
|
|
150
|
-
__version__ = "3.
|
|
155
|
+
__version__ = "3.4.1"
|
|
151
156
|
|
|
152
157
|
# Export public API
|
|
153
158
|
__all__ = [
|
|
@@ -226,6 +231,7 @@ __all__ = [
|
|
|
226
231
|
# Type aliases
|
|
227
232
|
"EmojiInput",
|
|
228
233
|
"MediaInput",
|
|
234
|
+
"MAX_SELECT_OPTIONS",
|
|
229
235
|
# V2 Cards & Sections
|
|
230
236
|
"card",
|
|
231
237
|
"action_section",
|
|
@@ -237,6 +243,9 @@ __all__ = [
|
|
|
237
243
|
"button_row",
|
|
238
244
|
"cycle_button",
|
|
239
245
|
"toggle_button",
|
|
246
|
+
# V2 Choices
|
|
247
|
+
"Choice",
|
|
248
|
+
"choice_row",
|
|
240
249
|
# V2 Content
|
|
241
250
|
"key_value",
|
|
242
251
|
"alert",
|
|
@@ -247,6 +256,10 @@ __all__ = [
|
|
|
247
256
|
"gap",
|
|
248
257
|
# V2 Navigation
|
|
249
258
|
"tab_nav",
|
|
259
|
+
# V2 Pagination
|
|
260
|
+
"PaginatedRegion",
|
|
261
|
+
# V2 Collapsible
|
|
262
|
+
"Collapsible",
|
|
250
263
|
# V2 Media
|
|
251
264
|
"gallery",
|
|
252
265
|
"file_attachment",
|
|
@@ -279,6 +292,7 @@ __all__ = [
|
|
|
279
292
|
"ValidationResult",
|
|
280
293
|
"validate_field",
|
|
281
294
|
"validate_fields",
|
|
295
|
+
"emoji",
|
|
282
296
|
"min_length",
|
|
283
297
|
"max_length",
|
|
284
298
|
"regex",
|
|
@@ -307,6 +321,7 @@ __all__ = [
|
|
|
307
321
|
"cascade_reducer",
|
|
308
322
|
"cascade_component",
|
|
309
323
|
"slugify",
|
|
324
|
+
"is_emoji",
|
|
310
325
|
"fetch_as_file",
|
|
311
326
|
# DevTools
|
|
312
327
|
"InspectorView",
|
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
# // ========================================( Modules )======================================== // #
|
|
2
2
|
|
|
3
3
|
|
|
4
|
+
import logging
|
|
4
5
|
from typing import Any, Callable, Dict, List, Optional, Union
|
|
5
6
|
|
|
6
7
|
import discord
|
|
7
8
|
from discord import CheckboxGroupOption, Interaction, RadioGroupOption, TextStyle
|
|
8
9
|
|
|
10
|
+
from ..utils.strings import slugify
|
|
9
11
|
from ..validation import validate_fields
|
|
10
12
|
from .base import StatefulComponent
|
|
11
13
|
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
12
16
|
# // ========================================( Classes )======================================== // #
|
|
13
17
|
|
|
14
18
|
|
|
@@ -72,11 +76,12 @@ class TextInput(StatefulComponent):
|
|
|
72
76
|
def _slug(cls, label: str) -> str:
|
|
73
77
|
"""Derive a stable ``custom_id`` slug from a field label.
|
|
74
78
|
|
|
75
|
-
Single source of truth for the slug rule.
|
|
76
|
-
:
|
|
77
|
-
|
|
79
|
+
Single source of truth for the modal-input slug rule. Prefixes
|
|
80
|
+
``input_`` and delegates to :func:`slugify`, so modal inputs share
|
|
81
|
+
the one safe lowercase-alphanumeric derivation the rest of the
|
|
82
|
+
library uses.
|
|
78
83
|
"""
|
|
79
|
-
return f"input_{label
|
|
84
|
+
return f"input_{slugify(label)}"
|
|
80
85
|
|
|
81
86
|
def create_discord_component(self):
|
|
82
87
|
"""Build a ``ui.Label`` wrapping the inner ``ui.TextInput``."""
|
|
@@ -181,6 +186,13 @@ class CheckboxGroup(StatefulComponent):
|
|
|
181
186
|
self.label = label
|
|
182
187
|
self.description = description
|
|
183
188
|
self.options = self._process_options(options, CheckboxGroupOption)
|
|
189
|
+
# Discord rejects an out-of-range option count with HTTP 400 when
|
|
190
|
+
# the modal opens; raising here surfaces the mistake at its source.
|
|
191
|
+
if not 1 <= len(self.options) <= 10:
|
|
192
|
+
raise ValueError(
|
|
193
|
+
f"CheckboxGroup {label!r} has {len(self.options)} options; Discord "
|
|
194
|
+
f"accepts 1-10. Trim the list or split the choices across inputs."
|
|
195
|
+
)
|
|
184
196
|
self.required = required
|
|
185
197
|
self.min_values = min_values
|
|
186
198
|
self.max_values = max_values
|
|
@@ -254,6 +266,14 @@ class RadioGroup(StatefulComponent):
|
|
|
254
266
|
self.label = label
|
|
255
267
|
self.description = description
|
|
256
268
|
self.options = CheckboxGroup._process_options(options, RadioGroupOption)
|
|
269
|
+
# Discord rejects an out-of-range option count with HTTP 400 when
|
|
270
|
+
# the modal opens; raising here surfaces the mistake at its source.
|
|
271
|
+
if not 2 <= len(self.options) <= 10:
|
|
272
|
+
raise ValueError(
|
|
273
|
+
f"RadioGroup {label!r} has {len(self.options)} options; Discord "
|
|
274
|
+
f"requires 2-10. Use a Checkbox for a single yes/no choice, or "
|
|
275
|
+
f"trim the list."
|
|
276
|
+
)
|
|
257
277
|
self.required = required
|
|
258
278
|
self.validators: List[Callable] = list(validators) if validators else []
|
|
259
279
|
self.custom_id = TextInput._slug(label)
|
|
@@ -418,6 +438,7 @@ class Modal(discord.ui.Modal, StatefulComponent):
|
|
|
418
438
|
if isinstance(input_item, _WRAPPED_INPUT_TYPES):
|
|
419
439
|
label_wrapper = input_item.create_discord_component()
|
|
420
440
|
inner = label_wrapper.component
|
|
441
|
+
self._reject_duplicate_input(input_item.custom_id)
|
|
421
442
|
self.add_item(label_wrapper)
|
|
422
443
|
self.inputs[input_item.custom_id] = input_item
|
|
423
444
|
self._wrapped_pairs.append((input_item, inner))
|
|
@@ -428,10 +449,28 @@ class Modal(discord.ui.Modal, StatefulComponent):
|
|
|
428
449
|
# modal-compatible component the user constructed directly.
|
|
429
450
|
self.add_item(input_item)
|
|
430
451
|
inner = _unwrap_label(input_item)
|
|
452
|
+
self._reject_duplicate_input(inner.custom_id)
|
|
431
453
|
self.inputs[inner.custom_id] = input_item
|
|
432
454
|
|
|
433
455
|
self.user_callback = callback
|
|
434
456
|
|
|
457
|
+
def _reject_duplicate_input(self, custom_id: str) -> None:
|
|
458
|
+
"""Raise before a duplicate input custom_id silently overwrites.
|
|
459
|
+
|
|
460
|
+
Modal input ids derive from the label (``input_{label}``), so two
|
|
461
|
+
inputs with the same label collide on one key. Without this guard
|
|
462
|
+
the second silently overwrites the first in ``self.inputs`` and
|
|
463
|
+
its submitted value is lost at ``on_submit``.
|
|
464
|
+
"""
|
|
465
|
+
if custom_id in self.inputs:
|
|
466
|
+
raise ValueError(
|
|
467
|
+
f"Duplicate modal input custom_id: {custom_id!r}. Modal input ids derive "
|
|
468
|
+
f"from the label ('input_{{label}}'), so two inputs with the same label "
|
|
469
|
+
f"collide on one key: one would silently overwrite the other and its "
|
|
470
|
+
f"submitted value would be lost.\n"
|
|
471
|
+
f" Fix: Give the colliding inputs distinct labels."
|
|
472
|
+
)
|
|
473
|
+
|
|
435
474
|
async def on_submit(self, interaction: Interaction):
|
|
436
475
|
"""Handle modal submission with optional validation."""
|
|
437
476
|
# Collect values from the underlying discord.py components,
|
|
@@ -469,8 +508,11 @@ class Modal(discord.ui.Modal, StatefulComponent):
|
|
|
469
508
|
if errors:
|
|
470
509
|
lines = []
|
|
471
510
|
for field_id, field_errors in errors.items():
|
|
511
|
+
# Field label ("Emoji"), not the derived custom_id slug ("input_emoji").
|
|
512
|
+
field = self.inputs.get(field_id)
|
|
513
|
+
name = getattr(field, "label", field_id)
|
|
472
514
|
for err in field_errors:
|
|
473
|
-
lines.append(f"**{
|
|
515
|
+
lines.append(f"**{name}**: {err.message}")
|
|
474
516
|
await interaction.response.send_message("\n".join(lines), ephemeral=True)
|
|
475
517
|
return
|
|
476
518
|
|
|
@@ -492,7 +534,29 @@ class Modal(discord.ui.Modal, StatefulComponent):
|
|
|
492
534
|
if self.user_callback:
|
|
493
535
|
await self.user_callback(interaction, values)
|
|
494
536
|
# Safety net: defer if the callback forgot to respond
|
|
495
|
-
|
|
496
|
-
await interaction.response.defer()
|
|
537
|
+
await self._safe_post_submit_defer(interaction)
|
|
497
538
|
else:
|
|
539
|
+
await self._safe_post_submit_defer(interaction)
|
|
540
|
+
|
|
541
|
+
async def _safe_post_submit_defer(self, interaction: Interaction) -> None:
|
|
542
|
+
"""Acknowledge the modal submission if the callback left it unanswered.
|
|
543
|
+
|
|
544
|
+
Mirrors the post-callback defer in the view's ``_scheduled_task``:
|
|
545
|
+
the submission's state dispatch has already landed, so a dead or
|
|
546
|
+
already-acked interaction on the trailing ack must not turn a
|
|
547
|
+
successful submit into an unhandled error routed to ``on_error``.
|
|
548
|
+
"""
|
|
549
|
+
if interaction.response.is_done():
|
|
550
|
+
return
|
|
551
|
+
try:
|
|
498
552
|
await interaction.response.defer()
|
|
553
|
+
except discord.NotFound:
|
|
554
|
+
# 10062: the interaction token is already gone -- nothing to ack.
|
|
555
|
+
logger.debug("Modal post-submit defer hit a dead interaction (10062)")
|
|
556
|
+
except discord.HTTPException as e:
|
|
557
|
+
# 40060 (already acknowledged) is a benign cancellation race; any
|
|
558
|
+
# other status is a genuine ack failure the user saw as a toast.
|
|
559
|
+
if e.code == 40060:
|
|
560
|
+
logger.debug("Modal post-submit defer raced an existing ack (40060)")
|
|
561
|
+
else:
|
|
562
|
+
logger.warning(f"Modal post-submit defer failed: status={e.status} code={e.code}")
|
|
@@ -3,12 +3,16 @@
|
|
|
3
3
|
|
|
4
4
|
from .v1 import ConfirmationButtons, PaginationControls, ProgressBar, ToggleGroup
|
|
5
5
|
from .v2 import (
|
|
6
|
+
Choice,
|
|
7
|
+
Collapsible,
|
|
6
8
|
EmojiGrid,
|
|
9
|
+
PaginatedRegion,
|
|
7
10
|
action_section,
|
|
8
11
|
alert,
|
|
9
12
|
button_grid,
|
|
10
13
|
button_row,
|
|
11
14
|
card,
|
|
15
|
+
choice_row,
|
|
12
16
|
confirm_section,
|
|
13
17
|
cycle_button,
|
|
14
18
|
divider,
|
|
@@ -46,6 +50,9 @@ __all__ = [
|
|
|
46
50
|
"button_row",
|
|
47
51
|
"cycle_button",
|
|
48
52
|
"toggle_button",
|
|
53
|
+
# V2 choices
|
|
54
|
+
"Choice",
|
|
55
|
+
"choice_row",
|
|
49
56
|
# V2 content
|
|
50
57
|
"key_value",
|
|
51
58
|
"alert",
|
|
@@ -56,6 +63,10 @@ __all__ = [
|
|
|
56
63
|
"gap",
|
|
57
64
|
# V2 navigation
|
|
58
65
|
"tab_nav",
|
|
66
|
+
# V2 pagination
|
|
67
|
+
"PaginatedRegion",
|
|
68
|
+
# V2 collapsible
|
|
69
|
+
"Collapsible",
|
|
59
70
|
# V2 media
|
|
60
71
|
"gallery",
|
|
61
72
|
"file_attachment",
|