@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.
- package/dist/emitter/utils.js.map +1 -1
- package/emitter/src/utils.ts +3 -3
- package/emitter/temp/tsconfig.tsbuildinfo +1 -1
- package/eng/scripts/setup/__pycache__/package_manager.cpython-311.pyc +0 -0
- package/eng/scripts/setup/__pycache__/venvtools.cpython-311.pyc +0 -0
- package/generator/build/lib/pygen/codegen/serializers/builder_serializer.py +6 -1
- package/generator/build/lib/pygen/codegen/templates/model_base.py.jinja2 +55 -2
- package/generator/build/lib/pygen/codegen/templates/serialization.py.jinja2 +14 -3
- package/generator/dist/pygen-0.1.0-py3-none-any.whl +0 -0
- package/generator/pygen/codegen/serializers/builder_serializer.py +6 -1
- package/generator/pygen/codegen/templates/model_base.py.jinja2 +55 -2
- package/generator/pygen/codegen/templates/serialization.py.jinja2 +14 -3
- package/generator/test/unittests/test_model_base_serialization.py +257 -5
- package/package.json +1 -1
|
Binary file
|
|
Binary file
|
|
@@ -403,7 +403,12 @@ class RequestBuilderSerializer(_BuilderBaseSerializer[RequestBuilderType]):
|
|
|
403
403
|
builder: RequestBuilderType,
|
|
404
404
|
) -> list[str]:
|
|
405
405
|
def _get_value(param):
|
|
406
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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):
|
|
Binary file
|
|
@@ -403,7 +403,12 @@ class RequestBuilderSerializer(_BuilderBaseSerializer[RequestBuilderType]):
|
|
|
403
403
|
builder: RequestBuilderType,
|
|
404
404
|
) -> list[str]:
|
|
405
405
|
def _get_value(param):
|
|
406
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|