@typespec/http-client-python 0.23.0 → 0.23.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -403,7 +403,12 @@ class RequestBuilderSerializer(_BuilderBaseSerializer[RequestBuilderType]):
403
403
  builder: RequestBuilderType,
404
404
  ) -> list[str]:
405
405
  def _get_value(param):
406
- declaration = param.get_declaration() if param.constant else None
406
+ if param.constant:
407
+ declaration = param.get_declaration()
408
+ elif param.client_default_value_declaration is not None:
409
+ declaration = param.client_default_value_declaration
410
+ else:
411
+ declaration = None
407
412
  if param.location in [ParameterLocation.HEADER, ParameterLocation.QUERY]:
408
413
  kwarg_dict = "headers" if param.location == ParameterLocation.HEADER else "params"
409
414
  return f"_{kwarg_dict}.pop('{param.wire_name}', {declaration})"
@@ -382,9 +382,39 @@ class _MyMutableMapping(MutableMapping[str, typing.Any]):
382
382
  return key in self._data
383
383
 
384
384
  def __getitem__(self, key: str) -> typing.Any:
385
+ # If this key has been deserialized (for mutable types), we need to handle serialization
386
+ if hasattr(self, "_attr_to_rest_field"):
387
+ cache_attr = f"_deserialized_{key}"
388
+ if hasattr(self, cache_attr):
389
+ rf = _get_rest_field(getattr(self, "_attr_to_rest_field"), key)
390
+ if rf:
391
+ value = self._data.get(key)
392
+ if isinstance(value, (dict, list, set)):
393
+ # For mutable types, serialize and return
394
+ # But also update _data with serialized form and clear flag
395
+ # so mutations via this returned value affect _data
396
+ serialized = _serialize(value, rf._format)
397
+ # If serialized form is same type (no transformation needed),
398
+ # return _data directly so mutations work
399
+ if isinstance(serialized, type(value)) and serialized == value:
400
+ return self._data.get(key)
401
+ # Otherwise return serialized copy and clear flag
402
+ try:
403
+ object.__delattr__(self, cache_attr)
404
+ except AttributeError:
405
+ pass
406
+ # Store serialized form back
407
+ self._data[key] = serialized
408
+ return serialized
385
409
  return self._data.__getitem__(key)
386
410
 
387
411
  def __setitem__(self, key: str, value: typing.Any) -> None:
412
+ # Clear any cached deserialized value when setting through dictionary access
413
+ cache_attr = f"_deserialized_{key}"
414
+ try:
415
+ object.__delattr__(self, cache_attr)
416
+ except AttributeError:
417
+ pass
388
418
  self._data.__setitem__(key, value)
389
419
 
390
420
  def __delitem__(self, key: str) -> None:
@@ -1093,14 +1123,37 @@ class _RestField:
1093
1123
  def __get__(self, obj: Model, type=None): # pylint: disable=redefined-builtin
1094
1124
  # by this point, type and rest_name will have a value bc we default
1095
1125
  # them in __new__ of the Model class
1096
- item = obj.get(self._rest_name)
1126
+ # Use _data.get() directly to avoid triggering __getitem__ which clears the cache
1127
+ item = obj._data.get(self._rest_name)
1097
1128
  if item is None:
1098
1129
  return item
1099
1130
  if self._is_model:
1100
1131
  return item
1101
- return _deserialize(self._type, _serialize(item, self._format), rf=self)
1132
+
1133
+ # For mutable types, we want mutations to directly affect _data
1134
+ # Check if we've already deserialized this value
1135
+ cache_attr = f"_deserialized_{self._rest_name}"
1136
+ if hasattr(obj, cache_attr):
1137
+ # Return the value from _data directly (it's been deserialized in place)
1138
+ return obj._data.get(self._rest_name)
1139
+
1140
+ deserialized = _deserialize(self._type, _serialize(item, self._format), rf=self)
1141
+
1142
+ # For mutable types, store the deserialized value back in _data
1143
+ # so mutations directly affect _data
1144
+ if isinstance(deserialized, (dict, list, set)):
1145
+ obj._data[self._rest_name] = deserialized
1146
+ object.__setattr__(obj, cache_attr, True) # Mark as deserialized
1147
+ return deserialized
1148
+
1149
+ return deserialized
1102
1150
 
