Repository: slackapi/python-slack-sdk (python)

Issue:
Tuple value for blocks argument does not work for Web API calls
Both [WebClient](https://github.com/slackapi/python-slack-sdk/blob/d2fd7ac7fbd6f7dbb832b56e8d662a27a65ed614/slack_sdk/web/client.py#L2021) and [AsyncWebClient](https://github.com/slackapi/python-slack-sdk/blob/d2fd7ac7fbd6f7dbb832b56e8d662a27a65ed614/slack_sdk/web/async_client.py#L2030) accept a sequence of blocks (and attachments) but [`_parse_web_class_objects`](https://github.com/slackapi/python-slack-sdk/blob/d2fd7ac7fbd6f7dbb832b56e8d662a27a65ed614/slack_sdk/web/internal_utils.py#L181-L203) fails to serialize the objects into dictionaries since it only checks for lists via `isinstance(blocks, list)`, therefore disagreeing with the clients' signature.

Impact on:
- both sync and async clients
- `chat_postEphemeral`, `chat_postMessage`, `chat_scheduleMessage`, `chat_unfurl`, `chat_update`
- both `Block` and `Attachment`

### Reproducible in:

#### The Slack SDK version

`slack-sdk=3.18.1`

#### Python runtime version

`Python 3.10.3`

#### OS info

```
ProductName:	macOS
ProductVersion:	12.5.1
BuildVersion:	21G83
Darwin Kernel Version 21.6.0: Wed Aug 10 14:28:23 PDT 2022; root:xnu-8020.141.5~2/RELEASE_ARM64_T6000
```

#### Steps to reproduce:

```py
client = WebClient(token="token")

# a tuple of blocks
blocks: Sequence[Block] = (
    SectionBlock(text="foo"),
    SectionBlock(text="bar"),
)

# works
client.chat_postMessage(channel="#channel", blocks=list(blocks))

# raises: Object of type SectionBlock is not JSON serializable
client.chat_postMessage(channel="#channel", blocks=blocks)
```

### Expected result:

Since both clients accept a `Sequence`, one should be able to pass both a list or a tuple.

### Actual result:

`TypeError: Object of type SectionBlock is not JSON serializable` since the blocks weren't serialized into dicts.


Pull request:
Fix #1258 Tuple value for blocks argument does not work for Web API calls
## Summary
Fix for `Tuple value for blocks argument does not work for Web API calls`

```py
blocks: Sequence[Block] = (
    SectionBlock(text="foo"),
    SectionBlock(text="bar"),
)

# fixes calls with non-list blocks and attachments
client.chat_postMessage(channel="#channel", blocks=blocks)
```

Resolves https://github.com/slackapi/python-slack-sdk/issues/1258

### Category (place an `x` in each of the `[ ]`)

- [x] **slack_sdk.web.WebClient (sync/async)** (Web API client)
- [ ] **slack_sdk.webhook.WebhookClient (sync/async)** (Incoming Webhook, response_url sender)
- [ ] **slack_sdk.socket_mode** (Socket Mode client)
- [ ] **slack_sdk.signature** (Request Signature Verifier)
- [ ] **slack_sdk.oauth** (OAuth Flow Utilities)
- [ ] **slack_sdk.models** (UI component builders)
- [ ] **slack_sdk.scim** (SCIM API client)
- [ ] **slack_sdk.audit_logs** (Audit Logs API client)
- [ ] **slack_sdk.rtm_v2** (RTM client)
- [ ] `/docs-src` (Documents, have you run `./scripts/docs.sh`?)
- [ ] `/docs-src-v2` (Documents, have you run `./scripts/docs-v2.sh`?)
- [ ] `/tutorial` (PythOnBoardingBot tutorial)
- [x] `tests`/`integration_tests` (Automated tests for this library)

## Requirements (place an `x` in each `[ ]`)

- [x] I've read and understood the [Contributing Guidelines](https://github.com/slackapi/python-slack-sdk/blob/main/.github/contributing.md) and have done my best effort to follow them.
- [x] I've read and agree to the [Code of Conduct](https://slackhq.github.io/code-of-conduct).
- [x] I've run `python3 -m venv .venv && source .venv/bin/activate && ./scripts/run_validation.sh` after making the changes.


Patch:
diff --git a/slack_sdk/web/internal_utils.py b/slack_sdk/web/internal_utils.py
index f1a7ad9..9242bc3 100644
--- a/slack_sdk/web/internal_utils.py
+++ b/slack_sdk/web/internal_utils.py
@@ -4,7 +4,7 @@ import platform
 import sys
 import warnings
 from ssl import SSLContext
-from typing import Dict, Union, Optional, Any, Sequence
+from typing import Any, Dict, Optional, Sequence, Union
 from urllib.parse import urljoin
 
 from slack_sdk import version
@@ -189,12 +189,12 @@ def _parse_web_class_objects(kwargs) -> None:
         return obj
 
     blocks = kwargs.get("blocks", None)
-    if blocks is not None and isinstance(blocks, list):
+    if blocks is not None and isinstance(blocks, Sequence):
         dict_blocks = [to_dict(b) for b in blocks]
         kwargs.update({"blocks": dict_blocks})
 
     attachments = kwargs.get("attachments", None)
-    if attachments is not None and isinstance(attachments, list):
+    if attachments is not None and isinstance(attachments, Sequence):
         dict_attachments = [to_dict(a) for a in attachments]
         kwargs.update({"attachments": dict_attachments})
 


Test patch:
diff --git a/tests/slack_sdk/web/test_internal_utils.py b/tests/slack_sdk/web/test_internal_utils.py
index d5cad1c..b60e1aa 100644
--- a/tests/slack_sdk/web/test_internal_utils.py
+++ b/tests/slack_sdk/web/test_internal_utils.py
@@ -1,5 +1,14 @@
 import unittest
-from slack_sdk.web.internal_utils import _build_unexpected_body_error_message
+from typing import Dict, Sequence, Union
+
+import pytest
+
+from slack_sdk.models.attachments import Attachment
+from slack_sdk.models.blocks import Block
+from slack_sdk.web.internal_utils import (
+    _build_unexpected_body_error_message,
+    _parse_web_class_objects
+)
 
 
 class TestInternalUtils(unittest.TestCase):
@@ -16,3 +25,33 @@ class TestInternalUtils(unittest.TestCase):
         assert message.startswith(
             """Received a response in a non-JSON format: <!DOCTYPE html><html lang="en"><head><meta charset="utf-8">"""
         )
+
+
+@pytest.mark.parametrize("initial_blocks", [
+    [Block(block_id="42"), Block(block_id="24")],  # list
+    (Block(block_id="42"), Block(block_id="24"),),  # tuple
+])
+def test_can_parse_sequence_of_blocks(initial_blocks: Sequence[Union[Dict, Block]]):
+    kwargs = {"blocks": initial_blocks}
+
+    _parse_web_class_objects(kwargs)
+
+    assert kwargs["blocks"]
+
+    for block in kwargs["blocks"]:
+        assert isinstance(block, Dict)
+
+
+@pytest.mark.parametrize("initial_attachments", [
+    [Attachment(text="foo"), Attachment(text="bar")],  # list
+    (Attachment(text="foo"), Attachment(text="bar"),),  # tuple
+])
+def test_can_parse_sequence_of_attachments(initial_attachments: Sequence[Union[Dict, Attachment]]):
+    kwargs = {"attachments": initial_attachments}
+
+    _parse_web_class_objects(kwargs)
+
+    assert kwargs["attachments"]
+
+    for attachment in kwargs["attachments"]:
+        assert isinstance(attachment, Dict)