Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
108 changes: 104 additions & 4 deletions blockkit/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ def validate(self, field_name: str, value: Any) -> None:
)


# TODO: add support for checking generic types like Typed(list[RawText | RichText])
class Typed(FieldValidator):
def __init__(self, *types: type):
if not types:
Expand Down Expand Up @@ -449,10 +450,19 @@ def build(self):
if hasattr(field.value, "build"):
obj[field.name] = field.value.build()
if isinstance(field.value, list | tuple | set):
obj[field.name] = [
item.build() if hasattr(item, "build") else item
for item in field.value
]
items = []
for item in field.value:
if isinstance(item, list | tuple | set):
nested_items = [
nested.build() if hasattr(nested, "build") else nested
for nested in item
]
items.append(nested_items)
else:
items.append(item.build() if hasattr(item, "build") else item)

obj[field.name] = items

return obj


Expand Down Expand Up @@ -796,6 +806,21 @@ def type(self, type: Literal["plain_text", "mrkdwn"]) -> Self:
)


class RawText(Component):
"""
Raw text object
"""

def __init__(self, text: str = None):
super().__init__("raw_text")
self.text(text)

def text(self, text: str | None) -> Self:
return self._add_field(
"text", text, validators=[Typed(str), Required(), Length(1, 10000)]
)


class Confirm(Component, StyleMixin):
"""
Confirmation dialog
Expand Down Expand Up @@ -2986,6 +3011,80 @@ def expand(self, expand: bool | None = True) -> Self:
return self._add_field("expand", expand, validators=[Typed(bool)])


class ColumnSettings(Component):
"""
Column settings

Lets you change text alignment and text wrapping behavior for table columns

Slack docs:
https://docs.slack.dev/reference/block-kit/blocks/table-block/
"""

LEFT: Final[Literal["left"]] = "left"
CENTER: Final[Literal["center"]] = "center"
RIGHT: Final[Literal["right"]] = "right"

def __init__(
self,
align: Literal["left", "center", "right"] | None = None,
is_wrapped: bool | None = None,
):
super().__init__()
self.align(align)
self.is_wrapped(is_wrapped)

def align(self, align: Literal["left", "center", "right"] | None) -> Self:
return self._add_field(
"align",
align,
validators=[Typed(str), Strings(self.LEFT, self.CENTER, self.RIGHT)],
)

def is_wrapped(self, is_wrapped: bool = True) -> Self:
return self._add_field("is_wrapped", is_wrapped, validators=[Typed(bool)])


class Table(Component, BlockIdMixin):
"""
Table block

Displays structured information in a table.

Slack docs:
https://docs.slack.dev/reference/block-kit/blocks/table-block
"""

def __init__(
self,
rows: list[list[RawText | RichText]] | None = None,
column_settings: list[ColumnSettings] | None = None,
block_id: str | None = None,
):
super().__init__("table")
self.rows(*rows or ())
self.column_settings(*column_settings or ())
self.block_id(block_id)

def rows(self, *rows: list[RawText | RichText]) -> Self:
return self._add_field(
"rows",
list(rows),
validators=[Typed(list), Required(), Length(1, 100)],
)

def add_row(self, *row: list[RawText | RichText]) -> Self:
return self._add_field_value("rows", list(row))

def column_settings(self, *column_settings: ColumnSettings) -> Self:
return self._add_field(
"column_settings", list(column_settings), validators=[Typed(ColumnSettings)]
)

def add_column_setting(self, column_setting: ColumnSettings) -> Self:
return self._add_field_value("column_settings", column_setting)


class Video(Component, BlockIdMixin):
"""
Video block
Expand Down Expand Up @@ -3088,6 +3187,7 @@ def author_name(self, author_name: str | None) -> Self:
| Markdown
| RichText
| Section
| Table
| Video
)

Expand Down
130 changes: 130 additions & 0 deletions tests/test_core.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from blockkit.core import ColumnSettings
from blockkit.core import RawText
from datetime import date, datetime, time
from zoneinfo import ZoneInfo

Expand Down Expand Up @@ -27,6 +29,7 @@
Image,
ImageEl,
Input,
Table,
InputParameter,
Markdown,
Message,
Expand Down Expand Up @@ -3290,6 +3293,133 @@ def test_builds_fields(self):
assert got == want


class TestTable:
def test_builds(self):
want = {
"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",
}
],
}
],
},
],
[
{"type": "raw_text", "text": "Data 2A"},
{
"type": "rich_text",
"elements": [
{
"type": "rich_text_section",
"elements": [
{
"text": "Data 2B",
"type": "link",
"url": "https://slack.com",
}
],
}
],
},
],
],
}

got = Table(
column_settings=[
ColumnSettings(is_wrapped=True),
ColumnSettings(align=ColumnSettings.RIGHT),
],
rows=[
[
RawText(text="Header A"),
RawText(text="Header B"),
],
[
RawText(text="Data 1A"),
RichText(
elements=[
RichTextSection(
elements=[
RichLinkEl(
text="Data 1B",
url="https://slack.com",
)
]
)
]
),
],
[
{"type": "raw_text", "text": "Data 2A"},
{
"type": "rich_text",
"elements": [
{
"type": "rich_text_section",
"elements": [
{
"text": "Data 2B",
"type": "link",
"url": "https://slack.com",
}
],
}
],
},
],
],
).build()
assert got == want

got = (
Table()
.add_column_setting(ColumnSettings(is_wrapped=True))
.add_column_setting(ColumnSettings(align=ColumnSettings.RIGHT))
.add_row(
RawText().text("Header A"),
RawText().text("Header B"),
)
.add_row(
RawText().text("Data 1A"),
RichText().add_element(
RichTextSection().add_element(
RichLinkEl().text("Data 1B").url("https://slack.com")
)
),
)
.add_row(
RawText().text("Data 2A"),
RichText().add_element(
RichTextSection().add_element(
RichLinkEl().text("Data 2B").url("https://slack.com")
)
),
)
).build()
assert got == want


class TestVideo:
def test_builds(self):
want = {
Expand Down
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.