1103
1151
  def __set__(self, obj: Model, value) -> None:
1152
+ # Clear the cached deserialized object when setting a new value
1153
+ cache_attr = f"_deserialized_{self._rest_name}"
1154
+ if hasattr(obj, cache_attr):
1155
+ object.__delattr__(obj, cache_attr)
1156
+
1104
1157
  if value is None:
1105
1158
  # we want to wipe out entries if users set attr to None
1106
1159
  try:
@@ -817,13 +817,20 @@ class Serializer: # pylint: disable=too-many-public-methods
817
817
  :param str data_type: Type of object in the iterable.
818
818
  :rtype: str, int, float, bool
819
819
  :return: serialized object
820
+ :raises TypeError: raise if data_type is not one of str, int, float, bool.
820
821
  """
821
822
  custom_serializer = cls._get_custom_serializers(data_type, **kwargs)
822
823
  if custom_serializer:
823
824
  return custom_serializer(data)
824
825
  if data_type == "str":
825
826
  return cls.serialize_unicode(data)
826
- return eval(data_type)(data) # nosec # pylint: disable=eval-used
827
+ if data_type == "int":
828
+ return int(data)
829
+ if data_type == "float":
830
+ return float(data)
831
+ if data_type == "bool":
832
+ return bool(data)
833
+ raise TypeError("Unknown basic data type: {}".format(data_type))
827
834
 
828
835
  @classmethod
829
836
  def serialize_unicode(cls, data):
@@ -1753,7 +1760,7 @@ class Deserializer:
1753
1760
  :param str data_type: deserialization data type.
1754
1761
  :return: Deserialized basic type.
1755
1762
  :rtype: str, int, float or bool
1756
- :raises TypeError: if string format is not valid.
1763
+ :raises TypeError: if string format is not valid or data_type is not one of str, int, float, bool.
1757
1764
  """
1758
1765
  # If we're here, data is supposed to be a basic type.
1759
1766
  # If it's still an XML node, take the text
@@ -1779,7 +1786,11 @@ class Deserializer:
1779
1786
 
1780
1787
  if data_type == "str":
1781
1788
  return self.deserialize_unicode(attr)
1782
- return eval(data_type)(attr) # nosec # pylint: disable=eval-used
1789
+ if data_type == "int":
1790
+ return int(attr)
1791
+ if data_type == "float":
1792
+ return float(attr)
1793
+ raise TypeError("Unknown basic data type: {}".format(data_type))
1783
1794
 
1784
1795
  @staticmethod
1785
1796
  def deserialize_unicode(data):
@@ -403,7 +403,12 @@ class RequestBuilderSerializer(_BuilderBaseSerializer[RequestBuilderType]):
403
403
  builder: RequestBuilderType,
404
404
  ) -> list[str]:
405
405
  def _get_value(param):
406
- declaration = param.get_declaration() if param.constant else None
406
+ if param.constant:
407
+ declaration = param.get_declaration()
408
+ elif param.client_default_value_declaration is not None:
409
+ declaration = param.client_default_value_declaration
410
+ else:
411
+ declaration = None
407
412
  if param.location in [ParameterLocation.HEADER, ParameterLocation.QUERY]:
408
413
  kwarg_dict = "headers" if param.location == ParameterLocation.HEADER else "params"
409
414
  return f"_{kwarg_dict}.pop('{param.wire_name}', {declaration})"
@@ -382,9 +382,39 @@ class _MyMutableMapping(MutableMapping[str, typing.Any]):
382
382
  return key in self._data
383
383
 
384
384
  def __getitem__(self, key: str) -> typing.Any:
