@typespec/http-client-python 0.23.0 → 0.24.0

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.
Files changed (77) hide show
  1. package/dist/emitter/code-model.d.ts.map +1 -1
  2. package/dist/emitter/code-model.js +35 -25
  3. package/dist/emitter/code-model.js.map +1 -1
  4. package/dist/emitter/http.d.ts +4 -4
  5. package/dist/emitter/http.d.ts.map +1 -1
  6. package/dist/emitter/http.js +41 -35
  7. package/dist/emitter/http.js.map +1 -1
  8. package/dist/emitter/types.d.ts +1 -1
  9. package/dist/emitter/types.d.ts.map +1 -1
  10. package/dist/emitter/types.js +2 -2
  11. package/dist/emitter/types.js.map +1 -1
  12. package/dist/emitter/utils.d.ts +2 -2
  13. package/dist/emitter/utils.d.ts.map +1 -1
  14. package/dist/emitter/utils.js +7 -6
  15. package/dist/emitter/utils.js.map +1 -1
  16. package/emitter/src/code-model.ts +61 -18
  17. package/emitter/src/http.ts +107 -22
  18. package/emitter/src/types.ts +2 -1
  19. package/emitter/src/utils.ts +14 -12
  20. package/emitter/temp/tsconfig.tsbuildinfo +1 -1
  21. package/eng/scripts/ci/dev_requirements.txt +3 -3
  22. package/eng/scripts/ci/pylintrc +1 -1
  23. package/eng/scripts/ci/regenerate.ts +8 -1
  24. package/eng/scripts/setup/__pycache__/package_manager.cpython-311.pyc +0 -0
  25. package/eng/scripts/setup/__pycache__/venvtools.cpython-311.pyc +0 -0
  26. package/generator/build/lib/pygen/codegen/models/code_model.py +4 -0
  27. package/generator/build/lib/pygen/codegen/models/enum_type.py +8 -1
  28. package/generator/build/lib/pygen/codegen/models/list_type.py +6 -2
  29. package/generator/build/lib/pygen/codegen/models/model_type.py +2 -2
  30. package/generator/build/lib/pygen/codegen/models/operation.py +10 -1
  31. package/generator/build/lib/pygen/codegen/models/operation_group.py +3 -1
  32. package/generator/build/lib/pygen/codegen/models/request_builder.py +20 -3
  33. package/generator/build/lib/pygen/codegen/models/response.py +2 -2
  34. package/generator/build/lib/pygen/codegen/models/utils.py +7 -0
  35. package/generator/build/lib/pygen/codegen/serializers/builder_serializer.py +26 -12
  36. package/generator/build/lib/pygen/codegen/serializers/client_serializer.py +1 -2
  37. package/generator/build/lib/pygen/codegen/serializers/general_serializer.py +1 -1
  38. package/generator/build/lib/pygen/codegen/serializers/model_serializer.py +3 -0
  39. package/generator/build/lib/pygen/codegen/templates/enum.py.jinja2 +3 -1
  40. package/generator/build/lib/pygen/codegen/templates/model_base.py.jinja2 +65 -9
  41. package/generator/build/lib/pygen/codegen/templates/serialization.py.jinja2 +14 -3
  42. package/generator/dist/pygen-0.1.0-py3-none-any.whl +0 -0
  43. package/generator/pygen/codegen/models/code_model.py +4 -0
  44. package/generator/pygen/codegen/models/enum_type.py +8 -1
  45. package/generator/pygen/codegen/models/list_type.py +6 -2
  46. package/generator/pygen/codegen/models/model_type.py +2 -2
  47. package/generator/pygen/codegen/models/operation.py +10 -1
  48. package/generator/pygen/codegen/models/operation_group.py +3 -1
  49. package/generator/pygen/codegen/models/request_builder.py +20 -3
  50. package/generator/pygen/codegen/models/response.py +2 -2
  51. package/generator/pygen/codegen/models/utils.py +7 -0
  52. package/generator/pygen/codegen/serializers/builder_serializer.py +26 -12
  53. package/generator/pygen/codegen/serializers/client_serializer.py +1 -2
  54. package/generator/pygen/codegen/serializers/general_serializer.py +1 -1
  55. package/generator/pygen/codegen/serializers/model_serializer.py +3 -0
  56. package/generator/pygen/codegen/templates/enum.py.jinja2 +3 -1
  57. package/generator/pygen/codegen/templates/model_base.py.jinja2 +65 -9
  58. package/generator/pygen/codegen/templates/serialization.py.jinja2 +14 -3
  59. package/generator/test/azure/mock_api_tests/asynctests/test_azure_client_generator_core_client_default_value_async.py +46 -0
  60. package/generator/test/azure/mock_api_tests/asynctests/test_azure_client_generator_core_client_location_async.py +48 -21
  61. package/generator/test/azure/mock_api_tests/asynctests/test_azure_resource_manager_multi_service_async.py +110 -0
  62. package/generator/test/azure/mock_api_tests/asynctests/test_service_multi_service_async.py +31 -0
  63. package/generator/test/azure/mock_api_tests/asynctests/test_special_words_async.py +18 -0
  64. package/generator/test/azure/mock_api_tests/test_azure_arm_operationtemplates.py +16 -0
  65. package/generator/test/azure/mock_api_tests/test_azure_client_generator_core_client_default_value.py +42 -0
  66. package/generator/test/azure/mock_api_tests/test_azure_client_generator_core_client_location.py +44 -19
  67. package/generator/test/azure/mock_api_tests/test_azure_resource_manager_multi_service.py +104 -0
  68. package/generator/test/azure/mock_api_tests/test_service_multi_service.py +29 -0
  69. package/generator/test/azure/mock_api_tests/test_special_words.py +17 -0
  70. package/generator/test/azure/requirements.txt +9 -1
  71. package/generator/test/generic_mock_api_tests/asynctests/test_parameters_query_async.py +18 -0
  72. package/generator/test/generic_mock_api_tests/test_parameters_query.py +17 -0
  73. package/generator/test/generic_mock_api_tests/test_typetest_scalar.py +0 -5
  74. package/generator/test/generic_mock_api_tests/test_typetest_union_discriminated.py +290 -0
  75. package/generator/test/unbranded/requirements.txt +2 -0
  76. package/generator/test/unittests/test_model_base_serialization.py +323 -5
  77. package/package.json +5 -5
