@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.
- package/dist/emitter/code-model.d.ts.map +1 -1
- package/dist/emitter/code-model.js +35 -25
- package/dist/emitter/code-model.js.map +1 -1
- package/dist/emitter/http.d.ts +4 -4
- package/dist/emitter/http.d.ts.map +1 -1
- package/dist/emitter/http.js +41 -35
- package/dist/emitter/http.js.map +1 -1
- package/dist/emitter/types.d.ts +1 -1
- package/dist/emitter/types.d.ts.map +1 -1
- package/dist/emitter/types.js +2 -2
- package/dist/emitter/types.js.map +1 -1
- package/dist/emitter/utils.d.ts +2 -2
- package/dist/emitter/utils.d.ts.map +1 -1
- package/dist/emitter/utils.js +7 -6
- package/dist/emitter/utils.js.map +1 -1
- package/emitter/src/code-model.ts +61 -18
- package/emitter/src/http.ts +107 -22
- package/emitter/src/types.ts +2 -1
- package/emitter/src/utils.ts +14 -12
- package/emitter/temp/tsconfig.tsbuildinfo +1 -1
- package/eng/scripts/ci/dev_requirements.txt +3 -3
- package/eng/scripts/ci/pylintrc +1 -1
- package/eng/scripts/ci/regenerate.ts +8 -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/models/code_model.py +4 -0
- package/generator/build/lib/pygen/codegen/models/enum_type.py +8 -1
- package/generator/build/lib/pygen/codegen/models/list_type.py +6 -2
- package/generator/build/lib/pygen/codegen/models/model_type.py +2 -2
- package/generator/build/lib/pygen/codegen/models/operation.py +10 -1
- package/generator/build/lib/pygen/codegen/models/operation_group.py +3 -1
- package/generator/build/lib/pygen/codegen/models/request_builder.py +20 -3
- package/generator/build/lib/pygen/codegen/models/response.py +2 -2
- package/generator/build/lib/pygen/codegen/models/utils.py +7 -0
- package/generator/build/lib/pygen/codegen/serializers/builder_serializer.py +26 -12
- package/generator/build/lib/pygen/codegen/serializers/client_serializer.py +1 -2
- package/generator/build/lib/pygen/codegen/serializers/general_serializer.py +1 -1
- package/generator/build/lib/pygen/codegen/serializers/model_serializer.py +3 -0
- package/generator/build/lib/pygen/codegen/templates/enum.py.jinja2 +3 -1
- package/generator/build/lib/pygen/codegen/templates/model_base.py.jinja2 +65 -9
- 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/models/code_model.py +4 -0
- package/generator/pygen/codegen/models/enum_type.py +8 -1
- package/generator/pygen/codegen/models/list_type.py +6 -2
- package/generator/pygen/codegen/models/model_type.py +2 -2
- package/generator/pygen/codegen/models/operation.py +10 -1
- package/generator/pygen/codegen/models/operation_group.py +3 -1
- package/generator/pygen/codegen/models/request_builder.py +20 -3
- package/generator/pygen/codegen/models/response.py +2 -2
- package/generator/pygen/codegen/models/utils.py +7 -0
- package/generator/pygen/codegen/serializers/builder_serializer.py +26 -12
- package/generator/pygen/codegen/serializers/client_serializer.py +1 -2
- package/generator/pygen/codegen/serializers/general_serializer.py +1 -1
- package/generator/pygen/codegen/serializers/model_serializer.py +3 -0
- package/generator/pygen/codegen/templates/enum.py.jinja2 +3 -1
- package/generator/pygen/codegen/templates/model_base.py.jinja2 +65 -9
- package/generator/pygen/codegen/templates/serialization.py.jinja2 +14 -3
- package/generator/test/azure/mock_api_tests/asynctests/test_azure_client_generator_core_client_default_value_async.py +46 -0
- package/generator/test/azure/mock_api_tests/asynctests/test_azure_client_generator_core_client_location_async.py +48 -21
- package/generator/test/azure/mock_api_tests/asynctests/test_azure_resource_manager_multi_service_async.py +110 -0
- package/generator/test/azure/mock_api_tests/asynctests/test_service_multi_service_async.py +31 -0
- package/generator/test/azure/mock_api_tests/asynctests/test_special_words_async.py +18 -0
- package/generator/test/azure/mock_api_tests/test_azure_arm_operationtemplates.py +16 -0
- package/generator/test/azure/mock_api_tests/test_azure_client_generator_core_client_default_value.py +42 -0
- package/generator/test/azure/mock_api_tests/test_azure_client_generator_core_client_location.py +44 -19
- package/generator/test/azure/mock_api_tests/test_azure_resource_manager_multi_service.py +104 -0
- package/generator/test/azure/mock_api_tests/test_service_multi_service.py +29 -0
- package/generator/test/azure/mock_api_tests/test_special_words.py +17 -0
- package/generator/test/azure/requirements.txt +9 -1
- package/generator/test/generic_mock_api_tests/asynctests/test_parameters_query_async.py +18 -0
- package/generator/test/generic_mock_api_tests/test_parameters_query.py +17 -0
- package/generator/test/generic_mock_api_tests/test_typetest_scalar.py +0 -5
- package/generator/test/generic_mock_api_tests/test_typetest_union_discriminated.py +290 -0
- package/generator/test/unbranded/requirements.txt +2 -0
- package/generator/test/unittests/test_model_base_serialization.py +323 -5
- 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
|
-
|
|
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)
|