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

Issue:
slack_sdk.models.blocks.Option's "description" value is invalid in blocks
The `description` value in `slack_sdk.models.blocks.Option` class has been invalid. The values must be an `plain_text` typed text object. However, the current implementation embeds the values as a string value in JSON payloads.

See also: https://api.slack.com/reference/block-kit/composition-objects#option

Thanks a lot for reporting this:
https://twitter.com/notmatt/status/1378092778379341826

### Reproducible in:

```python
option = Option(
    text="*this is plain_text text*",
    value="value-0",
    description="The most important choice!"
)
```

#### The Slack SDK version

Any versions

#### Python runtime version

Any versions

#### OS info

Any 

### Expected result:

```json
{
  "blocks": [
    {
      "type": "section",
      "text": {
        "type": "mrkdwn",
        "text": "test"
      },
      "accessory": {
        "type": "static_select",
        "placeholder": {
          "type": "plain_text",
          "text": "Select an item"
        },
        "options": [
          {
            "text": {
              "type": "plain_text",
              "text": "*this is plain_text text*"
            },
            "value": "value-0",
            "description": {
              "type": "plain_text",
              "text": "The most important choice!"
            }
          }
        ],
        "action_id": "static_select-action"
      }
    }
  ]
}
```

### Actual result:

```json
{
  "blocks": [
    {
      "type": "section",
      "text": {
        "type": "mrkdwn",
        "text": "test"
      },
      "accessory": {
        "type": "static_select",
        "placeholder": {
          "type": "plain_text",
          "text": "Select an item"
        },
        "options": [
          {
            "text": {
              "type": "plain_text",
              "text": "*this is plain_text text*"
            },
            "value": "value-0",
            "description": "The most important choice!"
          }
        ],
        "action_id": "static_select-action"
      }
    }
  ]
}
```

### Requirements

For general questions/issues about Slack API platform or its server-side, could you submit questions at https://my.slack.com/help/requests/new instead. :bow:

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 #990 slack_sdk.models.blocks.Option's description value is invalid in blocks
## Summary

This pull request resolves #990 . Refer to the issue for the details.

