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

Issue:
Initialize View state field to ViewState object based on type
The View class (`slack_sdk.models.views.View`) does not initialize the state parameter value to `ViewState(state)` or `ViewState(**state)` based on the corresponding type (`ViewState` or `dict`, respectively)

### Reproducible in:

#### The Slack SDK version

```
slack-sdk==3.5.1
```

#### Python runtime version

```
Python 3.8.9
```

#### OS info

```
ProductName:    macOS
ProductVersion: 11.3.1
BuildVersion:   20E241
Darwin Kernel Version 20.4.0: Thu Apr 22 21:46:47 PDT 2021; root:xnu-7195.101.2~1/RELEASE_X86_64
```

#### Steps to reproduce:

(Share the commands to run, source code, and project settings (e.g., setup.py))

1. Instantiate a View object with state dict value
2. See below

### Expected result:

`View().state` object field is `ViewState` type and `ViewState.values` is initialized with `ViewStateValue` objects based on state initialization parameter value

### Actual result:

`View().state` field is `dict`, and `ViewState` and `ViewStateValue` objects are not initialized based on state value

### 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 description:
Fix #1021 by updating View constructor to convert state as dict to class object
## Summary

This pull request fixes #1021 by updating the `slack_sdk.models.views.View`'s constructor to convert a given `state` dict value to the `ViewState` class object. 

As all other properties in `View` class are already converted to class objects, changing this part would be a great improvement in terms of consistency. The changed property is still compatible with the current type hint - `Optional[Union[dict, ViewState]]`. We are going to apply this change in the next minor version - 3.6.

### 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/views/__init__.py b/slack_sdk/models/views/__init__.py
index f524fa5..0b32273 100644
--- a/slack_sdk/models/views/__init__.py
+++ b/slack_sdk/models/views/__init__.py
@@ -73,6 +73,8 @@ class View(JsonObject):
         self.blocks = Block.parse_all(blocks)
         self.private_metadata = private_metadata
         self.state = state
+        if self.state is not None and isinstance(self.state, dict):
+            self.state = ViewState(**self.state)
         self.hash = hash
         self.clear_on_close = clear_on_close
         self.notify_on_close = notify_on_close
@@ -155,29 +157,30 @@ class ViewState(JsonObject):
     ):
         value_objects: Dict[str, Dict[str, ViewStateValue]] = {}
         new_state_values = copy.copy(values)
-        for block_id, actions in new_state_values.items():
-            if actions is None:  # skipcq: PYL-R1724
-                continue
-            elif isinstance(actions, dict):
-                new_actions = copy.copy(actions)
-                for action_id, v in actions.items():
-                    if isinstance(v, dict):
-                        d = copy.copy(v)
-                        value_object = ViewStateValue(**d)
-                    elif isinstance(v, ViewStateValue):
-                        value_object = v
-                    else:
-                        self._show_warning_about_unknown(v)
-                        continue
-                    new_actions[action_id] = value_object
-                value_objects[block_id] = new_actions
-            else:
-                self._show_warning_about_unknown(v)
+        if isinstance(new_state_values, dict):  # just in case
+            for block_id, actions in new_state_values.items():
+                if actions is None:  # skipcq: PYL-R1724
+                    continue
+                elif isinstance(actions, dict):
+                    new_actions = copy.copy(actions)
+                    for action_id, v in actions.items():
+                        if isinstance(v, dict):
+                            d = copy.copy(v)
+                            value_object = ViewStateValue(**d)
+                        elif isinstance(v, ViewStateValue):
+                            value_object = v
+                        else:
+                            self._show_warning_about_unknown(v)
+                            continue
+                        new_actions[action_id] = value_object
+                    value_objects[block_id] = new_actions
+                else:
+                    self._show_warning_about_unknown(v)
         self.values = value_objects
 
     def to_dict(self, *args) -> Dict[str, Dict[str, Dict[str, dict]]]:  # type: ignore
         self.validate_json()