@@ -0,0 +1,290 @@
1
+ # -------------------------------------------------------------------------
2
+ # Copyright (c) Microsoft Corporation. All rights reserved.
3
+ # Licensed under the MIT License. See License.txt in the project root for
4
+ # license information.
5
+ # --------------------------------------------------------------------------
6
+ import pytest
7
+ from typetest.discriminatedunion import DiscriminatedClient
8
+ from typetest.discriminatedunion import models
9
+
10
+
11
+ @pytest.fixture
12
+ def client():
13
+ with DiscriminatedClient() as client:
14
+ yield client
15
+
16
+
17
+ @pytest.fixture
18
+ def cat_body():
19
+ """Cat model for testing."""
20
+ return models.Cat(name="Whiskers", meow=True)
21
+
22
+
23
+ @pytest.fixture
24
+ def dog_body():
25
+ """Dog model for testing."""
26
+ return models.Dog(name="Rex", bark=False)
27
+
28
+
29
+ # Tests for No Envelope / Default (inline discriminator with "kind")
30
+ @pytest.mark.skip(reason="After completely support discriminated unions, enable these tests")
31
+ class TestNoEnvelopeDefault:
32
+ """Test discriminated union with inline discriminator (no envelope)."""
33
+
34
+ def test_get_default_cat(self, client: DiscriminatedClient, cat_body: models.Cat):
35
+ """Test getting cat with default (no query param or kind=cat).
36
+
37
+ Expected response:
38
+ {
39
+ "kind": "cat",
40
+ "name": "Whiskers",
41
+ "meow": true
42
+ }
43
+ """
44
+ result = client.no_envelope.default.get()
45
+ assert result == cat_body
46
+ assert isinstance(result, models.Cat)
47
+
48
+ def test_get_with_kind_cat(self, client: DiscriminatedClient, cat_body: models.Cat):
49
+ """Test getting cat with kind=cat query parameter.
50
+
51
+ Expected response:
52
+ {
53
+ "kind": "cat",
54
+ "name": "Whiskers",
55
+ "meow": true
56
+ }
57
+ """
58
+ result = client.no_envelope.default.get(kind="cat")
59
+ assert result == cat_body
60
+ assert isinstance(result, models.Cat)
61
+
62
+ def test_get_with_kind_dog(self, client: DiscriminatedClient, dog_body: models.Dog):
63
+ """Test getting dog with kind=dog query parameter.
64
+
65
+ Expected response:
66
+ {
67
+ "kind": "dog",
68
+ "name": "Rex",
69
+ "bark": false
70
+ }
71
+ """
72
+ result = client.no_envelope.default.get(kind="dog")
73
+ assert result == dog_body
74
+ assert isinstance(result, models.Dog)
75
+
76
+ def test_put_cat(self, client: DiscriminatedClient, cat_body: models.Cat):
77
+ """Test sending cat with inline discriminator.
78
+
79
+ Expected request:
80
+ {
81
+ "kind": "cat",
82
+ "name": "Whiskers",
83
+ "meow": true
84
+ }
85
+ """
86
+ result = client.no_envelope.default.put(cat_body)
87
+ assert result == cat_body
88
+ assert isinstance(result, models.Cat)
89
+
90
+
91
+ # Tests for No Envelope / Custom Discriminator (inline with custom "type" property)
92
+ @pytest.mark.skip(reason="After completely support discriminated unions, enable these tests")
93
+ class TestNoEnvelopeCustomDiscriminator:
94
+ """Test discriminated union with inline discriminator and custom discriminator property name."""
95
+
96
+ def test_get_default_cat(self, client: DiscriminatedClient, cat_body: models.Cat):
97
+ """Test getting cat with default (no query param or type=cat).
98
+
99
+ Expected response:
100
+ {
101
+ "type": "cat",
102
+ "name": "Whiskers",
103
+ "meow": true
104
+ }
105
+ """
106
+ result = client.no_envelope.custom_discriminator.get()
107
+ assert result == cat_body
108
+ assert isinstance(result, models.Cat)
109
+
110
+ def test_get_with_type_cat(self, client: DiscriminatedClient, cat_body: models.Cat):
111
+ """Test getting cat with type=cat query parameter.
112
+
113
+ Expected response:
114
+ {
115
+ "type": "cat",
116
+ "name": "Whiskers",
117
+ "meow": true
118
+ }
119
+ """
120
+ result = client.no_envelope.custom_discriminator.get(type="cat")
121
+ assert result == cat_body
122
+ assert isinstance(result, models.Cat)
123
+
124
+ def test_get_with_type_dog(self, client: DiscriminatedClient, dog_body: models.Dog):
125
+ """Test getting dog with type=dog query parameter.
126
+
127
+ Expected response:
128
+ {
129
+ "type": "dog",
130
+ "name": "Rex",
131
+ "bark": false
132
+ }
133
+ """
134
+ result = client.no_envelope.custom_discriminator.get(type="dog")
135
+ assert result == dog_body
136
+ assert isinstance(result, models.Dog)
137
+
138
+ def test_put_cat(self, client: DiscriminatedClient, cat_body: models.Cat):
139
+ """Test sending cat with inline custom discriminator.
140
+
141
+ Expected request:
142
+ {
143
+ "type": "cat",
144
+ "name": "Whiskers",
145
+ "meow": true
146
+ }
147
+ """
148
+ result = client.no_envelope.custom_discriminator.put(cat_body)
149
+ assert result == cat_body
150
+ assert isinstance(result, models.Cat)
151
+
152
+
153
+ # Tests for Envelope / Object / Default (envelope with "kind" and "value")
154
+ @pytest.mark.skip(reason="After completely support discriminated unions, enable these tests")
155
+ class TestEnvelopeObjectDefault:
156
+ """Test discriminated union with default envelope serialization."""
157
+
158
+ def test_get_default_cat(self, client: DiscriminatedClient, cat_body: models.Cat):
159
+ """Test getting cat with default (no query param or kind=cat).
160
+
161
+ Expected response:
162
+ {
163
+ "kind": "cat",
164
+ "value": {
165
+ "name": "Whiskers",
166
+ "meow": true
167
+ }
168
+ }
169
+ """
170
+ result = client.envelope.object.default.get()
171
+ assert result == cat_body
172
+ assert isinstance(result, models.Cat)
173
+
174
+ def test_get_with_kind_cat(self, client: DiscriminatedClient, cat_body: models.Cat):
175
+ """Test getting cat with kind=cat query parameter.
176
+
177
+ Expected response:
178
+ {
179
+ "kind": "cat",
180
+ "value": {
181
+ "name": "Whiskers",
182
+ "meow": true
183
+ }
184
+ }
185
+ """
186
+ result = client.envelope.object.default.get(kind="cat")
187
+ assert result == cat_body
188
+ assert isinstance(result, models.Cat)
189
+
190
+ def test_get_with_kind_dog(self, client: DiscriminatedClient, dog_body: models.Dog):
191
+ """Test getting dog with kind=dog query parameter.
192
+
193
+ Expected response:
194
+ {
195
+ "kind": "dog",
196
+ "value": {
197
+ "name": "Rex",
198
+ "bark": false
199
+ }
200
+ }
201
+ """
202
+ result = client.envelope.object.default.get(kind="dog")
203
+ assert result == dog_body
204
+ assert isinstance(result, models.Dog)
205
+
206
+ def test_put_cat(self, client: DiscriminatedClient, cat_body: models.Cat):
207
+ """Test sending cat with envelope serialization.
208
+
209
+ Expected request:
210
+ {
211
+ "kind": "cat",
212
+ "value": {
213
+ "name": "Whiskers",
214
+ "meow": true
215
+ }
216
+ }
217
+ """
218
+ result = client.envelope.object.default.put(cat_body)
219
+ assert result == cat_body
220
+ assert isinstance(result, models.Cat)
221
+
222
+
223
+ # Tests for Envelope / Object / Custom Properties (envelope with custom "petType" and "petData")
224
+ @pytest.mark.skip(reason="After completely support discriminated unions, enable these tests")
225
+ class TestEnvelopeObjectCustomProperties:
226
+ """Test discriminated union with custom property names in envelope."""
227
+
228
+ def test_get_default_cat(self, client: DiscriminatedClient, cat_body: models.Cat):
229
+ """Test getting cat with default (no query param or petType=cat).
230
+
231
+ Expected response:
232
+ {
233
+ "petType": "cat",
234
+ "petData": {
235
+ "name": "Whiskers",
236
+ "meow": true
237
+ }
238
+ }
239
+ """
240
+ result = client.envelope.object.custom_properties.get()
241
+ assert result == cat_body
242
+ assert isinstance(result, models.Cat)
243
+
244
+ def test_get_with_pet_type_cat(self, client: DiscriminatedClient, cat_body: models.Cat):
245
+ """Test getting cat with petType=cat query parameter.
246
+
247
+ Expected response:
248
+ {
249
+ "petType": "cat",
250
+ "petData": {
251
+ "name": "Whiskers",
252
+ "meow": true
253
+ }
254
+ }
255
+ """
256
+ result = client.envelope.object.custom_properties.get(pet_type="cat")
257
+ assert result == cat_body
258
+ assert isinstance(result, models.Cat)
259
+
260
+ def test_get_with_pet_type_dog(self, client: DiscriminatedClient, dog_body: models.Dog):
261
+ """Test getting dog with petType=dog query parameter.
262
+
263
+ Expected response:
264
+ {
265
+ "petType": "dog",
266
+ "petData": {
267
+ "name": "Rex",
268
+ "bark": false
269
+ }
270
+ }
271
+ """
272
+ result = client.envelope.object.custom_properties.get(pet_type="dog")
273
+ assert result == dog_body
274
+ assert isinstance(result, models.Dog)
275
+
276
+ def test_put_cat(self, client: DiscriminatedClient, cat_body: models.Cat):
277
+ """Test sending cat with custom property names in envelope.
278
+
279
+ Expected request:
280
+ {
281
+ "petType": "cat",
282
+ "petData": {
283
+ "name": "Whiskers",
284
+ "meow": true
285
+ }
286
+ }
287
+ """
288
+ result = client.envelope.object.custom_properties.put(cat_body)
289
+ assert result == cat_body
290
+ assert isinstance(result, models.Cat)
@@ -14,6 +14,7 @@
14
14
  -e ./generated/parameters-basic
