From 04f12c463e6ca3a2df670c55f89bd6e4755b12ed Mon Sep 17 00:00:00 2001 From: codomposer Date: Tue, 11 Nov 2025 11:04:32 -0500 Subject: [PATCH 1/7] add tablblock to blocks --- slack_sdk/models/blocks/__init__.py | 2 + slack_sdk/models/blocks/blocks.py | 45 +++++++++ tests/slack_sdk/models/test_blocks.py | 136 ++++++++++++++++++++++++++ 3 files changed, 183 insertions(+) diff --git a/slack_sdk/models/blocks/__init__.py b/slack_sdk/models/blocks/__init__.py index 334f55c40..a5281b94d 100644 --- a/slack_sdk/models/blocks/__init__.py +++ b/slack_sdk/models/blocks/__init__.py @@ -71,6 +71,7 @@ MarkdownBlock, RichTextBlock, SectionBlock, + TableBlock, VideoBlock, ) @@ -133,6 +134,7 @@ "InputBlock", "MarkdownBlock", "SectionBlock", + "TableBlock", "VideoBlock", "RichTextBlock", ] diff --git a/slack_sdk/models/blocks/blocks.py b/slack_sdk/models/blocks/blocks.py index 69200be25..f422b68fc 100644 --- a/slack_sdk/models/blocks/blocks.py +++ b/slack_sdk/models/blocks/blocks.py @@ -95,6 +95,8 @@ def parse(cls, block: Union[dict, "Block"]) -> Optional["Block"]: return VideoBlock(**block) elif type == RichTextBlock.type: return RichTextBlock(**block) + elif type == TableBlock.type: + return TableBlock(**block) else: cls.logger.warning(f"Unknown block detected and skipped ({block})") return None @@ -730,3 +732,46 @@ def __init__( show_unknown_key_warning(self, others) self.elements = BlockElement.parse_all(elements) + + +class TableBlock(Block): + type = "table" + + @property + def attributes(self) -> Set[str]: # type: ignore[override] + return super().attributes.union({"rows", "column_settings"}) + + def __init__( + self, + *, + rows: Sequence[Sequence[Dict[str, Any]]], + column_settings: Optional[Sequence[Optional[Dict[str, Any]]]] = None, + block_id: Optional[str] = None, + **others: dict, + ): + """Displays structured information in a table. + https://docs.slack.dev/reference/block-kit/blocks/table-block + + Args: + rows (required): A 2D array of table cells. Each row is an array of cell objects. + Each cell can be either a raw_text or rich_text element. + Example cell: {"type": "raw_text", "text": "Cell content"} + column_settings: Optional array of column settings objects to configure text alignment + and wrapping behavior for each column. Use None/null to skip a column. + Each setting can have: + - align: "left" (default), "center", or "right" + - is_wrapped: boolean (default: false) + 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.rows = rows + self.column_settings = column_settings + + @JsonValidator("rows attribute must be specified") + def _validate_rows(self): + return self.rows is not None and len(self.rows) > 0 diff --git a/tests/slack_sdk/models/test_blocks.py b/tests/slack_sdk/models/test_blocks.py index a07ce11b8..b8ebef0c1 100644 --- a/tests/slack_sdk/models/test_blocks.py +++ b/tests/slack_sdk/models/test_blocks.py @@ -29,6 +29,7 @@ RichTextSectionElement, SectionBlock, StaticSelectElement, + TableBlock, VideoBlock, ) from slack_sdk.models.blocks.basic_components import FeedbackButtonObject, SlackFile @@ -1267,3 +1268,138 @@ def test_parsing_empty_block_elements(self): self.assertIsNotNone(block_dict["elements"][1].get("elements")) self.assertIsNotNone(block_dict["elements"][2].get("elements")) self.assertIsNotNone(block_dict["elements"][3].get("elements")) + + +# ---------------------------------------------- +# Table +# ---------------------------------------------- + + +class TableBlockTests(unittest.TestCase): + def test_document(self): + """Test basic table block from Slack documentation example""" + input = { + "type": "table", + "column_settings": [{"is_wrapped": True}, {"align": "right"}], + "rows": [ + [{"type": "raw_text", "text": "Header A"}, {"type": "raw_text", "text": "Header B"}], + [{"type": "raw_text", "text": "Data 1A"}, {"type": "raw_text", "text": "Data 1B"}], + [{"type": "raw_text", "text": "Data 2A"}, {"type": "raw_text", "text": "Data 2B"}], + ], + } + self.assertDictEqual(input, TableBlock(**input).to_dict()) + self.assertDictEqual(input, Block.parse(input).to_dict()) + + def test_with_rich_text(self): + """Test table block with rich_text cells""" + input = { + "type": "table", + "column_settings": [{"is_wrapped": True}, {"align": "right"}], + "rows": [ + [{"type": "raw_text", "text": "Header A"}, {"type": "raw_text", "text": "Header B"}], + [ + {"type": "raw_text", "text": "Data 1A"}, + { + "type": "rich_text", + "elements": [ + { + "type": "rich_text_section", + "elements": [{"text": "Data 1B", "type": "link", "url": "https://slack.com"}], + } + ], + }, + ], + ], + } + self.assertDictEqual(input, TableBlock(**input).to_dict()) + self.assertDictEqual(input, Block.parse(input).to_dict()) + + def test_minimal_table(self): + """Test table with only required fields""" + input = { + "type": "table", + "rows": [[{"type": "raw_text", "text": "Cell"}]], + } + self.assertDictEqual(input, TableBlock(**input).to_dict()) + + def test_with_block_id(self): + """Test table block with block_id""" + input = { + "type": "table", + "block_id": "table-123", + "rows": [ + [{"type": "raw_text", "text": "A"}, {"type": "raw_text", "text": "B"}], + [{"type": "raw_text", "text": "1"}, {"type": "raw_text", "text": "2"}], + ], + } + self.assertDictEqual(input, TableBlock(**input).to_dict()) + + def test_column_settings_variations(self): + """Test various column_settings configurations""" + # Left align + input1 = { + "type": "table", + "column_settings": [{"align": "left"}], + "rows": [[{"type": "raw_text", "text": "Left"}]], + } + self.assertDictEqual(input1, TableBlock(**input1).to_dict()) + + # Center align + input2 = { + "type": "table", + "column_settings": [{"align": "center"}], + "rows": [[{"type": "raw_text", "text": "Center"}]], + } + self.assertDictEqual(input2, TableBlock(**input2).to_dict()) + + # With wrapping + input3 = { + "type": "table", + "column_settings": [{"is_wrapped": False}], + "rows": [[{"type": "raw_text", "text": "No wrap"}]], + } + self.assertDictEqual(input3, TableBlock(**input3).to_dict()) + + # Combined settings + input4 = { + "type": "table", + "column_settings": [{"align": "center", "is_wrapped": True}], + "rows": [[{"type": "raw_text", "text": "Both"}]], + } + self.assertDictEqual(input4, TableBlock(**input4).to_dict()) + + def test_column_settings_with_none(self): + """Test column_settings with None to skip columns""" + input = { + "type": "table", + "column_settings": [{"align": "left"}, None, {"align": "right"}], + "rows": [ + [ + {"type": "raw_text", "text": "Left"}, + {"type": "raw_text", "text": "Default"}, + {"type": "raw_text", "text": "Right"}, + ] + ], + } + self.assertDictEqual(input, TableBlock(**input).to_dict()) + + def test_rows_validation(self): + """Test that rows validation works correctly""" + # Empty rows should fail validation + with self.assertRaises(SlackObjectFormationError): + TableBlock(rows=[]).to_dict() + + def test_multi_row_table(self): + """Test table with multiple rows""" + input = { + "type": "table", + "rows": [ + [{"type": "raw_text", "text": "Name"}, {"type": "raw_text", "text": "Age"}], + [{"type": "raw_text", "text": "Alice"}, {"type": "raw_text", "text": "30"}], + [{"type": "raw_text", "text": "Bob"}, {"type": "raw_text", "text": "25"}], + [{"type": "raw_text", "text": "Charlie"}, {"type": "raw_text", "text": "35"}], + ], + } + block = TableBlock(**input) + self.assertEqual(len(block.rows), 4) + self.assertDictEqual(input, block.to_dict()) From b982f7e6aa49c768de43d7a5844c07546d91c021 Mon Sep 17 00:00:00 2001 From: codomposer Date: Tue, 11 Nov 2025 12:37:14 -0500 Subject: [PATCH 2/7] add RawTextObject --- slack_sdk/models/blocks/__init__.py | 2 + slack_sdk/models/blocks/basic_components.py | 39 +++++++++++ slack_sdk/models/blocks/blocks.py | 9 +-- tests/slack_sdk/models/test_blocks.py | 76 +++++++++++++++++++++ 4 files changed, 122 insertions(+), 4 deletions(-) diff --git a/slack_sdk/models/blocks/__init__.py b/slack_sdk/models/blocks/__init__.py index a5281b94d..d2776a9dc 100644 --- a/slack_sdk/models/blocks/__init__.py +++ b/slack_sdk/models/blocks/__init__.py @@ -16,6 +16,7 @@ Option, OptionGroup, PlainTextObject, + RawTextObject, TextObject, ) from .block_elements import ( @@ -84,6 +85,7 @@ "Option", "OptionGroup", "PlainTextObject", + "RawTextObject", "TextObject", "BlockElement", "ButtonElement", diff --git a/slack_sdk/models/blocks/basic_components.py b/slack_sdk/models/blocks/basic_components.py index f118c5a7f..00b478b51 100644 --- a/slack_sdk/models/blocks/basic_components.py +++ b/slack_sdk/models/blocks/basic_components.py @@ -148,6 +148,45 @@ def from_link(link: Link, title: str = "") -> "MarkdownTextObject": title = f": {title}" return MarkdownTextObject(text=f"{link}{title}") + +class RawTextObject(TextObject): + """raw_text typed text object (used in table block cells)""" + + type = "raw_text" + text_max_length = 3000 + + @property + def attributes(self) -> Set[str]: # type: ignore[override] + return {"text", "type"} + + def __init__(self, *, text: str): + """A raw text object used in table block cells. + https://docs.slack.dev/reference/block-kit/blocks/table-block + + Args: + text (required): The text content for the table cell. + The minimum length is 1 and maximum length is 3000 characters. + """ + super().__init__(text=text, type=self.type) + + @staticmethod + def from_str(text: str) -> "RawTextObject": + """Transforms a string into a RawTextObject""" + return RawTextObject(text=text) + + @staticmethod + def direct_from_string(text: str) -> Dict[str, Any]: + """Transforms a string into the required object shape to act as a RawTextObject""" + return RawTextObject.from_str(text).to_dict() + + @JsonValidator(f"text attribute cannot exceed {text_max_length} characters") + def _validate_text_length(self): + return len(self.text) <= self.text_max_length + + @JsonValidator("text attribute must have at least 1 character") + def _validate_text_min_length(self): + return len(self.text) >= 1 + @staticmethod def direct_from_link(link: Link, title: str = "") -> Dict[str, Any]: """ diff --git a/slack_sdk/models/blocks/blocks.py b/slack_sdk/models/blocks/blocks.py index f422b68fc..b5a7f5aa3 100644 --- a/slack_sdk/models/blocks/blocks.py +++ b/slack_sdk/models/blocks/blocks.py @@ -755,16 +755,17 @@ def __init__( Args: rows (required): A 2D array of table cells. Each row is an array of cell objects. Each cell can be either a raw_text or rich_text element. - Example cell: {"type": "raw_text", "text": "Cell content"} column_settings: Optional array of column settings objects to configure text alignment and wrapping behavior for each column. Use None/null to skip a column. - Each setting can have: - - align: "left" (default), "center", or "right" - - is_wrapped: boolean (default: false) 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. + + Note: + - Cell format: {"type": "raw_text", "text": "Cell content"} or use RawTextObject helper + - Column settings options: align ("left", "center", "right"), is_wrapped (boolean) + - Tables must be sent in the attachments field, not top-level blocks """ super().__init__(type=self.type, block_id=block_id) show_unknown_key_warning(self, others) diff --git a/tests/slack_sdk/models/test_blocks.py b/tests/slack_sdk/models/test_blocks.py index b8ebef0c1..b978dd059 100644 --- a/tests/slack_sdk/models/test_blocks.py +++ b/tests/slack_sdk/models/test_blocks.py @@ -21,6 +21,7 @@ Option, OverflowMenuElement, PlainTextObject, + RawTextObject, RichTextBlock, RichTextElementParts, RichTextListElement, @@ -1270,6 +1271,58 @@ def test_parsing_empty_block_elements(self): self.assertIsNotNone(block_dict["elements"][3].get("elements")) +# ---------------------------------------------- +# RawTextObject +# ---------------------------------------------- + + +class RawTextObjectTests(unittest.TestCase): + def test_basic_creation(self): + """Test basic RawTextObject creation""" + obj = RawTextObject(text="Hello") + expected = {"type": "raw_text", "text": "Hello"} + self.assertDictEqual(expected, obj.to_dict()) + + def test_from_str(self): + """Test RawTextObject.from_str() helper""" + obj = RawTextObject.from_str("Test text") + expected = {"type": "raw_text", "text": "Test text"} + self.assertDictEqual(expected, obj.to_dict()) + + def test_direct_from_string(self): + """Test RawTextObject.direct_from_string() helper""" + result = RawTextObject.direct_from_string("Direct text") + expected = {"type": "raw_text", "text": "Direct text"} + self.assertDictEqual(expected, result) + + def test_text_length_validation_max(self): + """Test that text exceeding 3000 characters fails validation""" + with self.assertRaises(SlackObjectFormationError): + RawTextObject(text="a" * 3001).to_dict() + + def test_text_length_validation_at_max(self): + """Test that text at exactly 3000 characters passes validation""" + obj = RawTextObject(text="a" * 3000) + obj.to_dict() # Should not raise + + def test_text_length_validation_min(self): + """Test that empty text fails validation""" + with self.assertRaises(SlackObjectFormationError): + RawTextObject(text="").to_dict() + + def test_text_length_validation_at_min(self): + """Test that text with 1 character passes validation""" + obj = RawTextObject(text="a") + obj.to_dict() # Should not raise + + def test_attributes(self): + """Test that RawTextObject only has text and type attributes""" + obj = RawTextObject(text="Test") + self.assertEqual(obj.attributes, {"text", "type"}) + # Should not have emoji attribute like PlainTextObject + self.assertNotIn("emoji", obj.to_dict()) + + # ---------------------------------------------- # Table # ---------------------------------------------- @@ -1403,3 +1456,26 @@ def test_multi_row_table(self): block = TableBlock(**input) self.assertEqual(len(block.rows), 4) self.assertDictEqual(input, block.to_dict()) + + def test_with_raw_text_object_helper(self): + """Test table using RawTextObject helper class""" + # Create table using RawTextObject helper + block = TableBlock( + rows=[ + [RawTextObject(text="Product").to_dict(), RawTextObject(text="Price").to_dict()], + [RawTextObject(text="Widget").to_dict(), RawTextObject(text="$10").to_dict()], + [RawTextObject(text="Gadget").to_dict(), RawTextObject(text="$20").to_dict()], + ], + column_settings=[{"is_wrapped": True}, {"align": "right"}], + ) + + expected = { + "type": "table", + "column_settings": [{"is_wrapped": True}, {"align": "right"}], + "rows": [ + [{"type": "raw_text", "text": "Product"}, {"type": "raw_text", "text": "Price"}], + [{"type": "raw_text", "text": "Widget"}, {"type": "raw_text", "text": "$10"}], + [{"type": "raw_text", "text": "Gadget"}, {"type": "raw_text", "text": "$20"}], + ], + } + self.assertDictEqual(expected, block.to_dict()) From 9cc71a6c3ec5a0f838a4b39e7a8a6fbd640af78d Mon Sep 17 00:00:00 2001 From: codomposer Date: Tue, 11 Nov 2025 13:06:58 -0500 Subject: [PATCH 3/7] add test for table blocks in attachments and top level --- .../web/test_web_client_table_block.py | 178 ++++++++++++++++++ 1 file changed, 178 insertions(+) create mode 100644 tests/slack_sdk/web/test_web_client_table_block.py diff --git a/tests/slack_sdk/web/test_web_client_table_block.py b/tests/slack_sdk/web/test_web_client_table_block.py new file mode 100644 index 000000000..13b33ae15 --- /dev/null +++ b/tests/slack_sdk/web/test_web_client_table_block.py @@ -0,0 +1,178 @@ +import unittest +from unittest.mock import Mock, patch + +from slack_sdk import WebClient +from slack_sdk.models.blocks import TableBlock, RawTextObject + + +class TestWebClientTableBlock(unittest.TestCase): + """Tests to verify the correct approach for sending table blocks""" + + def setUp(self): + self.client = WebClient(token="xoxb-test-token", base_url="http://localhost:8888") + + # Create a sample table block + self.table = TableBlock( + rows=[ + [{"type": "raw_text", "text": "Product"}, {"type": "raw_text", "text": "Price"}], + [{"type": "raw_text", "text": "Widget"}, {"type": "raw_text", "text": "$10"}], + [{"type": "raw_text", "text": "Gadget"}, {"type": "raw_text", "text": "$20"}], + ], + column_settings=[{"is_wrapped": True}, {"align": "right"}], + ) + + @patch("slack_sdk.web.client.WebClient.api_call") + def test_table_in_attachments_blocks(self, mock_api_call): + """Test sending table block in attachments.blocks (documented approach)""" + mock_api_call.return_value = {"ok": True, "channel": "C123", "ts": "1234567890.123456"} + + # Method 1: Table in attachments + response = self.client.chat_postMessage( + channel="C123456789", + text="Here's a table:", + attachments=[{"blocks": [self.table.to_dict()]}], + ) + + # Verify the call was made + self.assertTrue(mock_api_call.called) + call_args = mock_api_call.call_args + + # Check that attachments were passed correctly (WebClient wraps in 'json' key) + self.assertIn("json", call_args[1]) + json_data = call_args[1]["json"] + self.assertIn("attachments", json_data) + attachments = json_data["attachments"] + self.assertEqual(len(attachments), 1) + self.assertIn("blocks", attachments[0]) + self.assertEqual(attachments[0]["blocks"][0]["type"], "table") + + @patch("slack_sdk.web.client.WebClient.api_call") + def test_table_in_top_level_blocks(self, mock_api_call): + """Test sending table block in top-level blocks array (alternative approach)""" + mock_api_call.return_value = {"ok": True, "channel": "C123", "ts": "1234567890.123456"} + + # Method 2: Table in top-level blocks + response = self.client.chat_postMessage( + channel="C123456789", + text="Here's a table:", + blocks=[self.table.to_dict()], + ) + + # Verify the call was made + self.assertTrue(mock_api_call.called) + call_args = mock_api_call.call_args + + # Check that blocks were passed correctly (WebClient wraps in 'json' key) + self.assertIn("json", call_args[1]) + json_data = call_args[1]["json"] + self.assertIn("blocks", json_data) + blocks = json_data["blocks"] + self.assertEqual(len(blocks), 1) + self.assertEqual(blocks[0]["type"], "table") + + @patch("slack_sdk.web.client.WebClient.api_call") + def test_table_with_raw_text_object_helper(self, mock_api_call): + """Test creating table using RawTextObject helper""" + mock_api_call.return_value = {"ok": True, "channel": "C123", "ts": "1234567890.123456"} + + # Create table using RawTextObject helper + table_with_helper = TableBlock( + rows=[ + [RawTextObject(text="Name").to_dict(), RawTextObject(text="Age").to_dict()], + [RawTextObject(text="Alice").to_dict(), RawTextObject(text="30").to_dict()], + ] + ) + + response = self.client.chat_postMessage( + channel="C123456789", + text="Table with helpers:", + attachments=[{"blocks": [table_with_helper.to_dict()]}], + ) + + # Verify the call was made and table structure is correct + self.assertTrue(mock_api_call.called) + call_args = mock_api_call.call_args + json_data = call_args[1]["json"] + attachments = json_data["attachments"] + table_dict = attachments[0]["blocks"][0] + + self.assertEqual(table_dict["type"], "table") + self.assertEqual(table_dict["rows"][0][0]["type"], "raw_text") + self.assertEqual(table_dict["rows"][0][0]["text"], "Name") + + def test_table_to_dict_serialization(self): + """Test that TableBlock.to_dict() produces correct structure""" + table_dict = self.table.to_dict() + + # Verify structure + self.assertEqual(table_dict["type"], "table") + self.assertIn("rows", table_dict) + self.assertIn("column_settings", table_dict) + + # Verify rows structure + self.assertEqual(len(table_dict["rows"]), 3) + self.assertEqual(table_dict["rows"][0][0]["type"], "raw_text") + self.assertEqual(table_dict["rows"][0][0]["text"], "Product") + + # Verify column settings + self.assertEqual(len(table_dict["column_settings"]), 2) + self.assertEqual(table_dict["column_settings"][0]["is_wrapped"], True) + self.assertEqual(table_dict["column_settings"][1]["align"], "right") + + @patch("slack_sdk.web.client.WebClient.api_call") + def test_multiple_tables_not_allowed(self, mock_api_call): + """Test that only one table per message is allowed (per Slack documentation)""" + mock_api_call.return_value = {"ok": True, "channel": "C123", "ts": "1234567890.123456"} + + table2 = TableBlock( + rows=[[{"type": "raw_text", "text": "Another table"}]] + ) + + # According to docs, this should only send one table + # The SDK doesn't enforce this, but Slack API will return error + response = self.client.chat_postMessage( + channel="C123456789", + text="Multiple tables:", + attachments=[ + {"blocks": [self.table.to_dict()]}, + {"blocks": [table2.to_dict()]}, + ], + ) + + # Verify both tables were sent (SDK allows it, but Slack API will reject) + self.assertTrue(mock_api_call.called) + call_args = mock_api_call.call_args + json_data = call_args[1]["json"] + attachments = json_data["attachments"] + self.assertEqual(len(attachments), 2) + + def test_table_with_rich_text_cells(self): + """Test table with rich_text cells (links, formatting)""" + table_with_rich_text = TableBlock( + rows=[ + [{"type": "raw_text", "text": "Header A"}, {"type": "raw_text", "text": "Header B"}], + [ + {"type": "raw_text", "text": "Data 1A"}, + { + "type": "rich_text", + "elements": [ + { + "type": "rich_text_section", + "elements": [{"text": "Data 1B", "type": "link", "url": "https://slack.com"}], + } + ], + }, + ], + ] + ) + + table_dict = table_with_rich_text.to_dict() + + # Verify mixed cell types + self.assertEqual(table_dict["rows"][0][0]["type"], "raw_text") + self.assertEqual(table_dict["rows"][1][1]["type"], "rich_text") + self.assertIn("elements", table_dict["rows"][1][1]) + + +if __name__ == "__main__": + unittest.main() From b0b168655d7a1f14e7cfc49a6713bf8778fa4c22 Mon Sep 17 00:00:00 2001 From: codomposer Date: Wed, 19 Nov 2025 16:12:15 -0500 Subject: [PATCH 4/7] match note of tableblock to slack docs --- slack_sdk/models/blocks/basic_components.py | 16 ++++++++-------- slack_sdk/models/blocks/blocks.py | 20 ++++++++++---------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/slack_sdk/models/blocks/basic_components.py b/slack_sdk/models/blocks/basic_components.py index 00b478b51..5a6283587 100644 --- a/slack_sdk/models/blocks/basic_components.py +++ b/slack_sdk/models/blocks/basic_components.py @@ -148,6 +148,14 @@ def from_link(link: Link, title: str = "") -> "MarkdownTextObject": title = f": {title}" return MarkdownTextObject(text=f"{link}{title}") + @staticmethod + def direct_from_link(link: Link, title: str = "") -> Dict[str, Any]: + """ + Transform a Link object directly into the required object shape + to act as a MarkdownTextObject + """ + return MarkdownTextObject.from_link(link, title).to_dict() + class RawTextObject(TextObject): """raw_text typed text object (used in table block cells)""" @@ -187,14 +195,6 @@ def _validate_text_length(self): def _validate_text_min_length(self): return len(self.text) >= 1 - @staticmethod - def direct_from_link(link: Link, title: str = "") -> Dict[str, Any]: - """ - Transform a Link object directly into the required object shape - to act as a MarkdownTextObject - """ - return MarkdownTextObject.from_link(link, title).to_dict() - class Option(JsonObject): """Option object used in dialogs, legacy message actions (interactivity in attachments), diff --git a/slack_sdk/models/blocks/blocks.py b/slack_sdk/models/blocks/blocks.py index b5a7f5aa3..ad81212ae 100644 --- a/slack_sdk/models/blocks/blocks.py +++ b/slack_sdk/models/blocks/blocks.py @@ -753,19 +753,19 @@ def __init__( https://docs.slack.dev/reference/block-kit/blocks/table-block Args: - rows (required): A 2D array of table cells. Each row is an array of cell objects. - Each cell can be either a raw_text or rich_text element. - column_settings: Optional array of column settings objects to configure text alignment - and wrapping behavior for each column. Use None/null to skip a column. - block_id: A string acting as a unique identifier for a block. If not specified, one will be generated. + rows (required): An array consisting of table rows. Maximum 100 rows. + Each row object is an array with a max of 20 table cells. + Table cells can have a type of raw_text or rich_text. + column_settings: An array describing column behavior. If there are fewer items in the column_settings array + than there are columns in the table, then the items in the the column_settings array will describe + the same number of columns in the table as there are in the array itself. + Any additional columns will have the default behavior. Maximum 20 items. + See below for column settings schema. + block_id: A unique identifier for a block. If not specified, a block_id will be generated. + You can use this block_id when you receive an interaction payload to identify the source of the action. 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. - - Note: - - Cell format: {"type": "raw_text", "text": "Cell content"} or use RawTextObject helper - - Column settings options: align ("left", "center", "right"), is_wrapped (boolean) - - Tables must be sent in the attachments field, not top-level blocks """ super().__init__(type=self.type, block_id=block_id) show_unknown_key_warning(self, others) From c47f6d95b314fa71f3ec84fb087f75afe7228110 Mon Sep 17 00:00:00 2001 From: Eden Zimbelman Date: Wed, 19 Nov 2025 14:34:08 -0800 Subject: [PATCH 5/7] chore: lint --- tests/slack_sdk/web/test_web_client_table_block.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/tests/slack_sdk/web/test_web_client_table_block.py b/tests/slack_sdk/web/test_web_client_table_block.py index 13b33ae15..6b175ef50 100644 --- a/tests/slack_sdk/web/test_web_client_table_block.py +++ b/tests/slack_sdk/web/test_web_client_table_block.py @@ -10,7 +10,7 @@ class TestWebClientTableBlock(unittest.TestCase): def setUp(self): self.client = WebClient(token="xoxb-test-token", base_url="http://localhost:8888") - + # Create a sample table block self.table = TableBlock( rows=[ @@ -95,7 +95,7 @@ def test_table_with_raw_text_object_helper(self, mock_api_call): json_data = call_args[1]["json"] attachments = json_data["attachments"] table_dict = attachments[0]["blocks"][0] - + self.assertEqual(table_dict["type"], "table") self.assertEqual(table_dict["rows"][0][0]["type"], "raw_text") self.assertEqual(table_dict["rows"][0][0]["text"], "Name") @@ -108,12 +108,12 @@ def test_table_to_dict_serialization(self): self.assertEqual(table_dict["type"], "table") self.assertIn("rows", table_dict) self.assertIn("column_settings", table_dict) - + # Verify rows structure self.assertEqual(len(table_dict["rows"]), 3) self.assertEqual(table_dict["rows"][0][0]["type"], "raw_text") self.assertEqual(table_dict["rows"][0][0]["text"], "Product") - + # Verify column settings self.assertEqual(len(table_dict["column_settings"]), 2) self.assertEqual(table_dict["column_settings"][0]["is_wrapped"], True) @@ -124,9 +124,7 @@ def test_multiple_tables_not_allowed(self, mock_api_call): """Test that only one table per message is allowed (per Slack documentation)""" mock_api_call.return_value = {"ok": True, "channel": "C123", "ts": "1234567890.123456"} - table2 = TableBlock( - rows=[[{"type": "raw_text", "text": "Another table"}]] - ) + table2 = TableBlock(rows=[[{"type": "raw_text", "text": "Another table"}]]) # According to docs, this should only send one table # The SDK doesn't enforce this, but Slack API will return error @@ -167,7 +165,7 @@ def test_table_with_rich_text_cells(self): ) table_dict = table_with_rich_text.to_dict() - + # Verify mixed cell types self.assertEqual(table_dict["rows"][0][0]["type"], "raw_text") self.assertEqual(table_dict["rows"][1][1]["type"], "rich_text") From ee24a66e077842d10817e6b55d60bcf6f6f0165f Mon Sep 17 00:00:00 2001 From: Eden Zimbelman Date: Wed, 19 Nov 2025 15:22:14 -0800 Subject: [PATCH 6/7] fix: remove raw_text max length --- slack_sdk/models/blocks/basic_components.py | 11 +++-------- tests/slack_sdk/models/test_blocks.py | 10 ---------- 2 files changed, 3 insertions(+), 18 deletions(-) diff --git a/slack_sdk/models/blocks/basic_components.py b/slack_sdk/models/blocks/basic_components.py index ecab856f5..b6e71683a 100644 --- a/slack_sdk/models/blocks/basic_components.py +++ b/slack_sdk/models/blocks/basic_components.py @@ -158,10 +158,9 @@ def direct_from_link(link: Link, title: str = "") -> Dict[str, Any]: class RawTextObject(TextObject): - """raw_text typed text object (used in table block cells)""" + """raw_text typed text object""" type = "raw_text" - text_max_length = 3000 @property def attributes(self) -> Set[str]: # type: ignore[override] @@ -169,11 +168,11 @@ def attributes(self) -> Set[str]: # type: ignore[override] def __init__(self, *, text: str): """A raw text object used in table block cells. + https://docs.slack.dev/reference/block-kit/composition-objects/text-object/ https://docs.slack.dev/reference/block-kit/blocks/table-block Args: - text (required): The text content for the table cell. - The minimum length is 1 and maximum length is 3000 characters. + text (required): The text content for the table block cell. """ super().__init__(text=text, type=self.type) @@ -187,10 +186,6 @@ def direct_from_string(text: str) -> Dict[str, Any]: """Transforms a string into the required object shape to act as a RawTextObject""" return RawTextObject.from_str(text).to_dict() - @JsonValidator(f"text attribute cannot exceed {text_max_length} characters") - def _validate_text_length(self): - return len(self.text) <= self.text_max_length - @JsonValidator("text attribute must have at least 1 character") def _validate_text_min_length(self): return len(self.text) >= 1 diff --git a/tests/slack_sdk/models/test_blocks.py b/tests/slack_sdk/models/test_blocks.py index b978dd059..6f3b9f141 100644 --- a/tests/slack_sdk/models/test_blocks.py +++ b/tests/slack_sdk/models/test_blocks.py @@ -1295,16 +1295,6 @@ def test_direct_from_string(self): expected = {"type": "raw_text", "text": "Direct text"} self.assertDictEqual(expected, result) - def test_text_length_validation_max(self): - """Test that text exceeding 3000 characters fails validation""" - with self.assertRaises(SlackObjectFormationError): - RawTextObject(text="a" * 3001).to_dict() - - def test_text_length_validation_at_max(self): - """Test that text at exactly 3000 characters passes validation""" - obj = RawTextObject(text="a" * 3000) - obj.to_dict() # Should not raise - def test_text_length_validation_min(self): """Test that empty text fails validation""" with self.assertRaises(SlackObjectFormationError): From 2d4a18e14cb8d6b6cc1d3ff29260deea80e99444 Mon Sep 17 00:00:00 2001 From: Eden Zimbelman Date: Wed, 19 Nov 2025 15:40:10 -0800 Subject: [PATCH 7/7] revert: remove web_client table block tests --- .../web/test_web_client_table_block.py | 176 ------------------ 1 file changed, 176 deletions(-) delete mode 100644 tests/slack_sdk/web/test_web_client_table_block.py diff --git a/tests/slack_sdk/web/test_web_client_table_block.py b/tests/slack_sdk/web/test_web_client_table_block.py deleted file mode 100644 index 6b175ef50..000000000 --- a/tests/slack_sdk/web/test_web_client_table_block.py +++ /dev/null @@ -1,176 +0,0 @@ -import unittest -from unittest.mock import Mock, patch - -from slack_sdk import WebClient -from slack_sdk.models.blocks import TableBlock, RawTextObject - - -class TestWebClientTableBlock(unittest.TestCase): - """Tests to verify the correct approach for sending table blocks""" - - def setUp(self): - self.client = WebClient(token="xoxb-test-token", base_url="http://localhost:8888") - - # Create a sample table block - self.table = TableBlock( - rows=[ - [{"type": "raw_text", "text": "Product"}, {"type": "raw_text", "text": "Price"}], - [{"type": "raw_text", "text": "Widget"}, {"type": "raw_text", "text": "$10"}], - [{"type": "raw_text", "text": "Gadget"}, {"type": "raw_text", "text": "$20"}], - ], - column_settings=[{"is_wrapped": True}, {"align": "right"}], - ) - - @patch("slack_sdk.web.client.WebClient.api_call") - def test_table_in_attachments_blocks(self, mock_api_call): - """Test sending table block in attachments.blocks (documented approach)""" - mock_api_call.return_value = {"ok": True, "channel": "C123", "ts": "1234567890.123456"} - - # Method 1: Table in attachments - response = self.client.chat_postMessage( - channel="C123456789", - text="Here's a table:", - attachments=[{"blocks": [self.table.to_dict()]}], - ) - - # Verify the call was made - self.assertTrue(mock_api_call.called) - call_args = mock_api_call.call_args - - # Check that attachments were passed correctly (WebClient wraps in 'json' key) - self.assertIn("json", call_args[1]) - json_data = call_args[1]["json"] - self.assertIn("attachments", json_data) - attachments = json_data["attachments"] - self.assertEqual(len(attachments), 1) - self.assertIn("blocks", attachments[0]) - self.assertEqual(attachments[0]["blocks"][0]["type"], "table") - - @patch("slack_sdk.web.client.WebClient.api_call") - def test_table_in_top_level_blocks(self, mock_api_call): - """Test sending table block in top-level blocks array (alternative approach)""" - mock_api_call.return_value = {"ok": True, "channel": "C123", "ts": "1234567890.123456"} - - # Method 2: Table in top-level blocks - response = self.client.chat_postMessage( - channel="C123456789", - text="Here's a table:", - blocks=[self.table.to_dict()], - ) - - # Verify the call was made - self.assertTrue(mock_api_call.called) - call_args = mock_api_call.call_args - - # Check that blocks were passed correctly (WebClient wraps in 'json' key) - self.assertIn("json", call_args[1]) - json_data = call_args[1]["json"] - self.assertIn("blocks", json_data) - blocks = json_data["blocks"] - self.assertEqual(len(blocks), 1) - self.assertEqual(blocks[0]["type"], "table") - - @patch("slack_sdk.web.client.WebClient.api_call") - def test_table_with_raw_text_object_helper(self, mock_api_call): - """Test creating table using RawTextObject helper""" - mock_api_call.return_value = {"ok": True, "channel": "C123", "ts": "1234567890.123456"} - - # Create table using RawTextObject helper - table_with_helper = TableBlock( - rows=[ - [RawTextObject(text="Name").to_dict(), RawTextObject(text="Age").to_dict()], - [RawTextObject(text="Alice").to_dict(), RawTextObject(text="30").to_dict()], - ] - ) - - response = self.client.chat_postMessage( - channel="C123456789", - text="Table with helpers:", - attachments=[{"blocks": [table_with_helper.to_dict()]}], - ) - - # Verify the call was made and table structure is correct - self.assertTrue(mock_api_call.called) - call_args = mock_api_call.call_args - json_data = call_args[1]["json"] - attachments = json_data["attachments"] - table_dict = attachments[0]["blocks"][0] - - self.assertEqual(table_dict["type"], "table") - self.assertEqual(table_dict["rows"][0][0]["type"], "raw_text") - self.assertEqual(table_dict["rows"][0][0]["text"], "Name") - - def test_table_to_dict_serialization(self): - """Test that TableBlock.to_dict() produces correct structure""" - table_dict = self.table.to_dict() - - # Verify structure - self.assertEqual(table_dict["type"], "table") - self.assertIn("rows", table_dict) - self.assertIn("column_settings", table_dict) - - # Verify rows structure - self.assertEqual(len(table_dict["rows"]), 3) - self.assertEqual(table_dict["rows"][0][0]["type"], "raw_text") - self.assertEqual(table_dict["rows"][0][0]["text"], "Product") - - # Verify column settings - self.assertEqual(len(table_dict["column_settings"]), 2) - self.assertEqual(table_dict["column_settings"][0]["is_wrapped"], True) - self.assertEqual(table_dict["column_settings"][1]["align"], "right") - - @patch("slack_sdk.web.client.WebClient.api_call") - def test_multiple_tables_not_allowed(self, mock_api_call): - """Test that only one table per message is allowed (per Slack documentation)""" - mock_api_call.return_value = {"ok": True, "channel": "C123", "ts": "1234567890.123456"} - - table2 = TableBlock(rows=[[{"type": "raw_text", "text": "Another table"}]]) - - # According to docs, this should only send one table - # The SDK doesn't enforce this, but Slack API will return error - response = self.client.chat_postMessage( - channel="C123456789", - text="Multiple tables:", - attachments=[ - {"blocks": [self.table.to_dict()]}, - {"blocks": [table2.to_dict()]}, - ], - ) - - # Verify both tables were sent (SDK allows it, but Slack API will reject) - self.assertTrue(mock_api_call.called) - call_args = mock_api_call.call_args - json_data = call_args[1]["json"] - attachments = json_data["attachments"] - self.assertEqual(len(attachments), 2) - - def test_table_with_rich_text_cells(self): - """Test table with rich_text cells (links, formatting)""" - table_with_rich_text = TableBlock( - rows=[ - [{"type": "raw_text", "text": "Header A"}, {"type": "raw_text", "text": "Header B"}], - [ - {"type": "raw_text", "text": "Data 1A"}, - { - "type": "rich_text", - "elements": [ - { - "type": "rich_text_section", - "elements": [{"text": "Data 1B", "type": "link", "url": "https://slack.com"}], - } - ], - }, - ], - ] - ) - - table_dict = table_with_rich_text.to_dict() - - # Verify mixed cell types - self.assertEqual(table_dict["rows"][0][0]["type"], "raw_text") - self.assertEqual(table_dict["rows"][1][1]["type"], "rich_text") - self.assertIn("elements", table_dict["rows"][1][1]) - - -if __name__ == "__main__": - unittest.main()