385
+ # If this key has been deserialized (for mutable types), we need to handle serialization
386
+ if hasattr(self, "_attr_to_rest_field"):
387
+ cache_attr = f"_deserialized_{key}"
388
+ if hasattr(self, cache_attr):
389
+ rf = _get_rest_field(getattr(self, "_attr_to_rest_field"), key)
390
+ if rf:
391
+ value = self._data.get(key)
392
+ if isinstance(value, (dict, list, set)):
393
+ # For mutable types, serialize and return
394
+ # But also update _data with serialized form and clear flag
395
+ # so mutations via this returned value affect _data
396
+ serialized = _serialize(value, rf._format)
397
+ # If serialized form is same type (no transformation needed),
398
+ # return _data directly so mutations work
399
+ if isinstance(serialized, type(value)) and serialized == value:
400
+ return self._data.get(key)
401
+ # Otherwise return serialized copy and clear flag
402
+ try:
403
+ object.__delattr__(self, cache_attr)
404
+ except AttributeError:
405
+ pass
406
+ # Store serialized form back
407
+ self._data[key] = serialized
408
+ return serialized
385
409
  return self._data.__getitem__(key)
386
410
 
387
411
  def __setitem__(self, key: str, value: typing.Any) -> None:
412
+ # Clear any cached deserialized value when setting through dictionary access
413
+ cache_attr = f"_deserialized_{key}"
414
+ try:
415
+ object.__delattr__(self, cache_attr)
416
+ except AttributeError:
417
+ pass
388
418
  self._data.__setitem__(key, value)
389
419
 
390
420
  def __delitem__(self, key: str) -> None:
@@ -1093,14 +1123,37 @@ class _RestField:
1093
1123
  def __get__(self, obj: Model, type=None): # pylint: disable=redefined-builtin
1094
1124
  # by this point, type and rest_name will have a value bc we default
1095
1125
  # them in __new__ of the Model class
1096
- item = obj.get(self._rest_name)
1126
+ # Use _data.get() directly to avoid triggering __getitem__ which clears the cache
1127
+ item = obj._data.get(self._rest_name)
1097
1128
  if item is None:
1098
1129
  return item
1099
1130
  if self._is_model:
1100
1131
  return item
1101
- return _deserialize(self._type, _serialize(item, self._format), rf=self)
1132
+
1133
+ # For mutable types, we want mutations to directly affect _data
1134
+ # Check if we've already deserialized this value
1135
+ cache_attr = f"_deserialized_{self._rest_name}"
1136
+ if hasattr(obj, cache_attr):
1137
+ # Return the value from _data directly (it's been deserialized in place)
1138
+ return obj._data.get(self._rest_name)
1139
+
1140
+ deserialized = _deserialize(self._type, _serialize(item, self._format), rf=self)
1141
+
1142
+ # For mutable types, store the deserialized value back in _data
1143
+ # so mutations directly affect _data
1144
+ if isinstance(deserialized, (dict, list, set)):
1145
+ obj._data[self._rest_name] = deserialized
1146
+ object.__setattr__(obj, cache_attr, True) # Mark as deserialized
1147
+ return deserialized
1148
+
1149
+ return deserialized
1102
1150
 
1103
1151
  def __set__(self, obj: Model, value) -> None:
1152
+ # Clear the cached deserialized object when setting a new value
1153
+ cache_attr = f"_deserialized_{self._rest_name}"
1154
+ if hasattr(obj, cache_attr):
1155
+ object.__delattr__(obj, cache_attr)
1156
+
1104
1157
  if value is None:
1105
1158
  # we want to wipe out entries if users set attr to None
1106
1159
  try:
@@ -817,13 +817,20 @@ class Serializer: # pylint: disable=too-many-public-methods
817
817
  :param str data_type: Type of object in the iterable.
818
818
  :rtype: str, int, float, bool
819
819
  :return: serialized object
820
+ :raises TypeError: raise if data_type is not one of str, int, float, bool.
820
821
  """
821
822
  custom_serializer = cls._get_custom_serializers(data_type, **kwargs)
822
823
  if custom_serializer:
823
824
  return custom_serializer(data)
824
825
  if data_type == "str":
825
826
  return cls.serialize_unicode(data)
826
- return eval(data_type)(data) # nosec # pylint: disable=eval-used
827
+ if data_type == "int":
828
+ return int(data)
829
+ if data_type == "float":
830
+ return float(data)
831
+ if data_type == "bool":
832
+ return bool(data)
833
+ raise TypeError("Unknown basic data type: {}".format(data_type))
827
834
 
828
835
  @classmethod
829
836
  def serialize_unicode(cls, data):
@@ -1753,7 +1760,7 @@ class Deserializer:
1753
1760
  :param str data_type: deserialization data type.
1754
1761
  :return: Deserialized basic type.
1755
1762
  :rtype: str, int, float or bool
1756
- :raises TypeError: if string format is not valid.
1763
+ :raises TypeError: if string format is not valid or data_type is not one of str, int, float, bool.
1757
1764
  """