15
15
  -e ./generated/parameters-collection-format
16
16
  -e ./generated/parameters-path
17
+ -e ./generated/parameters-query
17
18
  -e ./generated/parameters-spread
18
19
  -e ./generated/serialization-encoded-name-json
19
20
  -e ./generated/server-endpoint-not-defined
@@ -40,6 +41,7 @@
40
41
  -e ./generated/typetest-property-additionalproperties
41
42
  -e ./generated/typetest-scalar
42
43
  -e ./generated/typetest-union
44
+ -e ./generated/typetest-discriminatedunion
43
45
  -e ./generated/typetest-model-empty
44
46
  -e ./generated/headasbooleantrue
45
47
  -e ./generated/headasbooleanfalse
@@ -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
@@ -331,6 +332,72 @@ def test_property_is_a_type():
331
332
  assert fishery.fish.species == fishery.fish["species"] == fishery["fish"]["species"] == "Salmon"
332
333
 
333
334
 
335
+ def test_model_initialization():
336
+ class DatetimeModel(Model):
337
+ datetime_value: datetime.datetime = rest_field(name="datetimeValue")
338
+
339
+ @overload
340
+ def __init__(self, *, datetime_value: datetime.datetime): ...
341
+
342
+ @overload
343
+ def __init__(self, mapping: Mapping[str, Any], /): ...
344
+
345
+ def __init__(self, *args, **kwargs):
346
+ super().__init__(*args, **kwargs)
347
+
348
+ val_str = "9999-12-31T23:59:59.999000Z"
349
+ val = isodate.parse_datetime(val_str)
350
+
351
+ # when initialize model with dict, the dict value is shall be serialized value
352
+ model1 = DatetimeModel({"datetimeValue": val_str})
353
+ assert model1["datetimeValue"] == val_str
354
+ assert model1.datetime_value == val
355
+
356
+ # when initialize model with keyword args, the value is deserialized value
357
+ model2 = DatetimeModel(datetime_value=val)
358
+ assert model2["datetimeValue"] == val_str
359
+ assert model2.datetime_value == val
360
+
361
+ # what if we initialize with dict but the dict has deserialized value? this case show what happens.
362
+ # Since we always serialize the value before initializing the model from dict, we could still get correct result
363
+ model3 = DatetimeModel({"datetimeValue": val})
364
+ assert model3["datetimeValue"] == val_str
365
+ assert model3.datetime_value == val
366
+
367
+
368
+ def test_model_dict_prop_initialization():
369
+ class DatetimeModel(Model):
370
+ dict_prop: dict[str, datetime.datetime] = rest_field(name="dictProp")
371
+
372
+ @overload
373
+ def __init__(self, *, dict_prop: dict[str, datetime.datetime]): ...
374
+
375
+ @overload
376
+ def __init__(self, mapping: Mapping[str, Any], /): ...
377
+
378
+ def __init__(self, *args, **kwargs):
379
+ super().__init__(*args, **kwargs)
380
+
381
+ val_str = "9999-12-31T23:59:59.999000Z"
382
+ val = isodate.parse_datetime(val_str)
383
+
384
+ # when initialize model with dict, the dict value is shall be serialized value
385
+ model1 = DatetimeModel({"dictProp": {"key1": val_str}})
386
+ assert model1["dictProp"] == {"key1": val_str}
387
+ assert model1.dict_prop == {"key1": val}
388
+
389
+ # when initialize model with keyword args, the value is deserialized value
390
+ model2 = DatetimeModel(dict_prop={"key1": val})
391
+ assert model2["dictProp"] == {"key1": val_str}
392
+ assert model2.dict_prop == {"key1": val}
393
+
394
+ # what if we initialize with dict but the dict has deserialized value? this case show what happens.
395
+ # Since we always serialize the value before initializing the model from dict, we could still get correct result
396
+ model3 = DatetimeModel({"dictProp": {"key1": val}})
397
+ assert model3["dictProp"] == {"key1": val_str}
398
+ assert model3.dict_prop == {"key1": val}
399
+
400
+
334
401
  def test_datetime_deserialization():
