Repository: aws-cloudformation/cloudformation-cli-python-plugin

Issue:
If a resource is created without Properties, request.desiredResourceState is None
This is not technically against the type annotations, but it would be nice if this wasn't the case.

Some snippets to test this with:

Schema:
```json
{
    "properties": {
        "Identifier": {"type": "string"},
    },   
    "required": [],
    "readOnlyProperties": ["/properties/Identifier"],
    "primaryIdentifier": ["/properties/Identifier"],
}
```

handlers.py inside of the create handler
```python
# this fails with `AttributeError: 'NoneType' object has no attribute 'Identifier'`
model = request.desiredResourceState
model.Identifier = identifier_utils.generate_resource_identifier(
    request.stackId, request.logicalResourceIdentifier,
    request.clientRequestToken, 255
)

# this works
model = request.desiredResourceState if request.desiredResourceState else ResourceModel(Identifier=None)
model.Identifier = identifier_utils.generate_resource_identifier(
    request.stackId, request.logicalResourceIdentifier,
    request.clientRequestToken, 255
)
```

I believe this can be solved by changing [this code in resource.py](https://github.com/aws-cloudformation/cloudformation-cli-python-plugin/blob/master/src/cloudformation_cli_python_lib/resource.py#L167)

from
```python
return UnmodelledRequest(
    # [...]
    desiredResourceState=request.requestData.resourceProperties,
     # [...]
 ).to_modelled(self._model_cls, self._type_configuration_model_cls)
```
to
```python
return UnmodelledRequest(
    # [...]
    desiredResourceState=request.requestData.resourceProperties if request.requestData.resourceProperties else {},
     # [...]
 ).to_modelled(self._model_cls, self._type_configuration_model_cls)
```

This probably also applies to previousResourceState and maybe even to the Tag-related properties

Comments:

Patch:
diff --git a/python/rpdk/python/codegen.py b/python/rpdk/python/codegen.py
index f5c49cf..60174aa 100644
--- a/python/rpdk/python/codegen.py
+++ b/python/rpdk/python/codegen.py
@@ -334,14 +334,24 @@ class Python36LanguagePlugin(LanguagePlugin):
 
         LOG.warning("Starting pip build.")
         try:
-            completed_proc = subprocess_run(  # nosec
-                command,
-                stdout=PIPE,
-                stderr=PIPE,
-                cwd=base_path,
-                check=True,
-                shell=True,
-            )
+            # On windows run pip command through the default shell (CMD)
+            if os.name == "nt":
+                completed_proc = subprocess_run(  # nosec
+                    command,
+                    stdout=PIPE,
+                    stderr=PIPE,
+                    cwd=base_path,
+                    check=True,
+                    shell=True,
+                )
+            else:
+                completed_proc = subprocess_run(  # nosec
+                    command,
+                    stdout=PIPE,
+                    stderr=PIPE,
+                    cwd=base_path,
+                    check=True,
+                )
             LOG.warning("pip build finished.")
         except (FileNotFoundError, CalledProcessError) as e:
             raise DownstreamError("pip build failed") from e
diff --git a/src/cloudformation_cli_python_lib/resource.py b/src/cloudformation_cli_python_lib/resource.py
index bf7ba30..bfc9384 100644
--- a/src/cloudformation_cli_python_lib/resource.py
+++ b/src/cloudformation_cli_python_lib/resource.py
@@ -164,9 +164,13 @@ class Resource:
         try:
             return UnmodelledRequest(
                 clientRequestToken=request.bearerToken,
-                desiredResourceState=request.requestData.resourceProperties,
+                desiredResourceState=request.requestData.resourceProperties
+                if request.requestData.resourceProperties
+                else {},
                 previousResourceState=request.requestData.previousResourceProperties,
-                desiredResourceTags=request.requestData.stackTags,
+                desiredResourceTags=request.requestData.stackTags
+                if request.requestData.stackTags
+                else {},
                 previousResourceTags=request.requestData.previousStackTags,
                 systemTags=request.requestData.systemTags,
                 previousSystemTags=request.requestData.previousSystemTags,


Test patch:
diff --git a/tests/plugin/codegen_test.py b/tests/plugin/codegen_test.py
index a36d04b..aa64261 100644
--- a/tests/plugin/codegen_test.py
+++ b/tests/plugin/codegen_test.py
@@ -416,6 +416,43 @@ def test__build_pip(plugin):
     mock_pip.assert_called_once_with(sentinel.base_path)
 
 
+def test__build_pip_posix(plugin):
+    patch_os_name = patch("rpdk.python.codegen.os.name", "posix")
+    patch_subproc = patch("rpdk.python.codegen.subprocess_run")
+
+    # Path must be set outside simulated os.name
+    temppath = Path(str(sentinel.base_path))
+    with patch_os_name, patch_subproc as mock_subproc:
+        plugin._pip_build(temppath)
+
+    mock_subproc.assert_called_once_with(
+        plugin._make_pip_command(temppath),
+        stdout=ANY,
+        stderr=ANY,
+        cwd=temppath,
+        check=ANY,
+    )
+
+
+def test__build_pip_windows(plugin):
+    patch_os_name = patch("rpdk.python.codegen.os.name", "nt")
+    patch_subproc = patch("rpdk.python.codegen.subprocess_run")
+
+    # Path must be set outside simulated os.name
+    temppath = Path(str(sentinel.base_path))
+    with patch_os_name, patch_subproc as mock_subproc:
+        plugin._pip_build(temppath)
+
+    mock_subproc.assert_called_once_with(
+        plugin._make_pip_command(temppath),
+        stdout=ANY,
+        stderr=ANY,
+        cwd=temppath,
+        check=ANY,
+        shell=True,
+    )
+
+
 def test__build_docker(plugin):
     plugin._use_docker = True