1758
1765
  # If we're here, data is supposed to be a basic type.
1759
1766
  # If it's still an XML node, take the text
@@ -1779,7 +1786,11 @@ class Deserializer:
1779
1786
 
1780
1787
  if data_type == "str":
1781
1788
  return self.deserialize_unicode(attr)
1782
- return eval(data_type)(attr) # nosec # pylint: disable=eval-used
1789
+ if data_type == "int":
1790
+ return int(attr)
1791
+ if data_type == "float":
1792
+ return float(attr)
1793
+ raise TypeError("Unknown basic data type: {}".format(data_type))
1783
1794
 
1784
1795
  @staticmethod
1785
1796
  def deserialize_unicode(data):
@@ -2,6 +2,7 @@
2
2
  # Copyright (c) Microsoft Corporation.
3
3
  # Licensed under the MIT License.
4
4
  # ------------------------------------
5
+ from ast import Mod
5
6
  import copy
6
7
  import decimal
7
8
  import json
@@ -928,9 +929,7 @@ def test_deserialization_callback_override():
928
929
 
929
930
  model_with_callback = MyModel2(prop=[1.3, 2.4, 3.5])
930
931
  assert model_with_callback.prop == ["1.3", "2.4", "3.5"]
931
- # since the deserialize function is not roundtrip-able, once we deserialize
932
- # the serialized version is the same
933
- assert model_with_callback["prop"] == [1.3, 2.4, 3.5]
932
+ assert model_with_callback["prop"] == ["1.3", "2.4", "3.5"]
934
933
 
935
934
 
936
935
  def test_deserialization_callback_override_parent():
@@ -966,7 +965,7 @@ def test_deserialization_callback_override_parent():
966
965
 
967
966
  child_model = ChildWithCallback(prop=[1, 1, 2, 3])
968
967
  assert child_model.prop == set(["1", "1", "2", "3"])
969
- assert child_model["prop"] == [1, 1, 2, 3]
968
+ assert child_model["prop"] == set(["1", "1", "2", "3"])
970
969
 
971
970
 
972
971
  def test_inheritance_basic():
@@ -3267,7 +3266,7 @@ def test_complex_array_wrapper(model: ArrayWrapper):
3267
3266
 
3268
3267
  model["array"] = [1, 2, 3, 4, 5]
3269
3268
  assert model.array == ["1", "2", "3", "4", "5"]
3270
- assert model["array"] == [1, 2, 3, 4, 5]
3269
+ assert model["array"] == ["1", "2", "3", "4", "5"]
3271
3270
 
3272
3271
 
3273
3272
  @pytest.mark.parametrize("model", [ArrayWrapper(array=[]), ArrayWrapper({"array": []})])
@@ -4372,3 +4371,256 @@ def test_array_encode_with_special_characters():
4372
4371
 
4373
4372
  assert model.pipe_values == ["path/to/file", "another-path", "final.path"]
4374
4373
  assert model["pipeValues"] == "path/to/file|another-path|final.path"