335
402
  class DatetimeModel(Model):
336
403
  datetime_value: datetime.datetime = rest_field(name="datetimeValue")
@@ -928,9 +995,7 @@ def test_deserialization_callback_override():
928
995
 
929
996
  model_with_callback = MyModel2(prop=[1.3, 2.4, 3.5])
930
997
  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]
998
+ assert model_with_callback["prop"] == ["1.3", "2.4", "3.5"]
934
999
 
935
1000
 
936
1001
  def test_deserialization_callback_override_parent():
@@ -966,7 +1031,7 @@ def test_deserialization_callback_override_parent():
966
1031
 
967
1032
  child_model = ChildWithCallback(prop=[1, 1, 2, 3])
968
1033
  assert child_model.prop == set(["1", "1", "2", "3"])
969
- assert child_model["prop"] == [1, 1, 2, 3]
1034
+ assert child_model["prop"] == set(["1", "1", "2", "3"])
970
1035
 
971
1036
 
972
1037
  def test_inheritance_basic():
@@ -3267,7 +3332,7 @@ def test_complex_array_wrapper(model: ArrayWrapper):
3267
3332
 
3268
3333
  model["array"] = [1, 2, 3, 4, 5]
3269
3334
  assert model.array == ["1", "2", "3", "4", "5"]