-        if self.values:  # skipcq: PYL-R1705
+        if self.values is not None:
             dict_values: Dict[str, Dict[str, dict]] = {}
             for block_id, actions in self.values.items():
                 if actions:


Test patch:
diff --git a/tests/data/view_modal_009.json b/tests/data/view_modal_009.json
index c8fe21d..7464086 100644
--- a/tests/data/view_modal_009.json
+++ b/tests/data/view_modal_009.json
@@ -35,7 +35,7 @@
   "callback_id": "view_4",
   "external_id": "some-unique-id",
   "state": {
-    "values": []
+    "values": {}
   },
   "hash": "1569362015.55b5e41b",
   "clear_on_close": true,
diff --git a/tests/data/view_modal_010.json b/tests/data/view_modal_010.json
index 7530e93..b017398 100644
--- a/tests/data/view_modal_010.json
+++ b/tests/data/view_modal_010.json
@@ -30,7 +30,7 @@
   "callback_id": "identify_your_modals",
   "external_id": "some-unique-id",
   "state": {
-    "values": []
+    "values": {}
   },
   "hash": "156772938.1827394",
   "clear_on_close": false,
diff --git a/tests/slack_sdk/models/test_elements.py b/tests/slack_sdk/models/test_elements.py
index 63573c9..3b7d600 100644
--- a/tests/slack_sdk/models/test_elements.py
+++ b/tests/slack_sdk/models/test_elements.py
@@ -228,7 +228,10 @@ class TimePickerElementTests(unittest.TestCase):
             "type": "timepicker",
             "action_id": "timepicker123",
             "initial_time": "11:40",
-            "placeholder": {"type": "plain_text", "text": "Select a time",},
+            "placeholder": {
+                "type": "plain_text",
+                "text": "Select a time",
+            },
         }
         self.assertDictEqual(input, TimePickerElement(**input).to_dict())
 
diff --git a/tests/slack_sdk/models/test_views.py b/tests/slack_sdk/models/test_views.py
index 11067b9..7980262 100644
--- a/tests/slack_sdk/models/test_views.py
+++ b/tests/slack_sdk/models/test_views.py
@@ -30,6 +30,7 @@ class ViewTests(unittest.TestCase):
     def verify_loaded_view_object(self, file):
         input = json.load(file)
         view = View(**input)
+        self.assertTrue(view.state is None or isinstance(view.state, ViewState))
         self.assertDictEqual(input, view.to_dict())
 
     # --------------------------------
@@ -110,6 +111,18 @@ class ViewTests(unittest.TestCase):
                     ),
                 ),
             ],
+            state=ViewState(
+                values={
+                    "b1": {
+                        "a1": ViewStateValue(type="plain_text_input", value="Title")
+                    },
+                    "b2": {
+                        "a2": ViewStateValue(
+                            type="plain_text_input", value="Description"
+                        )
+                    },
+                }
+            ),
         )
         modal_view.validate_json()
 
diff --git a/tests/slack_sdk_fixture/view_modal_009.json b/tests/slack_sdk_fixture/view_modal_009.json
index c8fe21d..7464086 100644
--- a/tests/slack_sdk_fixture/view_modal_009.json
+++ b/tests/slack_sdk_fixture/view_modal_009.json
@@ -35,7 +35,7 @@
   "callback_id": "view_4",
   "external_id": "some-unique-id",
   "state": {
-    "values": []
+    "values": {}
   },
   "hash": "1569362015.55b5e41b",
   "clear_on_close": true,
diff --git a/tests/slack_sdk_fixture/view_modal_010.json b/tests/slack_sdk_fixture/view_modal_010.json
index 7530e93..b017398 100644
--- a/tests/slack_sdk_fixture/view_modal_010.json
+++ b/tests/slack_sdk_fixture/view_modal_010.json
@@ -30,7 +30,7 @@
   "callback_id": "identify_your_modals",
   "external_id": "some-unique-id",
   "state": {
-    "values": []
+    "values": {}
   },
   "hash": "156772938.1827394",
   "clear_on_close": false,