### 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)
- [x] **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)
- [ ] `/docs-src` (Documents, have you run `./docs.sh`?)
- [ ] `/docs-src-v2` (Documents, have you run `./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 `python setup.py validate` after making the changes.


Patch:
diff --git a/slack_sdk/models/blocks/basic_components.py b/slack_sdk/models/blocks/basic_components.py
index 279237a..7178f13 100644
--- a/slack_sdk/models/blocks/basic_components.py
+++ b/slack_sdk/models/blocks/basic_components.py
@@ -158,7 +158,7 @@ class Option(JsonObject):
         value: str,
         label: Optional[str] = None,
         text: Optional[Union[str, dict, TextObject]] = None,  # Block Kit
-        description: Optional[str] = None,
+        description: Optional[Union[str, dict, TextObject]] = None,
         url: Optional[str] = None,
         **others: dict,
     ):
@@ -198,7 +198,21 @@ class Option(JsonObject):
         self.label: Optional[str] = self._label
 
         self.value: str = value
-        self.description: Optional[str] = description
+
+        # for backward-compatibility with version 2.0-2.5, the following fields return str values
+        if isinstance(description, str):
+            self.description = description
+            self._block_description = PlainTextObject.from_str(description)
+        elif isinstance(description, dict):
+            self.description = description["text"]
+            self._block_description = TextObject.parse(description)
+        elif isinstance(description, TextObject):
+            self.description = description.text
+            self._block_description = description
+        else:
+            self.description = None
+            self._block_description = None
+
         # A URL to load in the user's browser when the option is clicked.
         # The url attribute is only available in overflow menus.
         # Maximum length for this field is 3000 characters.
@@ -249,7 +263,9 @@ class Option(JsonObject):
         self.validate_json()
         if option_type == "dialog":  # skipcq: PYL-R1705
             return {"label": self.label, "value": self.value}
-        elif option_type == "action":
+        elif option_type == "action" or option_type == "attachment":
+            # "action" can be confusing but it means a legacy message action in attachments
+            # we don't remove the type name for backward compatibility though
             json = {"text": self.label, "value": self.value}
             if self.description is not None:
                 json["description"] = self.description
@@ -260,8 +276,8 @@ class Option(JsonObject):
                 "text": text.to_dict(),
                 "value": self.value,
             }
-            if self.description:
-                json["description"] = self.description
+            if self._block_description:
+                json["description"] = self._block_description.to_dict()
             if self.url:
                 json["url"] = self.url
             return json


Test patch:
diff --git a/tests/slack_sdk/models/test_objects.py b/tests/slack_sdk/models/test_objects.py
index 6ac56a2..f04057e 100644
--- a/tests/slack_sdk/models/test_objects.py
+++ b/tests/slack_sdk/models/test_objects.py
@@ -414,6 +414,63 @@ class OptionTests(unittest.TestCase):
         with self.assertRaises(SlackObjectFormationError):
             Option(label="option_1", value=STRING_301_CHARS).to_dict("text")
 
+    def test_valid_description_for_blocks(self):
+        option = Option(label="label", value="v", description="this is an option")
+        self.assertDictEqual(
+            option.to_dict(),
+            {
+                "text": {
+                    "type": "plain_text",
+                    "text": "label",
+                    "emoji": True,
+                },
+                "value": "v",
+                "description": {
+                    "type": "plain_text",
+                    "text": "this is an option",
+                    "emoji": True,
+                },
+            },
+        )
+        option = Option(
+            # Note that mrkdwn type is not allowed for this (as of April 2021)
+            text=PlainTextObject(text="label"),
+            value="v",
+            description="this is an option",
+        )
+        self.assertDictEqual(
+            option.to_dict(),
+            {
+                "text": {"type": "plain_text", "text": "label"},
+                "value": "v",
+                "description": {
+                    "type": "plain_text",
+                    "text": "this is an option",
+                    "emoji": True,
+                },
+            },
+        )
+
+    def test_valid_description_for_attachments(self):
+        option = Option(label="label", value="v", description="this is an option")
+        # legacy message actions in attachments
+        self.assertDictEqual(
+            option.to_dict("action"),
+            {
+                "text": "label",
+                "value": "v",
+                "description": "this is an option",
+            },
+        )
+        self.assertDictEqual(
+            option.to_dict("attachment"),
+            {
+                "text": "label",
+                "value": "v",
+                "description": "this is an option",
+            },
+        )
+
 
 class OptionGroupTests(unittest.TestCase):
     maxDiff = None
diff --git a/tests/slack_sdk/models/test_views.py b/tests/slack_sdk/models/test_views.py
index 631093c..11067b9 100644
--- a/tests/slack_sdk/models/test_views.py
+++ b/tests/slack_sdk/models/test_views.py
@@ -352,10 +352,7 @@ class ViewTests(unittest.TestCase):
         self.assertDictEqual(expected, state.to_dict())
 
     def test_view_state_value_empty_selected_options(self):
-        input = {
-            'type': 'checkboxes',
-            'selected_options': []
-        }
+        input = {"type": "checkboxes", "selected_options": []}
 
         view_state_value = ViewStateValue(**input)
         assert view_state_value.selected_options == []
@@ -365,11 +362,8 @@ class ViewTests(unittest.TestCase):
             "type": "checkboxes",
             "selected_options": [
                 {
-                    "text": {
-                        "type": "plain_text",
-                        "text": "test_option_text"
-                    },
-                    "value": "test_option_value"
+                    "text": {"type": "plain_text", "text": "test_option_text"},
+                    "value": "test_option_value",
                 }
             ],
         }
@@ -378,13 +372,10 @@ class ViewTests(unittest.TestCase):
             "type": "checkboxes",
             "selected_options": [
                 {
-                    "text": {
-                        "type": "plain_text",
-                        "text": "test_option_text"
-                    },
-                    "value": "test_option_value"
+                    "text": {"type": "plain_text", "text": "test_option_text"},
+                    "value": "test_option_value",
                 }
-            ]
+            ],
         }
 
         self.assertDictEqual(expected, ViewStateValue(**input).to_dict())