From 64a071862b095627da3179578882ef1ec921fa9d Mon Sep 17 00:00:00 2001 From: Eden Zimbelman Date: Mon, 22 Sep 2025 23:03:01 -0700 Subject: [PATCH 1/7] feat(models): add feedback_buttons and icon_button blocks as context_actions block elements --- slack_sdk/models/blocks/__init__.py | 127 +++++++++++--------- slack_sdk/models/blocks/basic_components.py | 66 +++++++++- slack_sdk/models/blocks/block_elements.py | 119 +++++++++++++++--- slack_sdk/models/blocks/blocks.py | 64 ++++++++-- tests/slack_sdk/models/test_blocks.py | 79 +++++++++--- tests/slack_sdk/models/test_elements.py | 106 ++++++++++++---- 6 files changed, 438 insertions(+), 123 deletions(-) diff --git a/slack_sdk/models/blocks/__init__.py b/slack_sdk/models/blocks/__init__.py index 455d28d5c..dbb8b5177 100644 --- a/slack_sdk/models/blocks/__init__.py +++ b/slack_sdk/models/blocks/__init__.py @@ -6,67 +6,79 @@ * https://api.slack.com/reference/block-kit/blocks * https://app.slack.com/block-kit-builder """ -from .basic_components import ButtonStyles -from .basic_components import ConfirmObject -from .basic_components import DynamicSelectElementTypes -from .basic_components import MarkdownTextObject -from .basic_components import Option -from .basic_components import OptionGroup -from .basic_components import PlainTextObject -from .basic_components import TextObject -from .block_elements import BlockElement -from .block_elements import ButtonElement -from .block_elements import ChannelMultiSelectElement -from .block_elements import ChannelSelectElement -from .block_elements import CheckboxesElement -from .block_elements import ConversationFilter -from .block_elements import ConversationMultiSelectElement -from .block_elements import ConversationSelectElement -from .block_elements import DatePickerElement -from .block_elements import TimePickerElement -from .block_elements import DateTimePickerElement -from .block_elements import ExternalDataMultiSelectElement -from .block_elements import ExternalDataSelectElement -from .block_elements import ImageElement -from .block_elements import InputInteractiveElement -from .block_elements import InteractiveElement -from .block_elements import LinkButtonElement -from .block_elements import OverflowMenuElement -from .block_elements import RichTextInputElement -from .block_elements import PlainTextInputElement -from .block_elements import EmailInputElement -from .block_elements import UrlInputElement -from .block_elements import NumberInputElement -from .block_elements import RadioButtonsElement -from .block_elements import SelectElement -from .block_elements import StaticMultiSelectElement -from .block_elements import StaticSelectElement -from .block_elements import UserMultiSelectElement -from .block_elements import UserSelectElement -from .block_elements import RichTextElement -from .block_elements import RichTextElementParts -from .block_elements import RichTextListElement -from .block_elements import RichTextPreformattedElement -from .block_elements import RichTextQuoteElement -from .block_elements import RichTextSectionElement -from .blocks import ActionsBlock -from .blocks import Block -from .blocks import CallBlock -from .blocks import ContextBlock -from .blocks import DividerBlock -from .blocks import FileBlock -from .blocks import HeaderBlock -from .blocks import ImageBlock -from .blocks import InputBlock -from .blocks import MarkdownBlock -from .blocks import SectionBlock -from .blocks import VideoBlock -from .blocks import RichTextBlock + +from .basic_components import ( + ButtonStyles, + ConfirmObject, + DynamicSelectElementTypes, + FeedbackButtonObject, + MarkdownTextObject, + Option, + OptionGroup, + PlainTextObject, + TextObject, +) +from .block_elements import ( + BlockElement, + ButtonElement, + ChannelMultiSelectElement, + ChannelSelectElement, + CheckboxesElement, + ConversationFilter, + ConversationMultiSelectElement, + ConversationSelectElement, + DatePickerElement, + DateTimePickerElement, + EmailInputElement, + ExternalDataMultiSelectElement, + ExternalDataSelectElement, + FeedbackButtonsElement, + IconButtonElement, + ImageElement, + InputInteractiveElement, + InteractiveElement, + LinkButtonElement, + NumberInputElement, + OverflowMenuElement, + PlainTextInputElement, + RadioButtonsElement, + RichTextElement, + RichTextElementParts, + RichTextInputElement, + RichTextListElement, + RichTextPreformattedElement, + RichTextQuoteElement, + RichTextSectionElement, + SelectElement, + StaticMultiSelectElement, + StaticSelectElement, + TimePickerElement, + UrlInputElement, + UserMultiSelectElement, + UserSelectElement, +) +from .blocks import ( + ActionsBlock, + Block, + CallBlock, + ContextActionsBlock, + ContextBlock, + DividerBlock, + FileBlock, + HeaderBlock, + ImageBlock, + InputBlock, + MarkdownBlock, + RichTextBlock, + SectionBlock, + VideoBlock, +) __all__ = [ "ButtonStyles", "ConfirmObject", "DynamicSelectElementTypes", + "FeedbackButtonObject", "MarkdownTextObject", "Option", "OptionGroup", @@ -85,6 +97,8 @@ "DateTimePickerElement", "ExternalDataMultiSelectElement", "ExternalDataSelectElement", + "FeedbackButtonsElement", + "IconButtonElement", "ImageElement", "InputInteractiveElement", "InteractiveElement", @@ -110,6 +124,7 @@ "ActionsBlock", "Block", "CallBlock", + "ContextActionsBlock", "ContextBlock", "DividerBlock", "FileBlock", diff --git a/slack_sdk/models/blocks/basic_components.py b/slack_sdk/models/blocks/basic_components.py index d6a1aed97..26222493d 100644 --- a/slack_sdk/models/blocks/basic_components.py +++ b/slack_sdk/models/blocks/basic_components.py @@ -1,13 +1,10 @@ import copy import logging import warnings -from typing import List, Optional, Set, Union, Sequence, Dict, Any +from typing import Any, Dict, List, Optional, Sequence, Set, Union from slack_sdk.models import show_unknown_key_warning -from slack_sdk.models.basic_objects import ( - JsonObject, - JsonValidator, -) +from slack_sdk.models.basic_objects import JsonObject, JsonValidator from slack_sdk.models.messages import Link ButtonStyles = {"danger", "primary"} @@ -526,6 +523,65 @@ def to_dict(self) -> Dict[str, Any]: return json +class FeedbackButtonObject(JsonObject): + attributes: Set[str] = set() + + text_max_length = 75 + value_max_length = 2000 + + @classmethod + def parse(cls, feedback_button: Union["FeedbackButtonObject", Dict[str, Any]]): + if feedback_button: + if isinstance(feedback_button, FeedbackButtonObject): + return feedback_button + elif isinstance(feedback_button, dict): + return FeedbackButtonObject(**feedback_button) + else: + # Not yet implemented: show some warning here + return None + return None + + def __init__( + self, + *, + text: Union[str, Dict[str, Any], PlainTextObject], + accessibility_label: Optional[str] = None, + value: str, + **others: Dict[str, Any], + ): + """ + A feedback button element object for either positive or negative feedback. + + Args: + text (required): An object containing some text. Maximum length for this field is 75 characters. + accessibility_label: A label for longer descriptive text about a button element. This label will be read out by screen readers instead of the button `text` object. + value (required): The button value. Maximum length for this field is 2000 characters. + """ + self._text: Optional[TextObject] = TextObject.parse(text, default_type=PlainTextObject.type) + self._accessibility_label: Optional[str] = accessibility_label + self._value: Optional[str] = value + show_unknown_key_warning(self, others) + + @JsonValidator(f"text attribute cannot exceed {text_max_length} characters") + def text_length(self) -> bool: + return self._text is None or len(self._text.text) <= self.text_max_length + + @JsonValidator(f"value attribute cannot exceed {value_max_length} characters") + def value_length(self) -> bool: + return self._value is None or len(self._value) <= self.value_max_length + + def to_dict(self) -> Dict[str, Any]: + self.validate_json() + json = {} + if self._text: + json["text"] = self._text + if self._accessibility_label: + json["accessibility_label"] = self._accessibility_label + if self._value: + json["value"] = self._value + return json + + class WorkflowTrigger(JsonObject): attributes = {"trigger"} diff --git a/slack_sdk/models/blocks/block_elements.py b/slack_sdk/models/blocks/block_elements.py index 4f0fc6d2d..f56e0d322 100644 --- a/slack_sdk/models/blocks/block_elements.py +++ b/slack_sdk/models/blocks/block_elements.py @@ -3,23 +3,24 @@ import re import warnings from abc import ABCMeta -from typing import Iterator, List, Optional, Set, Type, Union, Sequence, Dict, Any +from typing import Any, Dict, Iterator, List, Optional, Sequence, Set, Type, Union from slack_sdk.models import show_unknown_key_warning -from slack_sdk.models.basic_objects import ( - JsonObject, - JsonValidator, - EnumValidator, +from slack_sdk.models.basic_objects import EnumValidator, JsonObject, JsonValidator + +from .basic_components import ( + ButtonStyles, + ConfirmObject, + DispatchActionConfig, + FeedbackButtonObject, + MarkdownTextObject, + Option, + OptionGroup, + PlainTextObject, + SlackFile, + TextObject, + Workflow, ) -from .basic_components import ButtonStyles, Workflow, SlackFile -from .basic_components import ConfirmObject -from .basic_components import DispatchActionConfig -from .basic_components import MarkdownTextObject -from .basic_components import Option -from .basic_components import OptionGroup -from .basic_components import PlainTextObject -from .basic_components import TextObject - # ------------------------------------------------- # Block Elements @@ -539,6 +540,43 @@ def _validate_initial_date_time_valid(self) -> bool: return self.initial_date_time is None or (0 <= self.initial_date_time <= 9999999999) +# ------------------------------------------------- +# Feedback Buttons Element +# ------------------------------------------------- + + +class FeedbackButtonsElement(InteractiveElement): + type = "feedback_buttons" + + @property + def attributes(self) -> Set[str]: # type: ignore[override] + return super().attributes.union({"positive_button", "negative_button"}) + + def __init__( + self, + *, + action_id: Optional[str] = None, + positive_button: Union[dict, FeedbackButtonObject], + negative_button: Union[dict, FeedbackButtonObject], + **others: dict, + ): + """Buttons to indicate positive or negative feedback. + + Args: + action_id (required): An identifier for this action. + You can use this when you receive an interaction payload to identify the source of the action. + Should be unique among all other action_ids in the containing block. + Maximum length for this field is 255 characters. + positive_button (required): A button to indicate positive feedback. + negative_button (required): A button to indicate negative feedback. + """ + super().__init__(action_id=action_id, type=self.type) + show_unknown_key_warning(self, others) + + self.positive_button = FeedbackButtonObject.parse(positive_button) + self.negative_button = FeedbackButtonObject.parse(negative_button) + + # ------------------------------------------------- # Image # ------------------------------------------------- @@ -587,6 +625,59 @@ def _validate_alt_text_length(self) -> bool: return len(self.alt_text) <= self.alt_text_max_length # type: ignore[arg-type] +# ------------------------------------------------- +# Icon Button Element +# ------------------------------------------------- + + +class IconButtonElement(InteractiveElement): + type = "icon_button" + + @property + def attributes(self) -> Set[str]: # type: ignore[override] + return super().attributes.union({"icon", "text", "accessibility_label", "value", "visible_to_user_ids", "confirm"}) + + def __init__( + self, + *, + action_id: Optional[str] = None, + icon: str, + text: Union[str, dict, TextObject], + accessibility_label: Optional[str] = None, + value: Optional[str] = None, + visible_to_user_ids: Optional[List[str]] = None, + confirm: Optional[Union[dict, ConfirmObject]] = None, + **others: dict, + ): + """An icon button to perform actions. + + Args: + action_id: An identifier for this action. + You can use this when you receive an interaction payload to identify the source of the action. + Should be unique among all other action_ids in the containing block. + Maximum length for this field is 255 characters. + icon (required): The icon to show (e.g., 'trash'). + text (required): Defines an object containing some text. + accessibility_label: A label for longer descriptive text about a button element. + This label will be read out by screen readers instead of the button text object. + Maximum length for this field is 75 characters. + value: The button value. + Maximum length for this field is 2000 characters. + visible_to_user_ids: User IDs for which the icon appears. + Maximum length for this field is 10 user IDs. + confirm: A confirm object that defines an optional confirmation dialog after the button is clicked. + """ + super().__init__(action_id=action_id, type=self.type) + show_unknown_key_warning(self, others) + + self.icon = icon + self.text = TextObject.parse(text, PlainTextObject.type) + self.accessibility_label = accessibility_label + self.value = value + self.visible_to_user_ids = visible_to_user_ids + self.confirm = ConfirmObject.parse(confirm) if confirm else None + + # ------------------------------------------------- # Static Select # ------------------------------------------------- diff --git a/slack_sdk/models/blocks/blocks.py b/slack_sdk/models/blocks/blocks.py index 82a154056..569232dbe 100644 --- a/slack_sdk/models/blocks/blocks.py +++ b/slack_sdk/models/blocks/blocks.py @@ -4,19 +4,19 @@ from typing import Any, Dict, List, Optional, Sequence, Set, Union from slack_sdk.models import show_unknown_key_warning -from slack_sdk.models.basic_objects import ( - JsonObject, - JsonValidator, -) -from .basic_components import MarkdownTextObject, SlackFile -from .basic_components import PlainTextObject -from .basic_components import TextObject -from .block_elements import BlockElement, RichTextElement -from .block_elements import ImageElement -from .block_elements import InputInteractiveElement -from .block_elements import InteractiveElement -from ...errors import SlackObjectFormationError +from slack_sdk.models.basic_objects import JsonObject, JsonValidator +from ...errors import SlackObjectFormationError +from .basic_components import MarkdownTextObject, PlainTextObject, SlackFile, TextObject +from .block_elements import ( + BlockElement, + FeedbackButtonsElement, + IconButtonElement, + ImageElement, + InputInteractiveElement, + InteractiveElement, + RichTextElement, +) # ------------------------------------------------- # Base Classes @@ -79,6 +79,8 @@ def parse(cls, block: Union[dict, "Block"]) -> Optional["Block"]: return ActionsBlock(**block) elif type == ContextBlock.type: return ContextBlock(**block) + elif type == ContextActionsBlock.type: + return ContextActionsBlock(**block) elif type == InputBlock.type: return InputBlock(**block) elif type == FileBlock.type: @@ -357,6 +359,44 @@ def _validate_elements_length(self): return self.elements is None or len(self.elements) <= self.elements_max_length +class ContextActionsBlock(Block): + type = "context_actions" + elements_max_length = 5 + + @property + def attributes(self) -> Set[str]: # type: ignore[override] + return super().attributes.union({"elements"}) + + def __init__( + self, + *, + elements: Sequence[Union[dict, FeedbackButtonsElement, IconButtonElement]], + block_id: Optional[str] = None, + **others: dict, + ): + """Displays actions as contextual info, which can include both feedback buttons and icon buttons. + + Args: + elements (required): An array of feedback_buttons or icon_button block elements. Maximum number of items is 5. + block_id: A string acting as a unique identifier for a block. If not specified, one will be generated. + Maximum length for this field is 255 characters. + block_id should be unique for each message and each iteration of a message. + If a message is updated, use a new block_id. + """ + super().__init__(type=self.type, block_id=block_id) + show_unknown_key_warning(self, others) + + self.elements = BlockElement.parse_all(elements) + + @JsonValidator("elements attribute must be specified") + def _validate_elements(self): + return self.elements is None or len(self.elements) > 0 + + @JsonValidator(f"elements attribute cannot exceed {elements_max_length} elements") + def _validate_elements_length(self): + return self.elements is None or len(self.elements) <= self.elements_max_length + + class InputBlock(Block): type = "input" label_max_length = 2000 diff --git a/tests/slack_sdk/models/test_blocks.py b/tests/slack_sdk/models/test_blocks.py index f7d8c129b..a4ac1726a 100644 --- a/tests/slack_sdk/models/test_blocks.py +++ b/tests/slack_sdk/models/test_blocks.py @@ -4,31 +4,32 @@ from slack_sdk.errors import SlackObjectFormationError from slack_sdk.models.blocks import ( ActionsBlock, + Block, + ButtonElement, + CallBlock, + ContextActionsBlock, ContextBlock, DividerBlock, - ImageBlock, - SectionBlock, - InputBlock, FileBlock, - Block, - CallBlock, - ButtonElement, - StaticSelectElement, - OverflowMenuElement, + HeaderBlock, + ImageBlock, ImageElement, + InputBlock, LinkButtonElement, - PlainTextObject, - MarkdownTextObject, - HeaderBlock, MarkdownBlock, - VideoBlock, + MarkdownTextObject, Option, + OverflowMenuElement, + PlainTextObject, RichTextBlock, - RichTextSectionElement, + RichTextElementParts, RichTextListElement, - RichTextQuoteElement, RichTextPreformattedElement, - RichTextElementParts, + RichTextQuoteElement, + RichTextSectionElement, + SectionBlock, + StaticSelectElement, + VideoBlock, ) from slack_sdk.models.blocks.basic_components import SlackFile @@ -526,6 +527,54 @@ def test_element_parsing(self): self.assertDictEqual(original.to_dict(), parsed.to_dict()) +# ---------------------------------------------- +# ContextActionsBlock +# ---------------------------------------------- + + +class ContextActionsBlockTests(unittest.TestCase): + def test_document(self): + input = { + "type": "context_actions", + "block_id": "context-actions-1", + "elements": [ + { + "type": "feedback_buttons", + "action_id": "feedback-action", + "positive_button": {"text": {"type": "plain_text", "text": "+1"}, "value": "positive"}, + "negative_button": {"text": {"type": "plain_text", "text": "-1"}, "value": "negative"}, + }, + { + "type": "icon_button", + "action_id": "delete-action", + "icon": "trash", + "text": {"type": "plain_text", "text": "Delete"}, + "value": "delete", + }, + ], + } + self.assertDictEqual(input, ContextActionsBlock(**input).to_dict()) + self.assertDictEqual(input, Block.parse(input).to_dict()) + + def test_with_feedback_buttons(self): + feedback_buttons = FeedbackButtons( + action_id="feedback-action", + positive_button={"text": {"type": "plain_text", "text": "Good"}, "value": "positive"}, + negative_button={"text": {"type": "plain_text", "text": "Bad"}, "value": "negative"}, + ) + block = ContextActionsBlock(elements=[feedback_buttons]) + self.assertEqual(len(block.elements), 1) + self.assertEqual(block.elements[0].type, "feedback_buttons") + + def test_with_icon_button(self): + icon_button = IconButton( + action_id="icon-action", icon="star", text=PlainTextObject(text="Favorite"), value="favorite" + ) + block = ContextActionsBlock(elements=[icon_button]) + self.assertEqual(len(block.elements), 1) + self.assertEqual(block.elements[0].type, "icon_button") + + # ---------------------------------------------- # Context # ---------------------------------------------- diff --git a/tests/slack_sdk/models/test_elements.py b/tests/slack_sdk/models/test_elements.py index 6a9545b5e..421270266 100644 --- a/tests/slack_sdk/models/test_elements.py +++ b/tests/slack_sdk/models/test_elements.py @@ -4,44 +4,47 @@ from slack_sdk.models.blocks import ( BlockElement, ButtonElement, + ChannelMultiSelectElement, + ChannelSelectElement, + CheckboxesElement, + ConfirmObject, + ConversationMultiSelectElement, + ConversationSelectElement, DatePickerElement, - TimePickerElement, + ExternalDataMultiSelectElement, ExternalDataSelectElement, + FeedbackButtonsElement, + IconButtonElement, ImageElement, + InputInteractiveElement, + InteractiveElement, LinkButtonElement, - UserSelectElement, - StaticSelectElement, - CheckboxesElement, - StaticMultiSelectElement, - ExternalDataMultiSelectElement, - UserMultiSelectElement, - ConversationMultiSelectElement, - ChannelMultiSelectElement, + Option, OverflowMenuElement, PlainTextInputElement, - RadioButtonsElement, - ConversationSelectElement, - ChannelSelectElement, - ConfirmObject, - Option, - InputInteractiveElement, - InteractiveElement, PlainTextObject, + RadioButtonsElement, RichTextBlock, + StaticMultiSelectElement, + StaticSelectElement, + TimePickerElement, + UserMultiSelectElement, + UserSelectElement, ) from slack_sdk.models.blocks.basic_components import SlackFile from slack_sdk.models.blocks.block_elements import ( DateTimePickerElement, EmailInputElement, + FileInputElement, NumberInputElement, - UrlInputElement, - WorkflowButtonElement, + RichTextElementParts, RichTextInputElement, - FileInputElement, RichTextSectionElement, - RichTextElementParts, + UrlInputElement, + WorkflowButtonElement, ) -from . import STRING_3001_CHARS, STRING_301_CHARS + +from . import STRING_301_CHARS, STRING_3001_CHARS class BlockElementTests(unittest.TestCase): @@ -443,6 +446,67 @@ def test_focus_on_load(self): self.assertDictEqual(input, DateTimePickerElement(**input).to_dict()) +# ---------------------------------------------- +# FeedbackButtons +# ---------------------------------------------- + + +class FeedbackButtonsTests(unittest.TestCase): + def test_document(self): + input = { + "type": "feedback_buttons", + "action_id": "feedback-123", + "positive_button": { + "text": {"type": "plain_text", "text": "+1"}, + "accessibility_label": "Positive feedback", + "value": "positive", + }, + "negative_button": { + "text": {"type": "plain_text", "text": "-1"}, + "accessibility_label": "Negative feedback", + "value": "negative", + }, + } + self.assertDictEqual(input, FeedbackButtonsElement(**input).to_dict()) + + +# ---------------------------------------------- +# IconButton +# ---------------------------------------------- + + +class IconButtonTests(unittest.TestCase): + def test_document(self): + input = { + "type": "icon_button", + "action_id": "icon-123", + "icon": "trash", + "text": {"type": "plain_text", "text": "Delete"}, + "accessibility_label": "Delete item", + "value": "delete_item", + "visible_to_user_ids": ["U123456", "U789012"], + } + self.assertDictEqual(input, IconButtonElement(**input).to_dict()) + + def test_with_confirm(self): + input = { + "type": "icon_button", + "action_id": "icon-456", + "icon": "trash", + "text": {"type": "plain_text", "text": "Delete"}, + "value": "trash", + "confirm": { + "title": {"type": "plain_text", "text": "Are you sure?"}, + "text": {"type": "plain_text", "text": "This will send a warning."}, + "confirm": {"type": "plain_text", "text": "Yes"}, + "deny": {"type": "plain_text", "text": "No"}, + }, + } + icon_button = IconButtonElement(**input) + self.assertIsNotNone(icon_button.confirm) + self.assertDictEqual(input, icon_button.to_dict()) + + # ------------------------------------------------- # Image # ------------------------------------------------- From 25635d57de54c72b4fa43c2111acdbeabc25e01a Mon Sep 17 00:00:00 2001 From: Eden Zimbelman Date: Mon, 22 Sep 2025 23:27:22 -0700 Subject: [PATCH 2/7] test: feedback buttons object --- tests/slack_sdk/models/test_objects.py | 73 +++++++++++++++++++------- 1 file changed, 55 insertions(+), 18 deletions(-) diff --git a/tests/slack_sdk/models/test_objects.py b/tests/slack_sdk/models/test_objects.py index 383abee7c..ed0bd2a7e 100644 --- a/tests/slack_sdk/models/test_objects.py +++ b/tests/slack_sdk/models/test_objects.py @@ -1,26 +1,14 @@ import copy import unittest -from typing import Optional, List, Union +from typing import List, Optional, Union from slack_sdk.errors import SlackObjectFormationError from slack_sdk.models import JsonObject, JsonValidator -from slack_sdk.models.blocks import ( - ConfirmObject, - MarkdownTextObject, - Option, - OptionGroup, - PlainTextObject, -) -from slack_sdk.models.blocks.basic_components import Workflow, WorkflowTrigger -from slack_sdk.models.messages import ( - ChannelLink, - DateLink, - EveryoneLink, - HereLink, - Link, - ObjectLink, -) -from . import STRING_301_CHARS, STRING_51_CHARS +from slack_sdk.models.blocks import ConfirmObject, MarkdownTextObject, Option, OptionGroup, PlainTextObject +from slack_sdk.models.blocks.basic_components import FeedbackButtonObject, Workflow, WorkflowTrigger +from slack_sdk.models.messages import ChannelLink, DateLink, EveryoneLink, HereLink, Link, ObjectLink + +from . import STRING_51_CHARS, STRING_301_CHARS class SimpleJsonObject(JsonObject): @@ -374,6 +362,55 @@ def test_deny_length(self): ConfirmObject(title="title", text="Are you sure?", deny=STRING_51_CHARS).to_dict() +class FeedbackButtonObjectTests(unittest.TestCase): + def test_basic_json(self): + feedback_button = FeedbackButtonObject(text="+1", value="positive") + expected = {"text": {"type": "plain_text", "text": "+1", "emoji": True}, "value": "positive"} + self.assertDictEqual(expected, feedback_button.to_dict()) + + def test_with_accessibility_label(self): + feedback_button = FeedbackButtonObject(text="+1", value="positive", accessibility_label="Positive feedback button") + expected = { + "text": {"type": "plain_text", "text": "+1", "emoji": True}, + "value": "positive", + "accessibility_label": "Positive feedback button", + } + self.assertDictEqual(expected, feedback_button.to_dict()) + + def test_with_plain_text_object(self): + text_obj = PlainTextObject(text="-1", emoji=False) + feedback_button = FeedbackButtonObject(text=text_obj, value="negative") + expected = {"text": {"type": "plain_text", "text": "-1", "emoji": False}, "value": "negative"} + self.assertDictEqual(expected, feedback_button.to_dict()) + + def test_text_length_validation(self): + with self.assertRaises(SlackObjectFormationError): + FeedbackButtonObject(text="a" * 76, value="test").to_dict() + + def test_value_length_validation(self): + with self.assertRaises(SlackObjectFormationError): + FeedbackButtonObject(text="+1", value="a" * 2001).to_dict() + + def test_parse_from_dict(self): + data = {"text": "+1", "value": "positive", "accessibility_label": "Positive feedback"} + parsed = FeedbackButtonObject.parse(data) + self.assertIsInstance(parsed, FeedbackButtonObject) + expected = { + "text": {"type": "plain_text", "text": "+1", "emoji": True}, + "value": "positive", + "accessibility_label": "Positive feedback", + } + self.assertDictEqual(expected, parsed.to_dict()) + + def test_parse_from_existing_object(self): + original = FeedbackButtonObject(text="-1", value="negative") + parsed = FeedbackButtonObject.parse(original) + self.assertIs(original, parsed) + + def test_parse_none(self): + self.assertIsNone(FeedbackButtonObject.parse(None)) + + class OptionTests(unittest.TestCase): def setUp(self) -> None: self.common = Option(label="an option", value="option_1") From e9a97f1e522865adbc706766d65551763518f51d Mon Sep 17 00:00:00 2001 From: Eden Zimbelman Date: Mon, 22 Sep 2025 23:40:29 -0700 Subject: [PATCH 3/7] chore: lint --- slack_sdk/models/blocks/basic_components.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/slack_sdk/models/blocks/basic_components.py b/slack_sdk/models/blocks/basic_components.py index 26222493d..bb2dceaae 100644 --- a/slack_sdk/models/blocks/basic_components.py +++ b/slack_sdk/models/blocks/basic_components.py @@ -554,7 +554,8 @@ def __init__( Args: text (required): An object containing some text. Maximum length for this field is 75 characters. - accessibility_label: A label for longer descriptive text about a button element. This label will be read out by screen readers instead of the button `text` object. + accessibility_label: A label for longer descriptive text about a button element. This label will be read out by + screen readers instead of the button `text` object. value (required): The button value. Maximum length for this field is 2000 characters. """ self._text: Optional[TextObject] = TextObject.parse(text, default_type=PlainTextObject.type) From 49229e17154fe08f60f2eb72859f6a0612e2162c Mon Sep 17 00:00:00 2001 From: Eden Zimbelman Date: Tue, 23 Sep 2025 08:58:04 -0700 Subject: [PATCH 4/7] test: fix expected types --- tests/slack_sdk/models/test_blocks.py | 11 ++++++----- tests/slack_sdk/models/test_objects.py | 11 +++++++---- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/tests/slack_sdk/models/test_blocks.py b/tests/slack_sdk/models/test_blocks.py index a4ac1726a..561100cdd 100644 --- a/tests/slack_sdk/models/test_blocks.py +++ b/tests/slack_sdk/models/test_blocks.py @@ -31,7 +31,8 @@ StaticSelectElement, VideoBlock, ) -from slack_sdk.models.blocks.basic_components import SlackFile +from slack_sdk.models.blocks.basic_components import FeedbackButtonObject, SlackFile +from slack_sdk.models.blocks.block_elements import FeedbackButtonsElement, IconButtonElement from . import STRING_3001_CHARS @@ -557,17 +558,17 @@ def test_document(self): self.assertDictEqual(input, Block.parse(input).to_dict()) def test_with_feedback_buttons(self): - feedback_buttons = FeedbackButtons( + feedback_buttons = FeedbackButtonsElement( action_id="feedback-action", - positive_button={"text": {"type": "plain_text", "text": "Good"}, "value": "positive"}, - negative_button={"text": {"type": "plain_text", "text": "Bad"}, "value": "negative"}, + positive_button=FeedbackButtonObject(text="Good", value="positive"), + negative_button=FeedbackButtonObject(text="Bad", value="negative"), ) block = ContextActionsBlock(elements=[feedback_buttons]) self.assertEqual(len(block.elements), 1) self.assertEqual(block.elements[0].type, "feedback_buttons") def test_with_icon_button(self): - icon_button = IconButton( + icon_button = IconButtonElement( action_id="icon-action", icon="star", text=PlainTextObject(text="Favorite"), value="favorite" ) block = ContextActionsBlock(elements=[icon_button]) diff --git a/tests/slack_sdk/models/test_objects.py b/tests/slack_sdk/models/test_objects.py index ed0bd2a7e..9804236c0 100644 --- a/tests/slack_sdk/models/test_objects.py +++ b/tests/slack_sdk/models/test_objects.py @@ -365,13 +365,13 @@ def test_deny_length(self): class FeedbackButtonObjectTests(unittest.TestCase): def test_basic_json(self): feedback_button = FeedbackButtonObject(text="+1", value="positive") - expected = {"text": {"type": "plain_text", "text": "+1", "emoji": True}, "value": "positive"} + expected = {"text": PlainTextObject(text="+1"), "value": "positive"} self.assertDictEqual(expected, feedback_button.to_dict()) def test_with_accessibility_label(self): feedback_button = FeedbackButtonObject(text="+1", value="positive", accessibility_label="Positive feedback button") expected = { - "text": {"type": "plain_text", "text": "+1", "emoji": True}, + "text": PlainTextObject(text="+1"), "value": "positive", "accessibility_label": "Positive feedback button", } @@ -380,7 +380,10 @@ def test_with_accessibility_label(self): def test_with_plain_text_object(self): text_obj = PlainTextObject(text="-1", emoji=False) feedback_button = FeedbackButtonObject(text=text_obj, value="negative") - expected = {"text": {"type": "plain_text", "text": "-1", "emoji": False}, "value": "negative"} + expected = { + "text": PlainTextObject(text="-1"), + "value": "negative", + } self.assertDictEqual(expected, feedback_button.to_dict()) def test_text_length_validation(self): @@ -396,7 +399,7 @@ def test_parse_from_dict(self): parsed = FeedbackButtonObject.parse(data) self.assertIsInstance(parsed, FeedbackButtonObject) expected = { - "text": {"type": "plain_text", "text": "+1", "emoji": True}, + "text": PlainTextObject(text="+1"), "value": "positive", "accessibility_label": "Positive feedback", } From 92982cd87a2feda1f36dcbf92a04b885ee62b9dc Mon Sep 17 00:00:00 2001 From: Eden Zimbelman Date: Tue, 23 Sep 2025 09:04:13 -0700 Subject: [PATCH 5/7] fix: unwrap feedback button object text value --- slack_sdk/models/blocks/basic_components.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/slack_sdk/models/blocks/basic_components.py b/slack_sdk/models/blocks/basic_components.py index bb2dceaae..621cce5fa 100644 --- a/slack_sdk/models/blocks/basic_components.py +++ b/slack_sdk/models/blocks/basic_components.py @@ -575,7 +575,7 @@ def to_dict(self) -> Dict[str, Any]: self.validate_json() json = {} if self._text: - json["text"] = self._text + json["text"] = self._text.text if self._accessibility_label: json["accessibility_label"] = self._accessibility_label if self._value: From 974190673a84e5f8a056a3e2a9185b142a1324aa Mon Sep 17 00:00:00 2001 From: Eden Zimbelman Date: Tue, 23 Sep 2025 09:18:03 -0700 Subject: [PATCH 6/7] fix: parse as plain text and stingified to text object --- slack_sdk/models/blocks/basic_components.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/slack_sdk/models/blocks/basic_components.py b/slack_sdk/models/blocks/basic_components.py index 621cce5fa..caf208e2c 100644 --- a/slack_sdk/models/blocks/basic_components.py +++ b/slack_sdk/models/blocks/basic_components.py @@ -558,7 +558,7 @@ def __init__( screen readers instead of the button `text` object. value (required): The button value. Maximum length for this field is 2000 characters. """ - self._text: Optional[TextObject] = TextObject.parse(text, default_type=PlainTextObject.type) + self._text: Optional[TextObject] = PlainTextObject.parse(text, default_type=PlainTextObject.type) self._accessibility_label: Optional[str] = accessibility_label self._value: Optional[str] = value show_unknown_key_warning(self, others) @@ -575,7 +575,7 @@ def to_dict(self) -> Dict[str, Any]: self.validate_json() json = {} if self._text: - json["text"] = self._text.text + json["text"] = self._text.to_dict() if self._accessibility_label: json["accessibility_label"] = self._accessibility_label if self._value: From 44b040dd4ab5e08b45353d88a94260bbc9ea08c6 Mon Sep 17 00:00:00 2001 From: Eden Zimbelman Date: Tue, 23 Sep 2025 09:24:49 -0700 Subject: [PATCH 7/7] test: fix typechecks and unit tests --- slack_sdk/models/blocks/basic_components.py | 2 +- tests/slack_sdk/models/test_objects.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/slack_sdk/models/blocks/basic_components.py b/slack_sdk/models/blocks/basic_components.py index caf208e2c..ba3de57a4 100644 --- a/slack_sdk/models/blocks/basic_components.py +++ b/slack_sdk/models/blocks/basic_components.py @@ -573,7 +573,7 @@ def value_length(self) -> bool: def to_dict(self) -> Dict[str, Any]: self.validate_json() - json = {} + json: Dict[str, Union[str, dict]] = {} if self._text: json["text"] = self._text.to_dict() if self._accessibility_label: diff --git a/tests/slack_sdk/models/test_objects.py b/tests/slack_sdk/models/test_objects.py index 9804236c0..30bcb7002 100644 --- a/tests/slack_sdk/models/test_objects.py +++ b/tests/slack_sdk/models/test_objects.py @@ -365,13 +365,13 @@ def test_deny_length(self): class FeedbackButtonObjectTests(unittest.TestCase): def test_basic_json(self): feedback_button = FeedbackButtonObject(text="+1", value="positive") - expected = {"text": PlainTextObject(text="+1"), "value": "positive"} + expected = {"text": {"emoji": True, "text": "+1", "type": "plain_text"}, "value": "positive"} self.assertDictEqual(expected, feedback_button.to_dict()) def test_with_accessibility_label(self): feedback_button = FeedbackButtonObject(text="+1", value="positive", accessibility_label="Positive feedback button") expected = { - "text": PlainTextObject(text="+1"), + "text": {"emoji": True, "text": "+1", "type": "plain_text"}, "value": "positive", "accessibility_label": "Positive feedback button", } @@ -381,7 +381,7 @@ def test_with_plain_text_object(self): text_obj = PlainTextObject(text="-1", emoji=False) feedback_button = FeedbackButtonObject(text=text_obj, value="negative") expected = { - "text": PlainTextObject(text="-1"), + "text": {"emoji": False, "text": "-1", "type": "plain_text"}, "value": "negative", } self.assertDictEqual(expected, feedback_button.to_dict()) @@ -399,7 +399,7 @@ def test_parse_from_dict(self): parsed = FeedbackButtonObject.parse(data) self.assertIsInstance(parsed, FeedbackButtonObject) expected = { - "text": PlainTextObject(text="+1"), + "text": {"emoji": True, "text": "+1", "type": "plain_text"}, "value": "positive", "accessibility_label": "Positive feedback", }