3270
- assert model["array"] == [1, 2, 3, 4, 5]
3335
+ assert model["array"] == ["1", "2", "3", "4", "5"]
3271
3336
 
3272
3337
 
3273
3338
  @pytest.mark.parametrize("model", [ArrayWrapper(array=[]), ArrayWrapper({"array": []})])
@@ -4372,3 +4437,256 @@ def test_array_encode_with_special_characters():
4372
4437
 
4373
4438
  assert model.pipe_values == ["path/to/file", "another-path", "final.path"]
4374
4439
  assert model["pipeValues"] == "path/to/file|another-path|final.path"
4440
+
4441
+
4442
+ def test_dictionary_set():
4443
+ """Test that dictionary mutations via attribute syntax persist and sync to dictionary syntax."""
4444
+
4445
+ class MyModel(Model):
4446
+ my_dict: dict[str, int] = rest_field(visibility=["read", "create", "update", "delete", "query"])
4447
+
4448
+ # Test 1: Basic mutation via attribute syntax
4449
+ m = MyModel(my_dict={"a": 1, "b": 2})
4450
+ assert m.my_dict == {"a": 1, "b": 2}
4451
+
4452
+ # Test 2: Add new key via attribute syntax and verify it persists
4453
+ m.my_dict["c"] = 3
4454
+ assert m.my_dict["c"] == 3
4455
+ assert m.my_dict == {"a": 1, "b": 2, "c": 3}
4456
+
4457
+ # Test 3: Verify mutation is reflected in dictionary syntax
4458
+ assert m["my_dict"] == {"a": 1, "b": 2, "c": 3}
4459
+
4460
+ # Test 4: Modify existing key via attribute syntax
4461
+ m.my_dict["a"] = 100
4462
+ assert m.my_dict["a"] == 100
4463
+ assert m["my_dict"]["a"] == 100
4464
+
4465
+ # Test 5: Delete key via attribute syntax
4466
+ del m.my_dict["b"]
4467
+ assert "b" not in m.my_dict
4468
+ assert "b" not in m["my_dict"]
4469
+
4470
+ # Test 6: Update via dict methods
4471
+ m.my_dict.update({"d": 4, "e": 5})
4472
+ assert m.my_dict["d"] == 4
4473
+ assert m.my_dict["e"] == 5
4474
+ assert m["my_dict"]["d"] == 4
4475
+
4476
+ # Test 7: Clear via attribute syntax and verify via dictionary syntax
4477
+ m.my_dict.clear()
4478
+ assert len(m.my_dict) == 0
4479
+ assert len(m["my_dict"]) == 0
4480
+
4481
+ # Test 8: Reassign entire dictionary via attribute syntax
4482
+ m.my_dict = {"x": 10, "y": 20}
4483
+ assert m.my_dict == {"x": 10, "y": 20}
4484
+ assert m["my_dict"] == {"x": 10, "y": 20}
4485
+
4486
+ # Test 9: Mutation after reassignment
4487
+ m.my_dict["z"] = 30
4488
+ assert m.my_dict["z"] == 30
4489
+ assert m["my_dict"]["z"] == 30
4490
+
4491
+ # Test 10: Access via dictionary syntax first, then mutate via attribute syntax
4492
+ m.my_dict["w"] = 40
4493
+ assert m["my_dict"]["w"] == 40
4494
+
4495
+ # Test 11: Multiple accesses maintain same cached object
4496
+ dict_ref1 = m.my_dict
4497
+ dict_ref2 = m.my_dict
4498
+ assert dict_ref1 is dict_ref2
4499
+ dict_ref1["new_key"] = 999
4500
+ assert dict_ref2["new_key"] == 999
4501
+ assert m.my_dict["new_key"] == 999
4502
+
4503
+
4504
+ def test_list_set():
4505
+ """Test that list mutations via attribute syntax persist and sync to dictionary syntax."""
4506
+
4507
+ class MyModel(Model):
4508
+ my_list: list[int] = rest_field(visibility=["read", "create", "update", "delete", "query"])
4509
+
4510
+ # Test 1: Basic mutation via attribute syntax
4511
+ m = MyModel(my_list=[1, 2, 3])
4512
+ assert m.my_list == [1, 2, 3]
4513
+
4514
+ # Test 2: Append via attribute syntax and verify it persists
4515
+ m.my_list.append(4)
4516
+ assert m.my_list == [1, 2, 3, 4]
4517
+
4518
+ # Test 3: Verify mutation is reflected in dictionary syntax
4519
+ assert m["my_list"] == [1, 2, 3, 4]
4520
+
4521
+ # Test 4: Modify existing element via attribute syntax
4522
+ m.my_list[0] = 100
4523
+ assert m.my_list[0] == 100
4524
+ assert m["my_list"][0] == 100
4525
+
4526
+ # Test 5: Extend list via attribute syntax
4527
+ m.my_list.extend([5, 6])
4528
+ assert m.my_list == [100, 2, 3, 4, 5, 6]
4529
+ assert m["my_list"] == [100, 2, 3, 4, 5, 6]
4530
+
4531
+ # Test 6: Remove element via attribute syntax
4532
+ m.my_list.remove(2)
4533
+ assert 2 not in m.my_list
4534
+ assert 2 not in m["my_list"]
4535
+
4536
+ # Test 7: Pop element
4537
+ popped = m.my_list.pop()
4538
+ assert popped == 6
4539
+ assert 6 not in m.my_list
4540
+ assert 6 not in m["my_list"]
4541
+
4542
+ # Test 8: Insert element
4543
+ m.my_list.insert(0, 999)
4544
+ assert m.my_list[0] == 999
4545
+ assert m["my_list"][0] == 999
4546
+
4547
+ # Test 9: Clear via attribute syntax
4548
+ m.my_list.clear()
4549
+ assert len(m.my_list) == 0
4550
+ assert len(m["my_list"]) == 0
4551
+
4552
+ # Test 10: Reassign entire list via attribute syntax
4553
+ m.my_list = [10, 20, 30]
4554
+ assert m.my_list == [10, 20, 30]
4555
+ assert m["my_list"] == [10, 20, 30]
4556
+
4557
+ # Test 11: Mutation after reassignment
4558
+ m.my_list.append(40)
4559
+ assert m.my_list == [10, 20, 30, 40]
4560
+ assert m["my_list"] == [10, 20, 30, 40]
4561
+
4562
+ # Test 12: Multiple accesses maintain same cached object
4563
+ list_ref1 = m.my_list
4564
+ list_ref2 = m.my_list
4565
+ assert list_ref1 is list_ref2
4566
+ list_ref1.append(50)
4567
+ assert 50 in list_ref2
4568
+ assert 50 in m.my_list
4569
+
4570
+
4571
+ def test_set_collection():
4572
+ """Test that set mutations via attribute syntax persist and sync to dictionary syntax."""
4573
+
4574
+ class MyModel(Model):
4575
+ my_set: set[int] = rest_field(visibility=["read", "create", "update", "delete", "query"])
4576
+
4577
+ # Test 1: Basic mutation via attribute syntax
4578
+ m = MyModel(my_set={1, 2, 3})
4579
+ assert m.my_set == {1, 2, 3}
4580
+
4581
+ # Test 2: Add via attribute syntax and verify it persists
4582
+ m.my_set.add(4)
4583
+ assert 4 in m.my_set
4584
+
4585
+ # Test 3: Verify mutation is reflected in dictionary syntax
4586
+ assert 4 in m["my_set"]
4587
+
4588
+ # Test 4: Remove element via attribute syntax
4589
+ m.my_set.remove(2)
4590
+ assert 2 not in m.my_set
4591
+ assert 2 not in m["my_set"]
4592
+
4593
+ # Test 5: Update set via attribute syntax
4594
+ m.my_set.update({5, 6, 7})
4595
+ assert m.my_set == {1, 3, 4, 5, 6, 7}
4596
+ assert m["my_set"] == {1, 3, 4, 5, 6, 7}
4597
+
4598
+ # Test 6: Discard element
4599
+ m.my_set.discard(1)
4600
+ assert 1 not in m.my_set
4601
+ assert 1 not in m["my_set"]
4602
+
4603
+ # Test 7: Clear via attribute syntax
4604
+ m.my_set.clear()
4605
+ assert len(m.my_set) == 0
4606
+ assert len(m["my_set"]) == 0
4607
+
4608
+ # Test 8: Reassign entire set via attribute syntax
4609
+ m.my_set = {10, 20, 30}
4610
+ assert m.my_set == {10, 20, 30}
4611
+ assert m["my_set"] == {10, 20, 30}
4612
+
4613
+ # Test 9: Mutation after reassignment
4614
+ m.my_set.add(40)
4615
+ assert 40 in m.my_set
4616
+ assert 40 in m["my_set"]
4617
+
4618
+ # Test 10: Multiple accesses maintain same cached object
4619
+ set_ref1 = m.my_set
4620
+ set_ref2 = m.my_set
4621
+ assert set_ref1 is set_ref2
4622
+ set_ref1.add(50)
4623
+ assert 50 in set_ref2
4624
+ assert 50 in m.my_set
4625
+
4626
+
4627
+ def test_dictionary_set_datetime():
4628
+ """Test that dictionary with datetime values properly serializes/deserializes."""
4629
+ from datetime import datetime, timezone
4630
+
4631
+ class MyModel(Model):
4632
+ my_dict: dict[str, datetime] = rest_field(visibility=["read", "create", "update", "delete", "query"])
4633
+
4634
+ # Test 1: Initialize with datetime values
4635
+ dt1 = datetime(2023, 1, 15, 10, 30, 45, tzinfo=timezone.utc)
4636
+ dt2 = datetime(2023, 6, 20, 14, 15, 30, tzinfo=timezone.utc)
4637
+ m = MyModel(my_dict={"created": dt1, "updated": dt2})
4638
+
4639
+ # Test 2: Access via attribute syntax returns datetime objects
4640
+ assert isinstance(m.my_dict["created"], datetime)
4641
+ assert isinstance(m.my_dict["updated"], datetime)
4642
+ assert m.my_dict["created"] == dt1
4643
+ assert m.my_dict["updated"] == dt2
4644
+
4645
+ # Test 3: Access via dictionary syntax returns serialized strings (ISO format)
4646
+ dict_access = m["my_dict"]
4647
+ assert isinstance(dict_access["created"], str)
4648
+ assert isinstance(dict_access["updated"], str)
4649
+ assert dict_access["created"] == "2023-01-15T10:30:45Z"
4650
+ assert dict_access["updated"] == "2023-06-20T14:15:30Z"
4651
+
4652
+ # Test 4: Mutate via attribute syntax with new datetime
4653
+ dt3 = datetime(2023, 12, 25, 18, 0, 0, tzinfo=timezone.utc)
4654
+ m.my_dict["holiday"] = dt3
4655
+ assert m.my_dict["holiday"] == dt3
4656
+
4657
+ # Test 5: Verify mutation is serialized in dictionary syntax
4658
+ assert m["my_dict"]["holiday"] == "2023-12-25T18:00:00Z"
4659
+
4660
+ # Test 6: Update existing datetime via attribute syntax
4661
+ dt4 = datetime(2024, 1, 1, 0, 0, 0, tzinfo=timezone.utc)
4662
+ m.my_dict["created"] = dt4
4663
+ assert m.my_dict["created"] == dt4
4664
+ assert m["my_dict"]["created"] == "2024-01-01T00:00:00Z"
4665
+
4666
+ # Test 7: Verify all datetimes are deserialized correctly after mutation
4667
+ assert isinstance(m.my_dict["created"], datetime)
4668
+ assert isinstance(m.my_dict["updated"], datetime)
4669
+ assert isinstance(m.my_dict["holiday"], datetime)
4670
+
4671
+ # Test 8: Use dict update method with datetimes
4672
+ dt5 = datetime(2024, 6, 15, 12, 30, 0, tzinfo=timezone.utc)
4673
+ dt6 = datetime(2024, 7, 4, 16, 45, 0, tzinfo=timezone.utc)
4674
+ m.my_dict.update({"event1": dt5, "event2": dt6})
4675
+ assert m.my_dict["event1"] == dt5
4676
+ assert m["my_dict"]["event1"] == "2024-06-15T12:30:00Z"
4677
+
4678
+ # Test 9: Reassign entire dictionary with new datetimes
4679
+ dt7 = datetime(2025, 1, 1, 0, 0, 0, tzinfo=timezone.utc)
4680
+ dt8 = datetime(2025, 12, 31, 23, 59, 59, tzinfo=timezone.utc)
4681
+ m.my_dict = {"start": dt7, "end": dt8}
4682
+ assert m.my_dict["start"] == dt7
4683
+ assert m.my_dict["end"] == dt8
4684
+ assert m["my_dict"]["start"] == "2025-01-01T00:00:00Z"
4685
+ assert m["my_dict"]["end"] == "2025-12-31T23:59:59Z"
4686
+
4687
+ # Test 10: Cached object maintains datetime type
4688
+ dict_ref1 = m.my_dict
4689
+ dict_ref2 = m.my_dict
4690
+ assert dict_ref1 is dict_ref2
4691
+ assert isinstance(dict_ref1["start"], datetime)
4692
+ assert isinstance(dict_ref2["start"], datetime)