Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions slack_sdk/models/blocks/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
MarkdownBlock,
RichTextBlock,
SectionBlock,
TableBlock,
VideoBlock,
)

Expand Down Expand Up @@ -133,6 +134,7 @@
"InputBlock",
"MarkdownBlock",
"SectionBlock",
"TableBlock",
"VideoBlock",
"RichTextBlock",
]
45 changes: 45 additions & 0 deletions slack_sdk/models/blocks/blocks.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
136 changes: 136 additions & 0 deletions tests/slack_sdk/models/test_blocks.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
RichTextSectionElement,
SectionBlock,
StaticSelectElement,
TableBlock,
VideoBlock,
)
from slack_sdk.models.blocks.basic_components import FeedbackButtonObject, SlackFile
Expand Down Expand Up @@ -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())