Repository: slackapi/python-slack-sdk
Pull request #1576 (2024-10-28 00:19:31)

Enable rich_text_* elements to have an empty 'elements' property
## Summary

This pull request resolves a bug where a few rich_text block element representation in slack_sdk.models package does not an empty value for "element" property. An empty array must be allowed for the property while most other properties in Block Kid do not allow empty arrays.

### Category <!-- place an `x` in each of the `[ ]`  -->

- [ ] **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)
- [x] **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` (Documents)
- [ ] `/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.


codecov[bot]: ## [Codecov](https://app.codecov.io/gh/slackapi/python-slack-sdk/pull/1576?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=slackapi) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 84.90%. Comparing base [(`5c8f595`)](https://app.codecov.io/gh/slackapi/python-slack-sdk/commit/5c8f5954935402f10c968cf0b22335e5c50e4f9a?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=slackapi) to head [(`00d919e`)](https://app.codecov.io/gh/slackapi/python-slack-sdk/commit/00d919e0614868a976513a7abafbaac341dab494?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=slackapi).

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main    #1576      +/-   ##
==========================================
+ Coverage   84.87%   84.90%   +0.02%     
==========================================
  Files         113      113              
  Lines       12599    12604       +5     
==========================================
+ Hits        10693    10701       +8     
+ Misses       1906     1903       -3     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/slackapi/python-slack-sdk/pull/1576?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=slackapi).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=slackapi).


Patch:
diff --git a/docs/package-lock.json b/docs/package-lock.json
index c6f46a3..62097d5 100644
--- a/docs/package-lock.json
+++ b/docs/package-lock.json
@@ -7435,9 +7435,9 @@
       }
     },
     "node_modules/http-proxy-middleware": {
-      "version": "2.0.6",
-      "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz",
-      "integrity": "sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw==",
+      "version": "2.0.7",
+      "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.7.tgz",
+      "integrity": "sha512-fgVY8AV7qU7z/MmXJ/rxwbrtQH4jBQ9m7kp3llF0liB7glmFeVZFBepQb32T3y8n8k2+AEYuMPCpinYW+/CuRA==",
       "dependencies": {
         "@types/http-proxy": "^1.17.8",
         "http-proxy": "^1.18.1",
diff --git a/slack_sdk/models/basic_objects.py b/slack_sdk/models/basic_objects.py
index 232ec21..bd9ffe5 100644
--- a/slack_sdk/models/basic_objects.py
+++ b/slack_sdk/models/basic_objects.py
@@ -12,6 +12,15 @@ class BaseObject:
         return f"<slack_sdk.{self.__class__.__name__}>"
 
 
+# Usually, Block Kit components do not allow an empty array for a property value, but there are some exceptions.
+EMPTY_ALLOWED_TYPE_AND_PROPERTY_LIST = [
+    {"type": "rich_text_section", "property": "elements"},
+    {"type": "rich_text_list", "property": "elements"},
+    {"type": "rich_text_preformatted", "property": "elements"},
+    {"type": "rich_text_quote", "property": "elements"},
+]
+
+
 class JsonObject(BaseObject, metaclass=ABCMeta):
     """The base class for JSON serializable class objects"""
 
@@ -51,6 +60,14 @@ class JsonObject(BaseObject, metaclass=ABCMeta):
             value = getattr(self, key, None)
             if value is None:
                 return False
+
+            # Usually, Block Kit components do not allow an empty array for a property value, but there are some exceptions.
+            # The following code deals with these exceptions:
+            type_value = getattr(self, "type", None)
+            for empty_allowed in EMPTY_ALLOWED_TYPE_AND_PROPERTY_LIST:
+                if type_value == empty_allowed["type"] and key == empty_allowed["property"]:
+                    return True
+
             has_len = getattr(value, "__len__", None) is not None
             if has_len:  # skipcq: PYL-R1705
                 return len(value) > 0


Test patch:
diff --git a/tests/slack_sdk/models/test_blocks.py b/tests/slack_sdk/models/test_blocks.py
index 65f0baf..940669b 100644
--- a/tests/slack_sdk/models/test_blocks.py
+++ b/tests/slack_sdk/models/test_blocks.py
@@ -1143,3 +1143,27 @@ class RichTextBlockTests(unittest.TestCase):
         self.assertIsInstance(block.elements[3], RichTextListElement)
         self.assertIsInstance(block.elements[3].elements[0], RichTextSectionElement)
         self.assertIsInstance(block.elements[3].elements[0].elements[0], RichTextElementParts.Text)
+
+    def test_parsing_empty_block_elements(self):
+        empty_element_block = {
+            "block_id": "my-block",
+            "type": "rich_text",
+            "elements": [
+                {"type": "rich_text_section", "elements": []},
+                {"type": "rich_text_list", "style": "bullet", "elements": []},
+                {"type": "rich_text_preformatted", "elements": []},
+                {"type": "rich_text_quote", "elements": []},
+            ],
+        }
+        block = RichTextBlock(**empty_element_block)
+        self.assertIsInstance(block.elements[0], RichTextSectionElement)
+        self.assertIsNotNone(block.elements[0].elements)
+        self.assertIsNotNone(block.elements[1].elements)
+        self.assertIsNotNone(block.elements[2].elements)
+        self.assertIsNotNone(block.elements[3].elements)
+
+        block_dict = block.to_dict()
+        self.assertIsNotNone(block_dict["elements"][0].get("elements"))
+        self.assertIsNotNone(block_dict["elements"][1].get("elements"))
+        self.assertIsNotNone(block_dict["elements"][2].get("elements"))
+        self.assertIsNotNone(block_dict["elements"][3].get("elements"))