From ab98fbee50eb9635d04815bdd2642354e5355403 Mon Sep 17 00:00:00 2001 From: Ale Mercado Date: Thu, 6 Aug 2026 13:44:25 -0400 Subject: [PATCH 1/3] feat(models): add typed model classes for TableBlock cells and column_settings (#1938) --- slack_sdk/models/blocks/__init__.py | 6 + slack_sdk/models/blocks/basic_components.py | 116 ++++++++++++++++++++ slack_sdk/models/blocks/blocks.py | 15 ++- tests/slack_sdk/models/test_blocks.py | 89 +++++++++++++++ 4 files changed, 223 insertions(+), 3 deletions(-) diff --git a/slack_sdk/models/blocks/__init__.py b/slack_sdk/models/blocks/__init__.py index 6a26ed958..575b7198f 100644 --- a/slack_sdk/models/blocks/__init__.py +++ b/slack_sdk/models/blocks/__init__.py @@ -9,6 +9,7 @@ from .basic_components import ( ButtonStyles, + ColumnSettings, ConfirmObject, DynamicSelectElementTypes, FeedbackButtonObject, @@ -16,7 +17,9 @@ Option, OptionGroup, PlainTextObject, + RawNumberCell, RawTextObject, + RichTextCell, TextObject, ) from .block_elements import ( @@ -84,6 +87,7 @@ __all__ = [ "ButtonStyles", + "ColumnSettings", "ConfirmObject", "DynamicSelectElementTypes", "FeedbackButtonObject", @@ -91,7 +95,9 @@ "Option", "OptionGroup", "PlainTextObject", + "RawNumberCell", "RawTextObject", + "RichTextCell", "TextObject", "BlockElement", "ButtonElement", diff --git a/slack_sdk/models/blocks/basic_components.py b/slack_sdk/models/blocks/basic_components.py index b6e71683a..69aa7fcbc 100644 --- a/slack_sdk/models/blocks/basic_components.py +++ b/slack_sdk/models/blocks/basic_components.py @@ -191,6 +191,122 @@ def _validate_text_min_length(self): return len(self.text) >= 1 +class ColumnSettings(JsonObject): + """Column settings for TableBlock columns.""" + + @property + def attributes(self) -> Set[str]: + return {"align", "is_wrapped"} + + def __init__( + self, + *, + align: Optional[str] = None, + is_wrapped: Optional[bool] = None, + **others: dict, + ): + """Settings for a single column in a table block. + https://docs.slack.dev/reference/block-kit/blocks/table-block + + Args: + align: Alignment of the column content. One of "left", "center", or "right". + is_wrapped: Whether the column content should be wrapped. + """ + show_unknown_key_warning(self, others) + self.align = align + self.is_wrapped = is_wrapped + + @classmethod + def parse(cls, settings: Optional[Union[Dict[str, Any], "ColumnSettings"]]) -> Optional["ColumnSettings"]: + if settings is None: + return None + if isinstance(settings, ColumnSettings): + return settings + if isinstance(settings, dict): + return ColumnSettings(**settings) + return None + + +class RawNumberCell(JsonObject): + """A raw_number typed cell for use in TableBlock rows.""" + + type = "raw_number" + + @property + def attributes(self) -> Set[str]: + return {"type", "value", "text"} + + def __init__( + self, + *, + value: Union[int, float], + text: Optional[str] = None, + **others: dict, + ): + """A raw number cell used in table block rows. + https://docs.slack.dev/reference/block-kit/blocks/table-block + + Args: + value (required): The numeric value of the cell. + text: The display text for the cell. If not provided, the value is used. + """ + show_unknown_key_warning(self, others) + self.type = self.__class__.type + self.value = value + self.text = text + + @classmethod + def parse(cls, cell: Optional[Union[Dict[str, Any], "RawNumberCell"]]) -> Optional["RawNumberCell"]: + if cell is None: + return None + if isinstance(cell, RawNumberCell): + return cell + if isinstance(cell, dict): + d = {k: v for k, v in cell.items() if k != "type"} + return RawNumberCell(**d) + return None + + +class RichTextCell(JsonObject): + """A rich_text typed cell for use in TableBlock rows.""" + + type = "rich_text" + + @property + def attributes(self) -> Set[str]: + return {"type", "elements"} + + def __init__( + self, + *, + elements: Sequence[Union[Dict[str, Any], Any]], + **others: dict, + ): + """A rich text cell used in table block rows. + https://docs.slack.dev/reference/block-kit/blocks/table-block + + Args: + elements (required): An array of rich text element objects + (rich_text_section, rich_text_list, rich_text_quote, rich_text_preformatted). + """ + show_unknown_key_warning(self, others) + self.type = self.__class__.type + from slack_sdk.models.blocks.block_elements import BlockElement + + self.elements = BlockElement.parse_all(elements) + + @classmethod + def parse(cls, cell: Optional[Union[Dict[str, Any], "RichTextCell"]]) -> Optional["RichTextCell"]: + if cell is None: + return None + if isinstance(cell, RichTextCell): + return cell + if isinstance(cell, dict): + d = {k: v for k, v in cell.items() if k != "type"} + return RichTextCell(**d) + return None + + class Option(JsonObject): """Option object used in dialogs, legacy message actions (interactivity in attachments), and blocks. JSON must be retrieved with an explicit option_type - the Slack API has diff --git a/slack_sdk/models/blocks/blocks.py b/slack_sdk/models/blocks/blocks.py index db4de1f3a..b674b6f5c 100644 --- a/slack_sdk/models/blocks/blocks.py +++ b/slack_sdk/models/blocks/blocks.py @@ -7,7 +7,16 @@ from slack_sdk.models.basic_objects import JsonObject, JsonValidator from ...errors import SlackObjectFormationError -from .basic_components import MarkdownTextObject, PlainTextObject, SlackFile, TextObject +from .basic_components import ( + ColumnSettings, + MarkdownTextObject, + PlainTextObject, + RawNumberCell, + RawTextObject, + RichTextCell, + SlackFile, + TextObject, +) from .block_elements import ( BlockElement, FeedbackButtonsElement, @@ -756,8 +765,8 @@ def attributes(self) -> Set[str]: # type: ignore[override] def __init__( self, *, - rows: Sequence[Sequence[Dict[str, Any]]], - column_settings: Optional[Sequence[Optional[Dict[str, Any]]]] = None, + rows: Sequence[Sequence[Union[Dict[str, Any], "RawTextObject", "RawNumberCell", "RichTextCell"]]], + column_settings: Optional[Sequence[Optional[Union[Dict[str, Any], "ColumnSettings"]]]] = None, block_id: Optional[str] = None, **others: dict, ): diff --git a/tests/slack_sdk/models/test_blocks.py b/tests/slack_sdk/models/test_blocks.py index fc9ff3266..0ecc80b1c 100644 --- a/tests/slack_sdk/models/test_blocks.py +++ b/tests/slack_sdk/models/test_blocks.py @@ -10,6 +10,7 @@ CallBlock, CardBlock, CarouselBlock, + ColumnSettings, ContextActionsBlock, ContextBlock, DividerBlock, @@ -25,8 +26,10 @@ OverflowMenuElement, PlainTextObject, PlanBlock, + RawNumberCell, RawTextObject, RichTextBlock, + RichTextCell, RichTextElementParts, RichTextListElement, RichTextPreformattedElement, @@ -1462,6 +1465,92 @@ def test_with_block_id(self): } self.assertDictEqual(input, TableBlock(**input).to_dict()) + def test_with_column_settings_objects(self): + """Test table using typed ColumnSettings objects""" + block = TableBlock( + rows=[[{"type": "raw_text", "text": "A"}, {"type": "raw_text", "text": "B"}]], + column_settings=[ColumnSettings(align="right", is_wrapped=True), ColumnSettings(align="left")], + ) + expected = { + "type": "table", + "column_settings": [{"align": "right", "is_wrapped": True}, {"align": "left"}], + "rows": [[{"type": "raw_text", "text": "A"}, {"type": "raw_text", "text": "B"}]], + } + self.assertDictEqual(expected, block.to_dict()) + + def test_with_rich_text_cell_objects(self): + """Test table using typed RichTextCell objects""" + cell = RichTextCell(elements=[{"type": "rich_text_section", "elements": [{"type": "text", "text": "Hello"}]}]) + block = TableBlock( + rows=[ + [RawTextObject(text="Header"), cell], + ], + ) + expected = { + "type": "table", + "rows": [ + [ + {"type": "raw_text", "text": "Header"}, + { + "type": "rich_text", + "elements": [{"type": "rich_text_section", "elements": [{"type": "text", "text": "Hello"}]}], + }, + ] + ], + } + self.assertDictEqual(expected, block.to_dict()) + + def test_mixed_typed_and_dict_cells(self): + """Test table accepts a mix of typed objects and plain dicts""" + block = TableBlock( + rows=[ + [RawTextObject(text="Col A"), RawTextObject(text="Col B")], + [ + {"type": "raw_text", "text": "Data"}, + RichTextCell(elements=[{"type": "rich_text_section", "elements": [{"type": "text", "text": "rich"}]}]), + ], + ], + column_settings=[ColumnSettings(align="left"), {"align": "right"}], + ) + expected = { + "type": "table", + "column_settings": [{"align": "left"}, {"align": "right"}], + "rows": [ + [{"type": "raw_text", "text": "Col A"}, {"type": "raw_text", "text": "Col B"}], + [ + {"type": "raw_text", "text": "Data"}, + { + "type": "rich_text", + "elements": [{"type": "rich_text_section", "elements": [{"type": "text", "text": "rich"}]}], + }, + ], + ], + } + self.assertDictEqual(expected, block.to_dict()) + + def test_with_raw_number_cell_objects(self): + """Test table using typed RawNumberCell objects""" + block = TableBlock( + rows=[ + [RawTextObject(text="Item"), RawNumberCell(value=42, text="42")], + [RawTextObject(text="Price"), RawNumberCell(value=9.99)], + ], + ) + expected = { + "type": "table", + "rows": [ + [ + {"type": "raw_text", "text": "Item"}, + {"type": "raw_number", "value": 42, "text": "42"}, + ], + [ + {"type": "raw_text", "text": "Price"}, + {"type": "raw_number", "value": 9.99}, + ], + ], + } + self.assertDictEqual(expected, block.to_dict()) + def test_column_settings_variations(self): """Test various column_settings configurations""" # Left align From 5325ecb33a33c845de37df915ba94a0ddf1c2a1f Mon Sep 17 00:00:00 2001 From: Ale Mercado Date: Thu, 6 Aug 2026 14:29:29 -0400 Subject: [PATCH 2/3] fix(models): remove RawNumberCell until client rendering is confirmed --- slack_sdk/models/blocks/__init__.py | 2 -- slack_sdk/models/blocks/basic_components.py | 40 --------------------- slack_sdk/models/blocks/blocks.py | 3 +- tests/slack_sdk/models/test_blocks.py | 24 ------------- 4 files changed, 1 insertion(+), 68 deletions(-) diff --git a/slack_sdk/models/blocks/__init__.py b/slack_sdk/models/blocks/__init__.py index 575b7198f..ae343c9f4 100644 --- a/slack_sdk/models/blocks/__init__.py +++ b/slack_sdk/models/blocks/__init__.py @@ -17,7 +17,6 @@ Option, OptionGroup, PlainTextObject, - RawNumberCell, RawTextObject, RichTextCell, TextObject, @@ -95,7 +94,6 @@ "Option", "OptionGroup", "PlainTextObject", - "RawNumberCell", "RawTextObject", "RichTextCell", "TextObject", diff --git a/slack_sdk/models/blocks/basic_components.py b/slack_sdk/models/blocks/basic_components.py index 69aa7fcbc..963743569 100644 --- a/slack_sdk/models/blocks/basic_components.py +++ b/slack_sdk/models/blocks/basic_components.py @@ -227,46 +227,6 @@ def parse(cls, settings: Optional[Union[Dict[str, Any], "ColumnSettings"]]) -> O return None -class RawNumberCell(JsonObject): - """A raw_number typed cell for use in TableBlock rows.""" - - type = "raw_number" - - @property - def attributes(self) -> Set[str]: - return {"type", "value", "text"} - - def __init__( - self, - *, - value: Union[int, float], - text: Optional[str] = None, - **others: dict, - ): - """A raw number cell used in table block rows. - https://docs.slack.dev/reference/block-kit/blocks/table-block - - Args: - value (required): The numeric value of the cell. - text: The display text for the cell. If not provided, the value is used. - """ - show_unknown_key_warning(self, others) - self.type = self.__class__.type - self.value = value - self.text = text - - @classmethod - def parse(cls, cell: Optional[Union[Dict[str, Any], "RawNumberCell"]]) -> Optional["RawNumberCell"]: - if cell is None: - return None - if isinstance(cell, RawNumberCell): - return cell - if isinstance(cell, dict): - d = {k: v for k, v in cell.items() if k != "type"} - return RawNumberCell(**d) - return None - - class RichTextCell(JsonObject): """A rich_text typed cell for use in TableBlock rows.""" diff --git a/slack_sdk/models/blocks/blocks.py b/slack_sdk/models/blocks/blocks.py index b674b6f5c..77254f2c4 100644 --- a/slack_sdk/models/blocks/blocks.py +++ b/slack_sdk/models/blocks/blocks.py @@ -11,7 +11,6 @@ ColumnSettings, MarkdownTextObject, PlainTextObject, - RawNumberCell, RawTextObject, RichTextCell, SlackFile, @@ -765,7 +764,7 @@ def attributes(self) -> Set[str]: # type: ignore[override] def __init__( self, *, - rows: Sequence[Sequence[Union[Dict[str, Any], "RawTextObject", "RawNumberCell", "RichTextCell"]]], + rows: Sequence[Sequence[Union[Dict[str, Any], "RawTextObject", "RichTextCell"]]], column_settings: Optional[Sequence[Optional[Union[Dict[str, Any], "ColumnSettings"]]]] = None, block_id: Optional[str] = None, **others: dict, diff --git a/tests/slack_sdk/models/test_blocks.py b/tests/slack_sdk/models/test_blocks.py index 0ecc80b1c..6771728b7 100644 --- a/tests/slack_sdk/models/test_blocks.py +++ b/tests/slack_sdk/models/test_blocks.py @@ -26,7 +26,6 @@ OverflowMenuElement, PlainTextObject, PlanBlock, - RawNumberCell, RawTextObject, RichTextBlock, RichTextCell, @@ -1528,29 +1527,6 @@ def test_mixed_typed_and_dict_cells(self): } self.assertDictEqual(expected, block.to_dict()) - def test_with_raw_number_cell_objects(self): - """Test table using typed RawNumberCell objects""" - block = TableBlock( - rows=[ - [RawTextObject(text="Item"), RawNumberCell(value=42, text="42")], - [RawTextObject(text="Price"), RawNumberCell(value=9.99)], - ], - ) - expected = { - "type": "table", - "rows": [ - [ - {"type": "raw_text", "text": "Item"}, - {"type": "raw_number", "value": 42, "text": "42"}, - ], - [ - {"type": "raw_text", "text": "Price"}, - {"type": "raw_number", "value": 9.99}, - ], - ], - } - self.assertDictEqual(expected, block.to_dict()) - def test_column_settings_variations(self): """Test various column_settings configurations""" # Left align From 8df84fa48bc4a0e113b6ee76f0cb0df01ce2e5f9 Mon Sep 17 00:00:00 2001 From: Ale Mercado Date: Thu, 6 Aug 2026 15:08:03 -0400 Subject: [PATCH 3/3] docs(models): align ColumnSettings docstrings with official docs --- slack_sdk/models/blocks/basic_components.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/slack_sdk/models/blocks/basic_components.py b/slack_sdk/models/blocks/basic_components.py index 963743569..5cb794c08 100644 --- a/slack_sdk/models/blocks/basic_components.py +++ b/slack_sdk/models/blocks/basic_components.py @@ -209,8 +209,10 @@ def __init__( https://docs.slack.dev/reference/block-kit/blocks/table-block Args: - align: Alignment of the column content. One of "left", "center", or "right". - is_wrapped: Whether the column content should be wrapped. + align: The alignment for items in this column. Can be "left", "center", or "right". + Defaults to "left" if not defined. + is_wrapped: Whether the contents of this column should be wrapped or not. + Defaults to false if not defined. """ show_unknown_key_warning(self, others) self.align = align