From cb7c20401bd98c1c99c0462b8ef4c89444c8ad1f Mon Sep 17 00:00:00 2001 From: Eden Zimbelman Date: Fri, 26 Jun 2026 16:45:27 -0700 Subject: [PATCH 1/7] feat(models): add data_table Block Kit block Add the net-new `data_table` block to the Block Kit models. The block displays structured, paginated data with a required caption. - DataTableBlock with rows (required), caption (required), page_size, row_header_column_index, and block_id - register the type in Block.parse and export it from slack_sdk.models.blocks - validators for required rows/caption and page_size range (1-100) - tests mirroring the existing TableBlock tests Ref: https://docs.slack.dev/reference/block-kit/blocks/data-table-block Co-Authored-By: Claude --- slack_sdk/models/blocks/__init__.py | 2 + slack_sdk/models/blocks/blocks.py | 62 ++++++++++++++++++++++ tests/slack_sdk/models/test_blocks.py | 76 +++++++++++++++++++++++++++ 3 files changed, 140 insertions(+) diff --git a/slack_sdk/models/blocks/__init__.py b/slack_sdk/models/blocks/__init__.py index 6a26ed958..5cb4f0772 100644 --- a/slack_sdk/models/blocks/__init__.py +++ b/slack_sdk/models/blocks/__init__.py @@ -68,6 +68,7 @@ CarouselBlock, ContextActionsBlock, ContextBlock, + DataTableBlock, DividerBlock, FileBlock, HeaderBlock, @@ -139,6 +140,7 @@ "CarouselBlock", "ContextActionsBlock", "ContextBlock", + "DataTableBlock", "DividerBlock", "FileBlock", "HeaderBlock", diff --git a/slack_sdk/models/blocks/blocks.py b/slack_sdk/models/blocks/blocks.py index db4de1f3a..1fcf41d51 100644 --- a/slack_sdk/models/blocks/blocks.py +++ b/slack_sdk/models/blocks/blocks.py @@ -108,6 +108,8 @@ def parse(cls, block: Union[dict, "Block"]) -> Optional["Block"]: return AlertBlock(**block) elif type == CarouselBlock.type: return CarouselBlock(**block) + elif type == DataTableBlock.type: + return DataTableBlock(**block) else: cls.logger.warning(f"Unknown block detected and skipped ({block})") return None @@ -1030,3 +1032,63 @@ def _validate_elements_present(self): @JsonValidator(f"elements attribute cannot exceed {elements_max_length} cards") def _validate_elements_length(self): return self.elements is None or len(self.elements) <= self.elements_max_length + + +class DataTableBlock(Block): + type = "data_table" + rows_max_length = 101 + columns_max_length = 20 + page_size_min = 1 + page_size_max = 100 + + @property + def attributes(self) -> Set[str]: # type: ignore[override] + return super().attributes.union({"rows", "caption", "page_size", "row_header_column_index"}) + + def __init__( + self, + *, + rows: Sequence[Sequence[Dict[str, Any]]], + caption: str, + page_size: Optional[int] = None, + row_header_column_index: Optional[int] = None, + block_id: Optional[str] = None, + **others: dict, + ): + """Displays structured, paginated data in a table with a required caption. + https://docs.slack.dev/reference/block-kit/blocks/data-table-block + + Args: + rows (required): An array consisting of table rows. Minimum 2 rows (header plus one data row) + and maximum 101 rows (header plus 100 data rows). All rows must have an identical column + count, with a maximum of 20 columns. Each cell has a type of raw_text, raw_number, or + rich_text. The total character limit across all cells is 10,000. + caption (required): A caption for the table; used as the value for the HTML caption element. + page_size: The number of rows to show per page. Min 1, Max 100. Defaults to 5 if omitted. + row_header_column_index: The 0-based index of the column that uniquely identifies each row + (the row header). Defaults to 0 if omitted. + 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. + """ + super().__init__(type=self.type, block_id=block_id) + show_unknown_key_warning(self, others) + + self.rows = rows + self.caption = caption + self.page_size = page_size + self.row_header_column_index = row_header_column_index + + @JsonValidator("rows attribute must be specified") + def _validate_rows(self): + return self.rows is not None and len(self.rows) > 0 + + @JsonValidator("caption attribute must be specified") + def _validate_caption(self): + return self.caption is not None + + @JsonValidator(f"page_size must be between {page_size_min} and {page_size_max}") + def _validate_page_size(self): + return self.page_size is None or self.page_size_min <= self.page_size <= self.page_size_max diff --git a/tests/slack_sdk/models/test_blocks.py b/tests/slack_sdk/models/test_blocks.py index fc9ff3266..22de85b7d 100644 --- a/tests/slack_sdk/models/test_blocks.py +++ b/tests/slack_sdk/models/test_blocks.py @@ -12,6 +12,7 @@ CarouselBlock, ContextActionsBlock, ContextBlock, + DataTableBlock, DividerBlock, FileBlock, HeaderBlock, @@ -1556,6 +1557,81 @@ def test_with_raw_text_object_helper(self): self.assertDictEqual(expected, block.to_dict()) +class DataTableBlockTests(unittest.TestCase): + def test_document(self): + """Test basic data table block from Slack documentation example""" + input = { + "type": "data_table", + "caption": "Quarterly sales by region", + "rows": [ + [{"type": "raw_text", "text": "Region"}, {"type": "raw_text", "text": "Sales"}], + [{"type": "raw_text", "text": "West"}, {"type": "raw_number", "value": 120, "text": "120"}], + [{"type": "raw_text", "text": "East"}, {"type": "raw_number", "value": 95, "text": "95"}], + ], + } + self.assertDictEqual(input, DataTableBlock(**input).to_dict()) + self.assertDictEqual(input, Block.parse(input).to_dict()) + + def test_all_fields(self): + """Test data table block with every optional field set""" + input = { + "type": "data_table", + "block_id": "data-table-123", + "caption": "User directory", + "page_size": 25, + "row_header_column_index": 1, + "rows": [ + [{"type": "raw_text", "text": "ID"}, {"type": "raw_text", "text": "Name"}], + [{"type": "raw_number", "value": 1, "text": "1"}, {"type": "raw_text", "text": "Alice"}], + ], + } + self.assertDictEqual(input, DataTableBlock(**input).to_dict()) + self.assertDictEqual(input, Block.parse(input).to_dict()) + + def test_with_rich_text(self): + """Test data table block with rich_text cells""" + input = { + "type": "data_table", + "caption": "Links", + "rows": [ + [{"type": "raw_text", "text": "Site"}], + [ + { + "type": "rich_text", + "elements": [ + { + "type": "rich_text_section", + "elements": [{"text": "Slack", "type": "link", "url": "https://slack.com"}], + } + ], + }, + ], + ], + } + self.assertDictEqual(input, DataTableBlock(**input).to_dict()) + self.assertDictEqual(input, Block.parse(input).to_dict()) + + def test_rows_validation(self): + """Test that empty rows fail validation""" + with self.assertRaises(SlackObjectFormationError): + DataTableBlock(caption="empty", rows=[]).to_dict() + + def test_caption_required(self): + """Test that DataTableBlock requires a caption argument""" + with self.assertRaises(TypeError): + DataTableBlock(rows=[[{"type": "raw_text", "text": "A"}]]) + + def test_page_size_validation(self): + """Test that page_size outside the allowed range fails validation""" + rows = [[{"type": "raw_text", "text": "A"}], [{"type": "raw_text", "text": "B"}]] + with self.assertRaises(SlackObjectFormationError): + DataTableBlock(caption="too small", rows=rows, page_size=0).to_dict() + with self.assertRaises(SlackObjectFormationError): + DataTableBlock(caption="too big", rows=rows, page_size=101).to_dict() + # A valid page_size should pass + DataTableBlock(caption="ok", rows=rows, page_size=50).to_dict() + + class CardBlockTests(unittest.TestCase): def test_document(self): input = { From 9342f94b282ee945ccf86dcd22a8da98a0414f74 Mon Sep 17 00:00:00 2001 From: Eden Zimbelman Date: Tue, 8 Sep 2026 17:12:44 -0700 Subject: [PATCH 2/7] feat(models): add RawNumberObject + type data_table rows for cell objects Mirrors node-slack-sdk#2638: data_table rows accept raw_text, raw_number, and rich_text cells. Adds RawNumberObject (value + display text, min-length-1) and widens DataTableBlock.rows to Union[RawTextObject, RawNumberObject, RichTextBlock, Dict]. Field wording tracks the docs/node model verbatim. Co-Authored-By: Claude --- slack_sdk/models/blocks/__init__.py | 2 + slack_sdk/models/blocks/basic_components.py | 24 +++++++++ slack_sdk/models/blocks/blocks.py | 19 ++++--- tests/slack_sdk/models/test_blocks.py | 56 +++++++++++++++++++++ 4 files changed, 91 insertions(+), 10 deletions(-) diff --git a/slack_sdk/models/blocks/__init__.py b/slack_sdk/models/blocks/__init__.py index 2584edf1c..43cb2f99b 100644 --- a/slack_sdk/models/blocks/__init__.py +++ b/slack_sdk/models/blocks/__init__.py @@ -16,6 +16,7 @@ Option, OptionGroup, PlainTextObject, + RawNumberObject, RawTextObject, TableBlockColumnSettings, TextObject, @@ -94,6 +95,7 @@ "Option", "OptionGroup", "PlainTextObject", + "RawNumberObject", "RawTextObject", "TableBlockColumnSettings", "TextObject", diff --git a/slack_sdk/models/blocks/basic_components.py b/slack_sdk/models/blocks/basic_components.py index c86007bab..af9b1a0df 100644 --- a/slack_sdk/models/blocks/basic_components.py +++ b/slack_sdk/models/blocks/basic_components.py @@ -186,6 +186,30 @@ def _validate_text_min_length(self): return len(self.text) >= 1 +class RawNumberObject(JsonObject): + """raw_number typed object.""" + + type = "raw_number" + attributes = {"value", "text", "type"} + logger = logging.getLogger(__name__) + + def __init__(self, *, value: Union[int, float], text: str): + """Defines an object containing a numeric value. + + https://docs.slack.dev/reference/block-kit/blocks/data-table-block + + Args: + value (required): The numeric value. + text (required): The text used to display the value. The minimum length is 1 character. + """ + self.value = value + self.text = text + + @JsonValidator("text attribute must have at least 1 character") + def _validate_text_min_length(self): + return len(self.text) >= 1 + + class TableBlockColumnSettings(JsonObject): """Column settings for TableBlock columns.""" diff --git a/slack_sdk/models/blocks/blocks.py b/slack_sdk/models/blocks/blocks.py index b0eb8b991..16f4813ac 100644 --- a/slack_sdk/models/blocks/blocks.py +++ b/slack_sdk/models/blocks/blocks.py @@ -10,6 +10,7 @@ from .basic_components import ( MarkdownTextObject, PlainTextObject, + RawNumberObject, RawTextObject, SlackFile, TableBlockColumnSettings, @@ -1162,8 +1163,6 @@ def _validate_elements_length(self): class DataTableBlock(Block): type = "data_table" - rows_max_length = 101 - columns_max_length = 20 page_size_min = 1 page_size_max = 100 @@ -1174,25 +1173,25 @@ def attributes(self) -> Set[str]: # type: ignore[override] def __init__( self, *, - rows: Sequence[Sequence[Dict[str, Any]]], + rows: Sequence[Sequence[Union[RawTextObject, RawNumberObject, RichTextBlock, Dict[str, Any]]]], caption: str, page_size: Optional[int] = None, row_header_column_index: Optional[int] = None, block_id: Optional[str] = None, **others: dict, ): - """Displays structured, paginated data in a table with a required caption. + """Displays rich tables that support pagination, sorting, filtering, and interactivity. + https://docs.slack.dev/reference/block-kit/blocks/data-table-block Args: - rows (required): An array consisting of table rows. Minimum 2 rows (header plus one data row) - and maximum 101 rows (header plus 100 data rows). All rows must have an identical column - count, with a maximum of 20 columns. Each cell has a type of raw_text, raw_number, or - rich_text. The total character limit across all cells is 10,000. + rows (required): An array consisting of table rows. Each cell has a type of raw_text, + raw_number, or rich_text. caption (required): A caption for the table; used as the value for the HTML caption element. - page_size: The number of rows to show per page. Min 1, Max 100. Defaults to 5 if omitted. + page_size: Number of rows per page. Min 1, Max 100. Defaults to 5 if omitted. row_header_column_index: The 0-based index of the column that uniquely identifies each row - (the row header). Defaults to 0 if omitted. + (the row header). This column is treated as the row's primary identifier for screen readers. + Defaults to 0 if omitted. 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. diff --git a/tests/slack_sdk/models/test_blocks.py b/tests/slack_sdk/models/test_blocks.py index 493383b61..8ec3ff0f8 100644 --- a/tests/slack_sdk/models/test_blocks.py +++ b/tests/slack_sdk/models/test_blocks.py @@ -27,6 +27,7 @@ OverflowMenuElement, PlainTextObject, PlanBlock, + RawNumberObject, RawTextObject, RichTextBlock, RichTextElementParts, @@ -1401,6 +1402,40 @@ def test_attributes(self): self.assertNotIn("emoji", obj.to_dict()) +# ---------------------------------------------- +# RawNumberObject +# ---------------------------------------------- + + +class RawNumberObjectTests(unittest.TestCase): + def test_basic_creation(self): + """Test basic RawNumberObject creation""" + obj = RawNumberObject(value=120, text="120") + expected = {"type": "raw_number", "value": 120, "text": "120"} + self.assertDictEqual(expected, obj.to_dict()) + + def test_float_value(self): + """Test RawNumberObject accepts a float value""" + obj = RawNumberObject(value=3.14, text="3.14") + expected = {"type": "raw_number", "value": 3.14, "text": "3.14"} + self.assertDictEqual(expected, obj.to_dict()) + + def test_text_length_validation_min(self): + """Test that empty text fails validation""" + with self.assertRaises(SlackObjectFormationError): + RawNumberObject(value=0, text="").to_dict() + + def test_text_length_validation_at_min(self): + """Test that text with 1 character passes validation""" + obj = RawNumberObject(value=1, text="1") + obj.to_dict() # Should not raise + + def test_attributes(self): + """Test that RawNumberObject only has value, text, and type attributes""" + obj = RawNumberObject(value=1, text="1") + self.assertEqual(obj.attributes, {"value", "text", "type"}) + + # ---------------------------------------------- # Table # ---------------------------------------------- @@ -1679,6 +1714,27 @@ def test_with_rich_text(self): self.assertDictEqual(input, DataTableBlock(**input).to_dict()) self.assertDictEqual(input, Block.parse(input).to_dict()) + def test_with_cell_object_helpers(self): + """Test data table block built from RawTextObject and RawNumberObject helpers""" + block = DataTableBlock( + caption="Quarterly sales by region", + rows=[ + [RawTextObject(text="Region").to_dict(), RawTextObject(text="Sales").to_dict()], + [RawTextObject(text="West").to_dict(), RawNumberObject(value=120, text="120").to_dict()], + [RawTextObject(text="East").to_dict(), RawNumberObject(value=95, text="95").to_dict()], + ], + ) + expected = { + "type": "data_table", + "caption": "Quarterly sales by region", + "rows": [ + [{"type": "raw_text", "text": "Region"}, {"type": "raw_text", "text": "Sales"}], + [{"type": "raw_text", "text": "West"}, {"type": "raw_number", "value": 120, "text": "120"}], + [{"type": "raw_text", "text": "East"}, {"type": "raw_number", "value": 95, "text": "95"}], + ], + } + self.assertDictEqual(expected, block.to_dict()) + def test_rows_validation(self): """Test that empty rows fail validation""" with self.assertRaises(SlackObjectFormationError): From a58f1261fff62bdfb1050f357c82c7f247056225 Mon Sep 17 00:00:00 2001 From: Eden Zimbelman Date: Tue, 8 Sep 2026 17:18:38 -0700 Subject: [PATCH 3/7] docs(models): drop data-table-block URL from RawNumberObject docstring MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit raw_number has no dedicated composition-object reference page (it is only documented inline on the data-table-block page). Match node-slack-sdk#2638's RawNumberElement, which deliberately carries no @see link for the same reason. The DataTableBlock docstring keeps the URL — that page is the block's own home. Co-Authored-By: Claude --- slack_sdk/models/blocks/basic_components.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/slack_sdk/models/blocks/basic_components.py b/slack_sdk/models/blocks/basic_components.py index af9b1a0df..484563132 100644 --- a/slack_sdk/models/blocks/basic_components.py +++ b/slack_sdk/models/blocks/basic_components.py @@ -196,8 +196,6 @@ class RawNumberObject(JsonObject): def __init__(self, *, value: Union[int, float], text: str): """Defines an object containing a numeric value. - https://docs.slack.dev/reference/block-kit/blocks/data-table-block - Args: value (required): The numeric value. text (required): The text used to display the value. The minimum length is 1 character. From 96eecbbbbe0efeb873b96f945b676d0c80e85355 Mon Sep 17 00:00:00 2001 From: Eden Zimbelman Date: Tue, 8 Sep 2026 17:30:09 -0700 Subject: [PATCH 4/7] docs(models): use terse block_id line in DataTableBlock docstring Match the recently-added sibling blocks (AlertBlock, CardBlock, ContainerBlock, CarouselBlock), which all use the one-line block_id description. DataTableBlock had inherited the legacy verbose 5-line variant. Co-Authored-By: Claude --- slack_sdk/models/blocks/blocks.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/slack_sdk/models/blocks/blocks.py b/slack_sdk/models/blocks/blocks.py index 16f4813ac..393cb8208 100644 --- a/slack_sdk/models/blocks/blocks.py +++ b/slack_sdk/models/blocks/blocks.py @@ -1193,10 +1193,6 @@ def __init__( (the row header). This column is treated as the row's primary identifier for screen readers. Defaults to 0 if omitted. 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. """ super().__init__(type=self.type, block_id=block_id) show_unknown_key_warning(self, others) From 6ef7ed927b51d81248230c24dc5c6751a6e3df6c Mon Sep 17 00:00:00 2001 From: Eden Zimbelman Date: Tue, 8 Sep 2026 17:32:35 -0700 Subject: [PATCH 5/7] docs(models): match rows description to docs verbatim in DataTableBlock MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The docs data-table-block Fields table (and node-slack-sdk#2638) describe rows as exactly 'An array consisting of table rows.' — drop the extra cell-type sentence that was appended, per the verbatim-docs-field convention. Co-Authored-By: Claude --- slack_sdk/models/blocks/blocks.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/slack_sdk/models/blocks/blocks.py b/slack_sdk/models/blocks/blocks.py index 393cb8208..0d808bb1c 100644 --- a/slack_sdk/models/blocks/blocks.py +++ b/slack_sdk/models/blocks/blocks.py @@ -1185,8 +1185,7 @@ def __init__( https://docs.slack.dev/reference/block-kit/blocks/data-table-block Args: - rows (required): An array consisting of table rows. Each cell has a type of raw_text, - raw_number, or rich_text. + rows (required): An array consisting of table rows. caption (required): A caption for the table; used as the value for the HTML caption element. page_size: Number of rows per page. Min 1, Max 100. Defaults to 5 if omitted. row_header_column_index: The 0-based index of the column that uniquely identifies each row From e1cfb35d8009b7026318c5ba8e300701dc0d228f Mon Sep 17 00:00:00 2001 From: Eden Zimbelman Date: Tue, 8 Sep 2026 17:33:29 -0700 Subject: [PATCH 6/7] refactor(models): order RawNumberObject before RawTextObject MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Matches both alphabetical order (Number < Text) and node-slack-sdk#2638's composition-objects order (RawNumberElement before RawTextElement). No behavior change — RawNumberObject extends JsonObject, so it has no dependency on the sibling class. Co-Authored-By: Claude --- slack_sdk/models/blocks/basic_components.py | 44 ++++++++++----------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/slack_sdk/models/blocks/basic_components.py b/slack_sdk/models/blocks/basic_components.py index 484563132..451c8bb97 100644 --- a/slack_sdk/models/blocks/basic_components.py +++ b/slack_sdk/models/blocks/basic_components.py @@ -151,6 +151,28 @@ def direct_from_link(link: Link, title: str = "") -> Dict[str, Any]: return MarkdownTextObject.from_link(link, title).to_dict() +class RawNumberObject(JsonObject): + """raw_number typed object.""" + + type = "raw_number" + attributes = {"value", "text", "type"} + logger = logging.getLogger(__name__) + + def __init__(self, *, value: Union[int, float], text: str): + """Defines an object containing a numeric value. + + Args: + value (required): The numeric value. + text (required): The text used to display the value. The minimum length is 1 character. + """ + self.value = value + self.text = text + + @JsonValidator("text attribute must have at least 1 character") + def _validate_text_min_length(self): + return len(self.text) >= 1 + + class RawTextObject(TextObject): """raw_text typed text object.""" @@ -186,28 +208,6 @@ def _validate_text_min_length(self): return len(self.text) >= 1 -class RawNumberObject(JsonObject): - """raw_number typed object.""" - - type = "raw_number" - attributes = {"value", "text", "type"} - logger = logging.getLogger(__name__) - - def __init__(self, *, value: Union[int, float], text: str): - """Defines an object containing a numeric value. - - Args: - value (required): The numeric value. - text (required): The text used to display the value. The minimum length is 1 character. - """ - self.value = value - self.text = text - - @JsonValidator("text attribute must have at least 1 character") - def _validate_text_min_length(self): - return len(self.text) >= 1 - - class TableBlockColumnSettings(JsonObject): """Column settings for TableBlock columns.""" From b4d3a3e7033569b825768a5aa74843a0f4693f52 Mon Sep 17 00:00:00 2001 From: Eden Zimbelman Date: Tue, 8 Sep 2026 17:40:27 -0700 Subject: [PATCH 7/7] test(models): order data_table tests to mirror source class order Match the class order in the source files: - RawNumberObjectTests before RawTextObjectTests (Number before Text, as in basic_components.py and node-slack-sdk#2638). - DataTableBlockTests moved to the end, after CarouselBlockTests, mirroring DataTableBlock's position at the end of blocks.py. Pre-existing test ordering left untouched (out of scope for this PR). Co-Authored-By: Claude --- tests/slack_sdk/models/test_blocks.py | 260 +++++++++++++------------- 1 file changed, 130 insertions(+), 130 deletions(-) diff --git a/tests/slack_sdk/models/test_blocks.py b/tests/slack_sdk/models/test_blocks.py index 8ec3ff0f8..2a8a46aec 100644 --- a/tests/slack_sdk/models/test_blocks.py +++ b/tests/slack_sdk/models/test_blocks.py @@ -1360,6 +1360,40 @@ def test_parsing_empty_block_elements(self): self.assertIsNotNone(block_dict["elements"][3].get("elements")) +# ---------------------------------------------- +# RawNumberObject +# ---------------------------------------------- + + +class RawNumberObjectTests(unittest.TestCase): + def test_basic_creation(self): + """Test basic RawNumberObject creation""" + obj = RawNumberObject(value=120, text="120") + expected = {"type": "raw_number", "value": 120, "text": "120"} + self.assertDictEqual(expected, obj.to_dict()) + + def test_float_value(self): + """Test RawNumberObject accepts a float value""" + obj = RawNumberObject(value=3.14, text="3.14") + expected = {"type": "raw_number", "value": 3.14, "text": "3.14"} + self.assertDictEqual(expected, obj.to_dict()) + + def test_text_length_validation_min(self): + """Test that empty text fails validation""" + with self.assertRaises(SlackObjectFormationError): + RawNumberObject(value=0, text="").to_dict() + + def test_text_length_validation_at_min(self): + """Test that text with 1 character passes validation""" + obj = RawNumberObject(value=1, text="1") + obj.to_dict() # Should not raise + + def test_attributes(self): + """Test that RawNumberObject only has value, text, and type attributes""" + obj = RawNumberObject(value=1, text="1") + self.assertEqual(obj.attributes, {"value", "text", "type"}) + + # ---------------------------------------------- # RawTextObject # ---------------------------------------------- @@ -1402,40 +1436,6 @@ def test_attributes(self): self.assertNotIn("emoji", obj.to_dict()) -# ---------------------------------------------- -# RawNumberObject -# ---------------------------------------------- - - -class RawNumberObjectTests(unittest.TestCase): - def test_basic_creation(self): - """Test basic RawNumberObject creation""" - obj = RawNumberObject(value=120, text="120") - expected = {"type": "raw_number", "value": 120, "text": "120"} - self.assertDictEqual(expected, obj.to_dict()) - - def test_float_value(self): - """Test RawNumberObject accepts a float value""" - obj = RawNumberObject(value=3.14, text="3.14") - expected = {"type": "raw_number", "value": 3.14, "text": "3.14"} - self.assertDictEqual(expected, obj.to_dict()) - - def test_text_length_validation_min(self): - """Test that empty text fails validation""" - with self.assertRaises(SlackObjectFormationError): - RawNumberObject(value=0, text="").to_dict() - - def test_text_length_validation_at_min(self): - """Test that text with 1 character passes validation""" - obj = RawNumberObject(value=1, text="1") - obj.to_dict() # Should not raise - - def test_attributes(self): - """Test that RawNumberObject only has value, text, and type attributes""" - obj = RawNumberObject(value=1, text="1") - self.assertEqual(obj.attributes, {"value", "text", "type"}) - - # ---------------------------------------------- # Table # ---------------------------------------------- @@ -1660,102 +1660,6 @@ def test_with_raw_text_object_helper(self): self.assertDictEqual(expected, block.to_dict()) -class DataTableBlockTests(unittest.TestCase): - def test_document(self): - """Test basic data table block from Slack documentation example""" - input = { - "type": "data_table", - "caption": "Quarterly sales by region", - "rows": [ - [{"type": "raw_text", "text": "Region"}, {"type": "raw_text", "text": "Sales"}], - [{"type": "raw_text", "text": "West"}, {"type": "raw_number", "value": 120, "text": "120"}], - [{"type": "raw_text", "text": "East"}, {"type": "raw_number", "value": 95, "text": "95"}], - ], - } - self.assertDictEqual(input, DataTableBlock(**input).to_dict()) - self.assertDictEqual(input, Block.parse(input).to_dict()) - - def test_all_fields(self): - """Test data table block with every optional field set""" - input = { - "type": "data_table", - "block_id": "data-table-123", - "caption": "User directory", - "page_size": 25, - "row_header_column_index": 1, - "rows": [ - [{"type": "raw_text", "text": "ID"}, {"type": "raw_text", "text": "Name"}], - [{"type": "raw_number", "value": 1, "text": "1"}, {"type": "raw_text", "text": "Alice"}], - ], - } - self.assertDictEqual(input, DataTableBlock(**input).to_dict()) - self.assertDictEqual(input, Block.parse(input).to_dict()) - - def test_with_rich_text(self): - """Test data table block with rich_text cells""" - input = { - "type": "data_table", - "caption": "Links", - "rows": [ - [{"type": "raw_text", "text": "Site"}], - [ - { - "type": "rich_text", - "elements": [ - { - "type": "rich_text_section", - "elements": [{"text": "Slack", "type": "link", "url": "https://slack.com"}], - } - ], - }, - ], - ], - } - self.assertDictEqual(input, DataTableBlock(**input).to_dict()) - self.assertDictEqual(input, Block.parse(input).to_dict()) - - def test_with_cell_object_helpers(self): - """Test data table block built from RawTextObject and RawNumberObject helpers""" - block = DataTableBlock( - caption="Quarterly sales by region", - rows=[ - [RawTextObject(text="Region").to_dict(), RawTextObject(text="Sales").to_dict()], - [RawTextObject(text="West").to_dict(), RawNumberObject(value=120, text="120").to_dict()], - [RawTextObject(text="East").to_dict(), RawNumberObject(value=95, text="95").to_dict()], - ], - ) - expected = { - "type": "data_table", - "caption": "Quarterly sales by region", - "rows": [ - [{"type": "raw_text", "text": "Region"}, {"type": "raw_text", "text": "Sales"}], - [{"type": "raw_text", "text": "West"}, {"type": "raw_number", "value": 120, "text": "120"}], - [{"type": "raw_text", "text": "East"}, {"type": "raw_number", "value": 95, "text": "95"}], - ], - } - self.assertDictEqual(expected, block.to_dict()) - - def test_rows_validation(self): - """Test that empty rows fail validation""" - with self.assertRaises(SlackObjectFormationError): - DataTableBlock(caption="empty", rows=[]).to_dict() - - def test_caption_required(self): - """Test that DataTableBlock requires a caption argument""" - with self.assertRaises(TypeError): - DataTableBlock(rows=[[{"type": "raw_text", "text": "A"}]]) - - def test_page_size_validation(self): - """Test that page_size outside the allowed range fails validation""" - rows = [[{"type": "raw_text", "text": "A"}], [{"type": "raw_text", "text": "B"}]] - with self.assertRaises(SlackObjectFormationError): - DataTableBlock(caption="too small", rows=rows, page_size=0).to_dict() - with self.assertRaises(SlackObjectFormationError): - DataTableBlock(caption="too big", rows=rows, page_size=101).to_dict() - # A valid page_size should pass - DataTableBlock(caption="ok", rows=rows, page_size=50).to_dict() - - class CardBlockTests(unittest.TestCase): def test_document(self): input = { @@ -2055,3 +1959,99 @@ def test_single_card(self): def test_empty_elements_validation(self): with self.assertRaises(SlackObjectFormationError): CarouselBlock(elements=[]).validate_json() + + +class DataTableBlockTests(unittest.TestCase): + def test_document(self): + """Test basic data table block from Slack documentation example""" + input = { + "type": "data_table", + "caption": "Quarterly sales by region", + "rows": [ + [{"type": "raw_text", "text": "Region"}, {"type": "raw_text", "text": "Sales"}], + [{"type": "raw_text", "text": "West"}, {"type": "raw_number", "value": 120, "text": "120"}], + [{"type": "raw_text", "text": "East"}, {"type": "raw_number", "value": 95, "text": "95"}], + ], + } + self.assertDictEqual(input, DataTableBlock(**input).to_dict()) + self.assertDictEqual(input, Block.parse(input).to_dict()) + + def test_all_fields(self): + """Test data table block with every optional field set""" + input = { + "type": "data_table", + "block_id": "data-table-123", + "caption": "User directory", + "page_size": 25, + "row_header_column_index": 1, + "rows": [ + [{"type": "raw_text", "text": "ID"}, {"type": "raw_text", "text": "Name"}], + [{"type": "raw_number", "value": 1, "text": "1"}, {"type": "raw_text", "text": "Alice"}], + ], + } + self.assertDictEqual(input, DataTableBlock(**input).to_dict()) + self.assertDictEqual(input, Block.parse(input).to_dict()) + + def test_with_rich_text(self): + """Test data table block with rich_text cells""" + input = { + "type": "data_table", + "caption": "Links", + "rows": [ + [{"type": "raw_text", "text": "Site"}], + [ + { + "type": "rich_text", + "elements": [ + { + "type": "rich_text_section", + "elements": [{"text": "Slack", "type": "link", "url": "https://slack.com"}], + } + ], + }, + ], + ], + } + self.assertDictEqual(input, DataTableBlock(**input).to_dict()) + self.assertDictEqual(input, Block.parse(input).to_dict()) + + def test_with_cell_object_helpers(self): + """Test data table block built from RawTextObject and RawNumberObject helpers""" + block = DataTableBlock( + caption="Quarterly sales by region", + rows=[ + [RawTextObject(text="Region").to_dict(), RawTextObject(text="Sales").to_dict()], + [RawTextObject(text="West").to_dict(), RawNumberObject(value=120, text="120").to_dict()], + [RawTextObject(text="East").to_dict(), RawNumberObject(value=95, text="95").to_dict()], + ], + ) + expected = { + "type": "data_table", + "caption": "Quarterly sales by region", + "rows": [ + [{"type": "raw_text", "text": "Region"}, {"type": "raw_text", "text": "Sales"}], + [{"type": "raw_text", "text": "West"}, {"type": "raw_number", "value": 120, "text": "120"}], + [{"type": "raw_text", "text": "East"}, {"type": "raw_number", "value": 95, "text": "95"}], + ], + } + self.assertDictEqual(expected, block.to_dict()) + + def test_rows_validation(self): + """Test that empty rows fail validation""" + with self.assertRaises(SlackObjectFormationError): + DataTableBlock(caption="empty", rows=[]).to_dict() + + def test_caption_required(self): + """Test that DataTableBlock requires a caption argument""" + with self.assertRaises(TypeError): + DataTableBlock(rows=[[{"type": "raw_text", "text": "A"}]]) + + def test_page_size_validation(self): + """Test that page_size outside the allowed range fails validation""" + rows = [[{"type": "raw_text", "text": "A"}], [{"type": "raw_text", "text": "B"}]] + with self.assertRaises(SlackObjectFormationError): + DataTableBlock(caption="too small", rows=rows, page_size=0).to_dict() + with self.assertRaises(SlackObjectFormationError): + DataTableBlock(caption="too big", rows=rows, page_size=101).to_dict() + # A valid page_size should pass + DataTableBlock(caption="ok", rows=rows, page_size=50).to_dict()