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

Issue:
blocks/attachments as str for chat.* API calls should be clearly supported
The use case reported at https://github.com/slackapi/python-slack-sdk/pull/1259#issuecomment-1237007209 has been supported for a long time but it was **not by design**. 

```python
client = WebClient(token="....")
client.chat_postMessage(text="fallback", blocks="{ JSON string here }")
```

The `blocks` and `attachments` arguments for chat.postMessage API etc. are supposed to be `Sequence[Block | Attachment | dict]` as of today. However, passing the whole blocks/attachments as a single str should be a relatively common use case. In future versions, the use case should be clearly covered in both type hints and its implementation.

### 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.models** (UI component builders)
- [ ] **slack_sdk.oauth** (OAuth Flow Utilities)
- [ ] **slack_sdk.socket_mode** (Socket Mode client)
- [ ] **slack_sdk.audit_logs** (Audit Logs API client)
- [ ] **slack_sdk.scim** (SCIM API client)
- [ ] **slack_sdk.rtm** (RTM client)
- [ ] **slack_sdk.signature** (Request Signature Verifier)

### Requirements

Please read the [Contributing guidelines](https://github.com/slackapi/python-slack-sdk/blob/main/.github/contributing.md) and [Code of Conduct](https://slackhq.github.io/code-of-conduct) before creating this issue or pull request. By submitting, you are agreeing to those rules.


Pull request:
Fix #1261 blocks/attachments as str for chat.* API calls should be clearly supported
## Summary

This pull request resolves #1261

### 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)
- [ ] `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/async_client.py b/slack_sdk/web/async_client.py
index b03a9d1..24e4d05 100644
--- a/slack_sdk/web/async_client.py
+++ b/slack_sdk/web/async_client.py
@@ -1985,8 +1985,8 @@ class AsyncWebClient(AsyncBaseClient):
         user: str,
         text: Optional[str] = None,
         as_user: Optional[bool] = None,
-        attachments: Optional[Sequence[Union[Dict, Attachment]]] = None,
-        blocks: Optional[Sequence[Union[Dict, Block]]] = None,
+        attachments: Optional[Union[str, Sequence[Union[Dict, Attachment]]]] = None,
+        blocks: Optional[Union[str, Sequence[Union[Dict, Block]]]] = None,
         thread_ts: Optional[str] = None,
         icon_emoji: Optional[str] = None,
         icon_url: Optional[str] = None,
@@ -2026,8 +2026,8 @@ class AsyncWebClient(AsyncBaseClient):
         channel: str,
         text: Optional[str] = None,
         as_user: Optional[bool] = None,
-        attachments: Optional[Sequence[Union[Dict, Attachment]]] = None,
-        blocks: Optional[Sequence[Union[Dict, Block]]] = None,
+        attachments: Optional[Union[str, Sequence[Union[Dict, Attachment]]]] = None,
+        blocks: Optional[Union[str, Sequence[Union[Dict, Block]]]] = None,
         thread_ts: Optional[str] = None,
         reply_broadcast: Optional[bool] = None,
         unfurl_links: Optional[bool] = None,
@@ -2081,8 +2081,8 @@ class AsyncWebClient(AsyncBaseClient):
         post_at: Union[str, int],
         text: str,
         as_user: Optional[bool] = None,
-        attachments: Optional[Sequence[Union[Dict, Attachment]]] = None,
-        blocks: Optional[Sequence[Union[Dict, Block]]] = None,
+        attachments: Optional[Union[str, Sequence[Union[Dict, Attachment]]]] = None,
+        blocks: Optional[Union[str, Sequence[Union[Dict, Block]]]] = None,
         thread_ts: Optional[str] = None,
         parse: Optional[str] = None,
         reply_broadcast: Optional[bool] = None,
@@ -2126,7 +2126,7 @@ class AsyncWebClient(AsyncBaseClient):
         source: Optional[str] = None,
         unfurl_id: Optional[str] = None,
         unfurls: Dict[str, Dict],
-        user_auth_blocks: Optional[Sequence[Union[Dict, Block]]] = None,
+        user_auth_blocks: Optional[Union[str, Sequence[Union[Dict, Block]]]] = None,
         user_auth_message: Optional[str] = None,
         user_auth_required: Optional[bool] = None,
         user_auth_url: Optional[str] = None,
@@ -2158,8 +2158,8 @@ class AsyncWebClient(AsyncBaseClient):
         channel: str,
         ts: str,
         text: Optional[str] = None,
-        attachments: Optional[Sequence[Union[Dict, Attachment]]] = None,
-        blocks: Optional[Sequence[Union[Dict, Block]]] = None,
+        attachments: Optional[Union[str, Sequence[Union[Dict, Attachment]]]] = None,
+        blocks: Optional[Union[str, Sequence[Union[Dict, Block]]]] = None,
         as_user: Optional[bool] = None,
         file_ids: Optional[Union[str, Sequence[str]]] = None,
         link_names: Optional[bool] = None,
diff --git a/slack_sdk/web/client.py b/slack_sdk/web/client.py
index c66e90d..1e9b18c 100644
--- a/slack_sdk/web/client.py
+++ b/slack_sdk/web/client.py
@@ -1976,8 +1976,8 @@ class WebClient(BaseClient):
         user: str,
         text: Optional[str] = None,
         as_user: Optional[bool] = None,
-        attachments: Optional[Sequence[Union[Dict, Attachment]]] = None,
-        blocks: Optional[Sequence[Union[Dict, Block]]] = None,
+        attachments: Optional[Union[str, Sequence[Union[Dict, Attachment]]]] = None,
+        blocks: Optional[Union[str, Sequence[Union[Dict, Block]]]] = None,
         thread_ts: Optional[str] = None,
         icon_emoji: Optional[str] = None,
         icon_url: Optional[str] = None,
@@ -2017,8 +2017,8 @@ class WebClient(BaseClient):
         channel: str,
         text: Optional[str] = None,
         as_user: Optional[bool] = None,
-        attachments: Optional[Sequence[Union[Dict, Attachment]]] = None,
-        blocks: Optional[Sequence[Union[Dict, Block]]] = None,
+        attachments: Optional[Union[str, Sequence[Union[Dict, Attachment]]]] = None,
+        blocks: Optional[Union[str, Sequence[Union[Dict, Block]]]] = None,
         thread_ts: Optional[str] = None,
         reply_broadcast: Optional[bool] = None,
         unfurl_links: Optional[bool] = None,
@@ -2072,8 +2072,8 @@ class WebClient(BaseClient):
         post_at: Union[str, int],
         text: str,
         as_user: Optional[bool] = None,
-        attachments: Optional[Sequence[Union[Dict, Attachment]]] = None,
-        blocks: Optional[Sequence[Union[Dict, Block]]] = None,
+        attachments: Optional[Union[str, Sequence[Union[Dict, Attachment]]]] = None,
+        blocks: Optional[Union[str, Sequence[Union[Dict, Block]]]] = None,
         thread_ts: Optional[str] = None,
         parse: Optional[str] = None,
         reply_broadcast: Optional[bool] = None,
@@ -2117,7 +2117,7 @@ class WebClient(BaseClient):
         source: Optional[str] = None,
         unfurl_id: Optional[str] = None,
         unfurls: Dict[str, Dict],
-        user_auth_blocks: Optional[Sequence[Union[Dict, Block]]] = None,
+        user_auth_blocks: Optional[Union[str, Sequence[Union[Dict, Block]]]] = None,
         user_auth_message: Optional[str] = None,
         user_auth_required: Optional[bool] = None,
         user_auth_url: Optional[str] = None,
@@ -2149,8 +2149,8 @@ class WebClient(BaseClient):
         channel: str,
         ts: str,
         text: Optional[str] = None,
-        attachments: Optional[Sequence[Union[Dict, Attachment]]] = None,
-        blocks: Optional[Sequence[Union[Dict, Block]]] = None,
+        attachments: Optional[Union[str, Sequence[Union[Dict, Attachment]]]] = None,
+        blocks: Optional[Union[str, Sequence[Union[Dict, Block]]]] = None,
         as_user: Optional[bool] = None,
         file_ids: Optional[Union[str, Sequence[str]]] = None,
         link_names: Optional[bool] = None,
diff --git a/slack_sdk/web/internal_utils.py b/slack_sdk/web/internal_utils.py
index 9242bc3..9dcb49c 100644
--- a/slack_sdk/web/internal_utils.py
+++ b/slack_sdk/web/internal_utils.py
@@ -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, Sequence):
+    if blocks is not None and isinstance(blocks, Sequence) and (not isinstance(blocks, str)):
         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, Sequence):
+    if attachments is not None and isinstance(attachments, Sequence) and (not isinstance(attachments, str)):
         dict_attachments = [to_dict(a) for a in attachments]
         kwargs.update({"attachments": dict_attachments})
 
diff --git a/slack_sdk/web/legacy_client.py b/slack_sdk/web/legacy_client.py
index 7b28b31..784fae6 100644
--- a/slack_sdk/web/legacy_client.py
+++ b/slack_sdk/web/legacy_client.py
@@ -1987,8 +1987,8 @@ class LegacyWebClient(LegacyBaseClient):
         user: str,
         text: Optional[str] = None,
         as_user: Optional[bool] = None,
-        attachments: Optional[Sequence[Union[Dict, Attachment]]] = None,
-        blocks: Optional[Sequence[Union[Dict, Block]]] = None,
+        attachments: Optional[Union[str, Sequence[Union[Dict, Attachment]]]] = None,
+        blocks: Optional[Union[str, Sequence[Union[Dict, Block]]]] = None,
         thread_ts: Optional[str] = None,
         icon_emoji: Optional[str] = None,
         icon_url: Optional[str] = None,
@@ -2028,8 +2028,8 @@ class LegacyWebClient(LegacyBaseClient):
         channel: str,
         text: Optional[str] = None,
         as_user: Optional[bool] = None,
-        attachments: Optional[Sequence[Union[Dict, Attachment]]] = None,
-        blocks: Optional[Sequence[Union[Dict, Block]]] = None,
+        attachments: Optional[Union[str, Sequence[Union[Dict, Attachment]]]] = None,
+        blocks: Optional[Union[str, Sequence[Union[Dict, Block]]]] = None,
         thread_ts: Optional[str] = None,
         reply_broadcast: Optional[bool] = None,
         unfurl_links: Optional[bool] = None,
@@ -2083,8 +2083,8 @@ class LegacyWebClient(LegacyBaseClient):
         post_at: Union[str, int],
         text: str,
         as_user: Optional[bool] = None,
-        attachments: Optional[Sequence[Union[Dict, Attachment]]] = None,
-        blocks: Optional[Sequence[Union[Dict, Block]]] = None,
+        attachments: Optional[Union[str, Sequence[Union[Dict, Attachment]]]] = None,
+        blocks: Optional[Union[str, Sequence[Union[Dict, Block]]]] = None,
         thread_ts: Optional[str] = None,
         parse: Optional[str] = None,
         reply_broadcast: Optional[bool] = None,
@@ -2128,7 +2128,7 @@ class LegacyWebClient(LegacyBaseClient):
         source: Optional[str] = None,
         unfurl_id: Optional[str] = None,
         unfurls: Dict[str, Dict],
-        user_auth_blocks: Optional[Sequence[Union[Dict, Block]]] = None,
+        user_auth_blocks: Optional[Union[str, Sequence[Union[Dict, Block]]]] = None,
         user_auth_message: Optional[str] = None,
         user_auth_required: Optional[bool] = None,
         user_auth_url: Optional[str] = None,
@@ -2160,8 +2160,8 @@ class LegacyWebClient(LegacyBaseClient):
         channel: str,
         ts: str,
         text: Optional[str] = None,
-        attachments: Optional[Sequence[Union[Dict, Attachment]]] = None,
-        blocks: Optional[Sequence[Union[Dict, Block]]] = None,
+        attachments: Optional[Union[str, Sequence[Union[Dict, Attachment]]]] = None,
+        blocks: Optional[Union[str, Sequence[Union[Dict, Block]]]] = None,
         as_user: Optional[bool] = None,
         file_ids: Optional[Union[str, Sequence[str]]] = None,
         link_names: Optional[bool] = None,


Test patch:
diff --git a/tests/slack_sdk/web/test_internal_utils.py b/tests/slack_sdk/web/test_internal_utils.py
index 8372240..0056819 100644
--- a/tests/slack_sdk/web/test_internal_utils.py
+++ b/tests/slack_sdk/web/test_internal_utils.py
@@ -1,3 +1,4 @@
+import json
 import unittest
 from typing import Dict, Sequence, Union
 
@@ -23,44 +24,41 @@ class TestInternalUtils(unittest.TestCase):
             """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)
+    def test_can_parse_sequence_of_blocks(self):
+        for blocks in [
+            [Block(block_id="42"), Block(block_id="24")],  # list
+            (Block(block_id="42"), Block(block_id="24")),  # tuple
+        ]:
+            kwargs = {"blocks": blocks}
+            _parse_web_class_objects(kwargs)
+            assert kwargs["blocks"]
+            for block in kwargs["blocks"]:
+                assert isinstance(block, Dict)
+
+    def test_can_parse_sequence_of_attachments(self):
+        for attachments in [
+            [Attachment(text="foo"), Attachment(text="bar")],  # list
+            (
+                Attachment(text="foo"),
+                Attachment(text="bar"),
+            ),  # tuple
+        ]:
+            kwargs = {"attachments": attachments}
+            _parse_web_class_objects(kwargs)
+            assert kwargs["attachments"]
+            for attachment in kwargs["attachments"]:
+                assert isinstance(attachment, Dict)
+
+    def test_can_parse_str_blocks(self):
+        input = json.dumps([Block(block_id="42").to_dict(), Block(block_id="24").to_dict()])
+        kwargs = {"blocks": input}
+        _parse_web_class_objects(kwargs)
+        assert isinstance(kwargs["blocks"], str)
+        assert input == kwargs["blocks"]
+
+    def test_can_parse_str_attachments(self):
+        input = json.dumps([Attachment(text="foo").to_dict(), Attachment(text="bar").to_dict()])
+        kwargs = {"attachments": input}
+        _parse_web_class_objects(kwargs)
+        assert isinstance(kwargs["attachments"], str)
+        assert input == kwargs["attachments"]