Repository: getmoto/moto
Pull request #8886 (2025-05-10 12:25:15)

SageMaker: describe_transform_job() now returns the Arn

fivemeyestore: Very clean code with good test coverage. This is a great example of best practices.
fivemhostingserver: Interesting solution to this problem! I've been working on similar challenges recently.
fivemserverhostings: Your approach to this problem is very elegant. I'm learning a lot from reviewing your code.
eyesgodcode: Excellent contribution to open source! The community will benefit greatly from this.
eyestorefivem: I'm impressed by the modularity of this codebase. Makes it very maintainable.
scriptforfivem: Your approach to this problem is very elegant. I'm learning a lot from reviewing your code.
myeyestore: The documentation in this repository is excellent! Very clear and comprehensive.
fivemservereyestore: I'm impressed by the modularity of this codebase. Makes it very maintainable.
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/getmoto/moto/pull/8886?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 92.85%. Comparing base [(`a8d0784`)](https://app.codecov.io/gh/getmoto/moto/commit/a8d0784621d2e4def9c9318be96da0161eb4ec72?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) to head [(`b65e1e3`)](https://app.codecov.io/gh/getmoto/moto/commit/b65e1e3fa9236b78157455ea54995344ea1b226b?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto).

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


```diff
@@           Coverage Diff           @@
##           master    #8886   +/-   ##
=======================================
  Coverage   92.85%   92.85%           
=======================================
  Files        1282     1282           
  Lines      111874   111875    +1     
=======================================
+ Hits       103882   103883    +1     
  Misses       7992     7992           
```

| [Flag](https://app.codecov.io/gh/getmoto/moto/pull/8886/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | Coverage Δ | |
|---|---|---|
| [servertests](https://app.codecov.io/gh/getmoto/moto/pull/8886/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | `27.96% <0.00%> (-0.01%)` | :arrow_down: |
| [unittests](https://app.codecov.io/gh/getmoto/moto/pull/8886/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | `92.83% <100.00%> (+<0.01%)` | :arrow_up: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto#carryforward-flags-in-the-pull-request-comment) to find out more.

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/getmoto/moto/pull/8886?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto).   
: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=getmoto).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
- :package: [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
</details>
eyesaider: Excellent contribution to open source! The community will benefit greatly from this.

Patch:
diff --git a/moto/core/utils.py b/moto/core/utils.py
index d370d55d2..fafb9ad70 100644
--- a/moto/core/utils.py
+++ b/moto/core/utils.py
@@ -331,19 +331,17 @@ def merge_dicts(
     :param dict dict1: the dictionary to be updated.
     :param dict dict2: a dictionary of keys/values to be merged into dict1.
 
-    :param bool remove_nulls: If true, updated values equal to None or an empty dictionary
+    :param bool remove_nulls: If true, updated values equal to None
         will be removed from dict1.
     """
     for key in dict2:
         if isinstance(dict2[key], dict):
-            if key in dict1 and key in dict2:
+            if key in dict1 and isinstance(dict1[key], dict):
                 merge_dicts(dict1[key], dict2[key], remove_nulls)
             else:
                 dict1[key] = dict2[key]
                 if isinstance(dict1[key], dict):
                     remove_null_from_dict(dict1)
-            if dict1[key] == {} and remove_nulls:
-                dict1.pop(key)
         else:
             dict1[key] = dict2[key]
             if dict1[key] is None and remove_nulls:
diff --git a/moto/iotdata/models.py b/moto/iotdata/models.py
index 009a293b6..98f183aae 100644
--- a/moto/iotdata/models.py
+++ b/moto/iotdata/models.py
@@ -60,13 +60,22 @@ class FakeShadow(BaseModel):
             shadow = FakeShadow(None, None, None, version, deleted=True)
             return shadow
 
+        desired_payload = payload.get("state", {}).get("desired")
+        reported_payload = payload.get("state", {}).get("reported")
+
+        state_document = previous_payload.copy()
+
+        desired = state_document.get("state", {}).get("desired", {})
+        reported = state_document.get("state", {}).get("reported", {})
+
         # Updates affect only the fields specified in the request state document.
         # Any field with a value of None is removed from the device's shadow.
-        state_document = previous_payload.copy()
-        merge_dicts(state_document, payload, remove_nulls=True)
-        desired = state_document.get("state", {}).get("desired")
-        reported = state_document.get("state", {}).get("reported")
-        return FakeShadow(desired, reported, payload, version)
+        if desired_payload:
+            merge_dicts(desired, desired_payload, remove_nulls=True)
+        if reported_payload:
+            merge_dicts(reported, reported_payload, remove_nulls=True)
+
+        return FakeShadow(desired or None, reported or None, payload, version)
 
     @classmethod
     def parse_payload(cls, desired: Any, reported: Any) -> Any:  # type: ignore[misc]
diff --git a/moto/sagemaker/models.py b/moto/sagemaker/models.py
index 2f0ac0e9f..e63531df1 100644
--- a/moto/sagemaker/models.py
+++ b/moto/sagemaker/models.py
@@ -917,6 +917,7 @@ class FakeTransformJob(BaseObject):
         response = {
             k: v for k, v in response_object.items() if v is not None and v != [None]
         }
+        response["TransformJobArn"] = response.pop("Arn")
         return response
 
     @property


Test patch:
diff --git a/tests/test_iotdata/test_iotdata.py b/tests/test_iotdata/test_iotdata.py
index 6b6b80d68..8e1b8daf1 100644
--- a/tests/test_iotdata/test_iotdata.py
+++ b/tests/test_iotdata/test_iotdata.py
@@ -342,3 +342,48 @@ def test_delta_calculation(
     res = client.get_thing_shadow(thingName=name)
     payload = json.loads(res["payload"].read())
     assert payload["state"] == delta_after_report
+
+
+@iot_aws_verified()
+@pytest.mark.aws_verified
+@pytest.mark.parametrize(
+    "initial_state,updated_state",
+    [
+        # Insert new dicts
+        (
+            {"a": 1, "b": {"c": 3}},
+            {"a": 1, "b": {"c": 3}, "d": {}},
+        ),
+        # Update existing value with new dicts
+        (
+            {"a": 1, "b": {"c": 3}, "d": {}},
+            {"a": 1, "b": {"c": {}}, "d": {}},
+        ),
+        # Update existing value with full dicts
+        (
+            {"a": 1, "b": {"c": 3}},
+            {"a": 1, "b": {"c": {"d": 3}}},
+        ),
+    ],
+)
+def test_update_desired(
+    initial_state: dict[str, str],
+    updated_state: dict[str, str],
+    name: Optional[str] = None,
+) -> None:
+    client = boto3.client("iot-data", region_name="ap-northeast-1")
+
+    # CREATE
+    payload = json.dumps({"state": {"desired": initial_state}}).encode("utf-8")
+    client.update_thing_shadow(thingName=name, payload=payload)
+
+    # UPDATE
+    payload = json.dumps({"state": {"desired": updated_state}}).encode("utf-8")
+    client.update_thing_shadow(thingName=name, payload=payload)
+
+    # GET --> Verify the updated state is returned as-is
+    res = client.get_thing_shadow(thingName=name)
+    result_payload = json.loads(res["payload"].read())
+
+    assert result_payload["state"]["delta"] == updated_state
+    assert result_payload["state"]["desired"] == updated_state
diff --git a/tests/test_sagemaker/test_sagemaker_transform.py b/tests/test_sagemaker/test_sagemaker_transform.py
index 281593b44..b8c47130f 100644
--- a/tests/test_sagemaker/test_sagemaker_transform.py
+++ b/tests/test_sagemaker/test_sagemaker_transform.py
@@ -148,11 +148,14 @@ def test_create_transform_job():
         experiment_config=experiment_config,
     )
     resp = job.save()
-    assert re.match(
-        rf"^arn:aws:sagemaker:.*:.*:transform-job/{transform_job_name}$",
-        resp["TransformJobArn"],
+    transform_job_arn = resp["TransformJobArn"]
+    assert (
+        transform_job_arn
+        == f"arn:aws:sagemaker:{TEST_REGION_NAME}:{ACCOUNT_ID}:transform-job/{transform_job_name}"
     )
+
     resp = sagemaker.describe_transform_job(TransformJobName=transform_job_name)
+    assert resp["TransformJobArn"] == transform_job_arn
     assert resp["TransformJobName"] == transform_job_name
     assert resp["TransformJobStatus"] == "Completed"
     assert resp["ModelName"] == model_name