4374
+
4375
+
4376
+ def test_dictionary_set():
4377
+ """Test that dictionary mutations via attribute syntax persist and sync to dictionary syntax."""
4378
+
4379
+ class MyModel(Model):
4380
+ my_dict: dict[str, int] = rest_field(visibility=["read", "create", "update", "delete", "query"])
4381
+
4382
+ # Test 1: Basic mutation via attribute syntax
4383
+ m = MyModel(my_dict={"a": 1, "b": 2})
4384
+ assert m.my_dict == {"a": 1, "b": 2}
4385
+
4386
+ # Test 2: Add new key via attribute syntax and verify it persists
4387
+ m.my_dict["c"] = 3
4388
+ assert m.my_dict["c"] == 3
4389
+ assert m.my_dict == {"a": 1, "b": 2, "c": 3}
4390
+
4391
+ # Test 3: Verify mutation is reflected in dictionary syntax
4392
+ assert m["my_dict"] == {"a": 1, "b": 2, "c": 3}
4393
+
4394
+ # Test 4: Modify existing key via attribute syntax
4395
+ m.my_dict["a"] = 100
4396
+ assert m.my_dict["a"] == 100
4397
+ assert m["my_dict"]["a"] == 100
4398
+
4399
+ # Test 5: Delete key via attribute syntax
4400
+ del m.my_dict["b"]
4401
+ assert "b" not in m.my_dict
4402
+ assert "b" not in m["my_dict"]
4403
+
4404
+ # Test 6: Update via dict methods
4405
+ m.my_dict.update({"d": 4, "e": 5})
4406
+ assert m.my_dict["d"] == 4
4407
+ assert m.my_dict["e"] == 5
4408
+ assert m["my_dict"]["d"] == 4
4409
+
4410
+ # Test 7: Clear via attribute syntax and verify via dictionary syntax
4411
+ m.my_dict.clear()
4412
+ assert len(m.my_dict) == 0
4413
+ assert len(m["my_dict"]) == 0
4414
+
4415
+ # Test 8: Reassign entire dictionary via attribute syntax
4416
+ m.my_dict = {"x": 10, "y": 20}
4417
+ assert m.my_dict == {"x": 10, "y": 20}
4418
+ assert m["my_dict"] == {"x": 10, "y": 20}
4419
+
4420
+ # Test 9: Mutation after reassignment
4421
+ m.my_dict["z"] = 30
4422
+ assert m.my_dict["z"] == 30
4423
+ assert m["my_dict"]["z"] == 30
4424
+
4425
+ # Test 10: Access via dictionary syntax first, then mutate via attribute syntax
4426
+ m.my_dict["w"] = 40
4427
+ assert m["my_dict"]["w"] == 40
4428
+
4429
+ # Test 11: Multiple accesses maintain same cached object
4430
+ dict_ref1 = m.my_dict
4431
+ dict_ref2 = m.my_dict
4432
+ assert dict_ref1 is dict_ref2
4433
+ dict_ref1["new_key"] = 999
4434
+ assert dict_ref2["new_key"] == 999
4435
+ assert m.my_dict["new_key"] == 999
4436
+
4437
+
4438
+ def test_list_set():
4439
+ """Test that list mutations via attribute syntax persist and sync to dictionary syntax."""
4440
+
4441
+ class MyModel(Model):
4442
+ my_list: list[int] = rest_field(visibility=["read", "create", "update", "delete", "query"])
4443
+
4444
+ # Test 1: Basic mutation via attribute syntax
4445
+ m = MyModel(my_list=[1, 2, 3])
4446
+ assert m.my_list == [1, 2, 3]
4447
+
4448
+ # Test 2: Append via attribute syntax and verify it persists
4449
+ m.my_list.append(4)
4450
+ assert m.my_list == [1, 2, 3, 4]
4451
+
4452
+ # Test 3: Verify mutation is reflected in dictionary syntax
4453
+ assert m["my_list"] == [1, 2, 3, 4]
4454
+
4455
+ # Test 4: Modify existing element via attribute syntax
4456
+ m.my_list[0] = 100
4457
+ assert m.my_list[0] == 100
4458
+ assert m["my_list"][0] == 100
4459
+
4460
+ # Test 5: Extend list via attribute syntax
4461
+ m.my_list.extend([5, 6])
4462
+ assert m.my_list == [100, 2, 3, 4, 5, 6]
4463
+ assert m["my_list"] == [100, 2, 3, 4, 5, 6]
4464
+
4465
+ # Test 6: Remove element via attribute syntax
4466
+ m.my_list.remove(2)
4467
+ assert 2 not in m.my_list
4468
+ assert 2 not in m["my_list"]
4469
+
4470
+ # Test 7: Pop element
4471
+ popped = m.my_list.pop()
4472
+ assert popped == 6
4473
+ assert 6 not in m.my_list
4474
+ assert 6 not in m["my_list"]
4475
+
4476
+ # Test 8: Insert element
4477
+ m.my_list.insert(0, 999)
4478
+ assert m.my_list[0] == 999
4479
+ assert m["my_list"][0] == 999
4480
+
4481
+ # Test 9: Clear via attribute syntax
4482
+ m.my_list.clear()
4483
+ assert len(m.my_list) == 0
4484
+ assert len(m["my_list"]) == 0
4485
+
4486
+ # Test 10: Reassign entire list via attribute syntax
4487
+ m.my_list = [10, 20, 30]
4488
+ assert m.my_list == [10, 20, 30]
4489
+ assert m["my_list"] == [10, 20, 30]
4490
+
4491
+ # Test 11: Mutation after reassignment
4492
+ m.my_list.append(40)
4493
+ assert m.my_list == [10, 20, 30, 40]
4494
+ assert m["my_list"] == [10, 20, 30, 40]
4495
+
4496
+ # Test 12: Multiple accesses maintain same cached object
4497
+ list_ref1 = m.my_list
4498
+ list_ref2 = m.my_list
4499
+ assert list_ref1 is list_ref2
4500
+ list_ref1.append(50)
4501
+ assert 50 in list_ref2
4502
+ assert 50 in m.my_list
4503
+
4504
+
4505
+ def test_set_collection():
4506
+ """Test that set mutations via attribute syntax persist and sync to dictionary syntax."""
4507
+
4508
+ class MyModel(Model):
4509
+ my_set: set[int] = rest_field(visibility=["read", "create", "update", "delete", "query"])
4510
+
4511
+ # Test 1: Basic mutation via attribute syntax
4512
+ m = MyModel(my_set={1, 2, 3})
4513
+ assert m.my_set == {1, 2, 3}
4514
+
4515
+ # Test 2: Add via attribute syntax and verify it persists
4516
+ m.my_set.add(4)
4517
+ assert 4 in m.my_set
4518
+
4519
+ # Test 3: Verify mutation is reflected in dictionary syntax
4520
+ assert 4 in m["my_set"]
4521
+
4522
+ # Test 4: Remove element via attribute syntax
4523
+ m.my_set.remove(2)
4524
+ assert 2 not in m.my_set
4525
+ assert 2 not in m["my_set"]
4526
+
4527
+ # Test 5: Update set via attribute syntax
4528
+ m.my_set.update({5, 6, 7})
4529
+ assert m.my_set == {1, 3, 4, 5, 6, 7}
4530
+ assert m["my_set"] == {1, 3, 4, 5, 6, 7}
4531
+
4532
+ # Test 6: Discard element
4533
+ m.my_set.discard(1)
4534
+ assert 1 not in m.my_set
4535
+ assert 1 not in m["my_set"]
4536
+
4537
+ # Test 7: Clear via attribute syntax
4538
+ m.my_set.clear()
4539
+ assert len(m.my_set) == 0
4540
+ assert len(m["my_set"]) == 0
4541
+
4542
+ # Test 8: Reassign entire set via attribute syntax
4543
+ m.my_set = {10, 20, 30}
4544
+ assert m.my_set == {10, 20, 30}
4545
+ assert m["my_set"] == {10, 20, 30}
4546
+
4547
+ # Test 9: Mutation after reassignment
4548
+ m.my_set.add(40)
4549
+ assert 40 in m.my_set
4550
+ assert 40 in m["my_set"]
4551
+
4552
+ # Test 10: Multiple accesses maintain same cached object
4553
+ set_ref1 = m.my_set
4554
+ set_ref2 = m.my_set
4555
+ assert set_ref1 is set_ref2
4556
+ set_ref1.add(50)
4557
+ assert 50 in set_ref2
4558
+ assert 50 in m.my_set
4559
+
4560
+
4561
+ def test_dictionary_set_datetime():
4562
+ """Test that dictionary with datetime values properly serializes/deserializes."""
4563
+ from datetime import datetime, timezone
4564
+
4565
+ class MyModel(Model):
4566
+ my_dict: dict[str, datetime] = rest_field(visibility=["read", "create", "update", "delete", "query"])
4567
+
4568
+ # Test 1: Initialize with datetime values
4569
+ dt1 = datetime(2023, 1, 15, 10, 30, 45, tzinfo=timezone.utc)
4570
+ dt2 = datetime(2023, 6, 20, 14, 15, 30, tzinfo=timezone.utc)
4571
+ m = MyModel(my_dict={"created": dt1, "updated": dt2})
4572
+
4573
+ # Test 2: Access via attribute syntax returns datetime objects
4574
+ assert isinstance(m.my_dict["created"], datetime)
4575
+ assert isinstance(m.my_dict["updated"], datetime)
4576
+ assert m.my_dict["created"] == dt1
4577
+ assert m.my_dict["updated"] == dt2
4578
+
4579
+ # Test 3: Access via dictionary syntax returns serialized strings (ISO format)
4580
+ dict_access = m["my_dict"]
4581
+ assert isinstance(dict_access["created"], str)
4582
+ assert isinstance(dict_access["updated"], str)
4583
+ assert dict_access["created"] == "2023-01-15T10:30:45Z"
4584
+ assert dict_access["updated"] == "2023-06-20T14:15:30Z"
4585
+
4586
+ # Test 4: Mutate via attribute syntax with new datetime
4587
+ dt3 = datetime(2023, 12, 25, 18, 0, 0, tzinfo=timezone.utc)
4588
+ m.my_dict["holiday"] = dt3
4589
+ assert m.my_dict["holiday"] == dt3
4590
+
4591
+ # Test 5: Verify mutation is serialized in dictionary syntax
4592
+ assert m["my_dict"]["holiday"] == "2023-12-25T18:00:00Z"
4593
+
4594
+ # Test 6: Update existing datetime via attribute syntax
4595
+ dt4 = datetime(2024, 1, 1, 0, 0, 0, tzinfo=timezone.utc)
4596
+ m.my_dict["created"] = dt4
4597
+ assert m.my_dict["created"] == dt4
4598
+ assert m["my_dict"]["created"] == "2024-01-01T00:00:00Z"
4599
+
4600
+ # Test 7: Verify all datetimes are deserialized correctly after mutation
4601
+ assert isinstance(m.my_dict["created"], datetime)
4602
+ assert isinstance(m.my_dict["updated"], datetime)
4603
+ assert isinstance(m.my_dict["holiday"], datetime)
4604
+
4605
+ # Test 8: Use dict update method with datetimes
4606
+ dt5 = datetime(2024, 6, 15, 12, 30, 0, tzinfo=timezone.utc)
4607
+ dt6 = datetime(2024, 7, 4, 16, 45, 0, tzinfo=timezone.utc)
4608
+ m.my_dict.update({"event1": dt5, "event2": dt6})
4609
+ assert m.my_dict["event1"] == dt5
4610
+ assert m["my_dict"]["event1"] == "2024-06-15T12:30:00Z"
4611
+
4612
+ # Test 9: Reassign entire dictionary with new datetimes
4613
+ dt7 = datetime(2025, 1, 1, 0, 0, 0, tzinfo=timezone.utc)
4614
+ dt8 = datetime(2025, 12, 31, 23, 59, 59, tzinfo=timezone.utc)
4615
+ m.my_dict = {"start": dt7, "end": dt8}
4616
+ assert m.my_dict["start"] == dt7
4617
+ assert m.my_dict["end"] == dt8
4618
+ assert m["my_dict"]["start"] == "2025-01-01T00:00:00Z"
4619
+ assert m["my_dict"]["end"] == "2025-12-31T23:59:59Z"
4620
+
4621
+ # Test 10: Cached object maintains datetime type
4622
+ dict_ref1 = m.my_dict
4623
+ dict_ref2 = m.my_dict
4624
+ assert dict_ref1 is dict_ref2
4625
+ assert isinstance(dict_ref1["start"], datetime)
4626
+ assert isinstance(dict_ref2["start"], datetime)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@typespec/http-client-python",
3
- "version": "0.23.0",
3
+ "version": "0.23.1",
4
4
  "author": "Microsoft Corporation",
5
5
  "description": "TypeSpec emitter for Python SDKs",
6
6
  "homepage": "https://typespec.io",