@typespec/http-client-python 0.22.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/types.d.ts.map +1 -1
- package/dist/emitter/types.js +1 -0
- package/dist/emitter/types.js.map +1 -1
- package/dist/emitter/utils.js.map +1 -1
- package/emitter/src/types.ts +1 -0
- package/emitter/src/utils.ts +3 -3
- package/emitter/temp/tsconfig.tsbuildinfo +1 -1
- package/eng/scripts/ci/regenerate.ts +5 -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/property.py +1 -0
- package/generator/build/lib/pygen/codegen/serializers/builder_serializer.py +6 -1
- package/generator/build/lib/pygen/codegen/serializers/model_serializer.py +2 -0
- package/generator/build/lib/pygen/codegen/templates/macros.jinja2 +12 -5
- package/generator/build/lib/pygen/codegen/templates/model_base.py.jinja2 +83 -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/models/property.py +1 -0
- package/generator/pygen/codegen/serializers/builder_serializer.py +6 -1
- package/generator/pygen/codegen/serializers/model_serializer.py +2 -0
- package/generator/pygen/codegen/templates/macros.jinja2 +12 -5
- package/generator/pygen/codegen/templates/model_base.py.jinja2 +83 -2
- package/generator/pygen/codegen/templates/serialization.py.jinja2 +14 -3
- package/generator/test/azure/requirements.txt +1 -0
- package/generator/test/generic_mock_api_tests/asynctests/test_encode_array_async.py +43 -0
- package/generator/test/generic_mock_api_tests/asynctests/test_specs_documentation_async.py +60 -0
- package/generator/test/generic_mock_api_tests/test_encode_array.py +38 -0
- package/generator/test/generic_mock_api_tests/test_specs_documentation.py +52 -0
- package/generator/test/unbranded/requirements.txt +1 -0
- package/generator/test/unittests/test_model_base_serialization.py +521 -5
- package/package.json +1 -1
|
@@ -0,0 +1,38 @@
|
|
|
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
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
from encode.array import ArrayClient, models
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@pytest.fixture
|
|
12
|
+
def client():
|
|
13
|
+
with ArrayClient() as client:
|
|
14
|
+
yield client
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_comma_delimited(client: ArrayClient):
|
|
18
|
+
body = models.CommaDelimitedArrayProperty(value=["blue", "red", "green"])
|
|
19
|
+
result = client.property.comma_delimited(body)
|
|
20
|
+
assert result.value == ["blue", "red", "green"]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_space_delimited(client: ArrayClient):
|
|
24
|
+
body = models.SpaceDelimitedArrayProperty(value=["blue", "red", "green"])
|
|
25
|
+
result = client.property.space_delimited(body)
|
|
26
|
+
assert result.value == ["blue", "red", "green"]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_pipe_delimited(client: ArrayClient):
|
|
30
|
+
body = models.PipeDelimitedArrayProperty(value=["blue", "red", "green"])
|
|
31
|
+
result = client.property.pipe_delimited(body)
|
|
32
|
+
assert result.value == ["blue", "red", "green"]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_newline_delimited(client: ArrayClient):
|
|
36
|
+
body = models.NewlineDelimitedArrayProperty(value=["blue", "red", "green"])
|
|
37
|
+
result = client.property.newline_delimited(body)
|
|
38
|
+
assert result.value == ["blue", "red", "green"]
|
|
@@ -0,0 +1,52 @@
|
|
|
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
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
from specs.documentation import DocumentationClient, models
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@pytest.fixture
|
|
12
|
+
def client():
|
|
13
|
+
with DocumentationClient(endpoint="http://localhost:3000") as client:
|
|
14
|
+
yield client
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class TestLists:
|
|
18
|
+
def test_bullet_points_op(self, client: DocumentationClient):
|
|
19
|
+
# GET /documentation/lists/bullet-points/op
|
|
20
|
+
# Expected: 204 No Content
|
|
21
|
+
client.lists.bullet_points_op()
|
|
22
|
+
|
|
23
|
+
@pytest.mark.skip(reason="https://github.com/microsoft/typespec/issues/9173")
|
|
24
|
+
def test_bullet_points_model(self, client: DocumentationClient):
|
|
25
|
+
# POST /documentation/lists/bullet-points/model
|
|
26
|
+
# Expected: 200 OK
|
|
27
|
+
client.lists.bullet_points_model(input=models.BulletPointsModel(prop="Simple"))
|
|
28
|
+
|
|
29
|
+
# Also test with JSON
|
|
30
|
+
client.lists.bullet_points_model(body={"input": {"prop": "Simple"}})
|
|
31
|
+
|
|
32
|
+
def test_numbered(self, client: DocumentationClient):
|
|
33
|
+
# GET /documentation/lists/numbered
|
|
34
|
+
# Expected: 204 No Content
|
|
35
|
+
client.lists.numbered()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class TestTextFormatting:
|
|
39
|
+
def test_bold_text(self, client: DocumentationClient):
|
|
40
|
+
# GET /documentation/text-formatting/bold
|
|
41
|
+
# Expected: 204 No Content
|
|
42
|
+
client.text_formatting.bold_text()
|
|
43
|
+
|
|
44
|
+
def test_italic_text(self, client: DocumentationClient):
|
|
45
|
+
# GET /documentation/text-formatting/italic
|
|
46
|
+
# Expected: 204 No Content
|
|
47
|
+
client.text_formatting.italic_text()
|
|
48
|
+
|
|
49
|
+
def test_combined_formatting(self, client: DocumentationClient):
|
|
50
|
+
# GET /documentation/text-formatting/combined
|
|
51
|
+
# Expected: 204 No Content
|
|
52
|
+
client.text_formatting.combined_formatting()
|
|
@@ -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": []})])
|
|
@@ -4108,3 +4107,520 @@ def test_multi_layer_discriminator():
|
|
|
4108
4107
|
|
|
4109
4108
|
assert AnotherPet(name="Buddy", trained=True) == model_pet
|
|
4110
4109
|
assert AnotherDog(name="Rex", trained=True, breed="German Shepherd") == model_dog
|
|
4110
|
+
|
|
4111
|
+
|
|
4112
|
+
def test_array_encode_comma_delimited():
|
|
4113
|
+
"""Test commaDelimited format for array of strings"""
|
|
4114
|
+
|
|
4115
|
+
class CommaDelimitedModel(Model):
|
|
4116
|
+
colors: list[str] = rest_field(format="commaDelimited")
|
|
4117
|
+
|
|
4118
|
+
@overload
|
|
4119
|
+
def __init__(self, *, colors: list[str]): ...
|
|
4120
|
+
|
|
4121
|
+
@overload
|
|
4122
|
+
def __init__(self, mapping: Mapping[str, Any], /): ...
|
|
4123
|
+
|
|
4124
|
+
def __init__(self, *args, **kwargs):
|
|
4125
|
+
super().__init__(*args, **kwargs)
|
|
4126
|
+
|
|
4127
|
+
# Test serialization: list[str] -> comma-delimited string
|
|
4128
|
+
model = CommaDelimitedModel(colors=["blue", "red", "green"])
|
|
4129
|
+
assert model.colors == ["blue", "red", "green"]
|
|
4130
|
+
assert model["colors"] == "blue,red,green"
|
|
4131
|
+
|
|
4132
|
+
# Test deserialization: comma-delimited string -> list[str]
|
|
4133
|
+
model = CommaDelimitedModel({"colors": "blue,red,green"})
|
|
4134
|
+
assert model.colors == ["blue", "red", "green"]
|
|
4135
|
+
assert model["colors"] == "blue,red,green"
|
|
4136
|
+
|
|
4137
|
+
# Test with empty list
|
|
4138
|
+
model = CommaDelimitedModel(colors=[])
|
|
4139
|
+
assert model.colors == []
|
|
4140
|
+
assert model["colors"] == ""
|
|
4141
|
+
|
|
4142
|
+
# Test with single item
|
|
4143
|
+
model = CommaDelimitedModel(colors=["blue"])
|
|
4144
|
+
assert model.colors == ["blue"]
|
|
4145
|
+
assert model["colors"] == "blue"
|
|
4146
|
+
|
|
4147
|
+
|
|
4148
|
+
def test_array_encode_pipe_delimited():
|
|
4149
|
+
"""Test pipeDelimited format for array of strings"""
|
|
4150
|
+
|
|
4151
|
+
class PipeDelimitedModel(Model):
|
|
4152
|
+
colors: list[str] = rest_field(format="pipeDelimited")
|
|
4153
|
+
|
|
4154
|
+
@overload
|
|
4155
|
+
def __init__(self, *, colors: list[str]): ...
|
|
4156
|
+
|
|
4157
|
+
@overload
|
|
4158
|
+
def __init__(self, mapping: Mapping[str, Any], /): ...
|
|
4159
|
+
|
|
4160
|
+
def __init__(self, *args, **kwargs):
|
|
4161
|
+
super().__init__(*args, **kwargs)
|
|
4162
|
+
|
|
4163
|
+
# Test serialization: list[str] -> pipe-delimited string
|
|
4164
|
+
model = PipeDelimitedModel(colors=["blue", "red", "green"])
|
|
4165
|
+
assert model.colors == ["blue", "red", "green"]
|
|
4166
|
+
assert model["colors"] == "blue|red|green"
|
|
4167
|
+
|
|
4168
|
+
# Test deserialization: pipe-delimited string -> list[str]
|
|
4169
|
+
model = PipeDelimitedModel({"colors": "blue|red|green"})
|
|
4170
|
+
assert model.colors == ["blue", "red", "green"]
|
|
4171
|
+
assert model["colors"] == "blue|red|green"
|
|
4172
|
+
|
|
4173
|
+
# Test with empty list
|
|
4174
|
+
model = PipeDelimitedModel(colors=[])
|
|
4175
|
+
assert model.colors == []
|
|
4176
|
+
assert model["colors"] == ""
|
|
4177
|
+
|
|
4178
|
+
|
|
4179
|
+
def test_array_encode_space_delimited():
|
|
4180
|
+
"""Test spaceDelimited format for array of strings"""
|
|
4181
|
+
|
|
4182
|
+
class SpaceDelimitedModel(Model):
|
|
4183
|
+
colors: list[str] = rest_field(format="spaceDelimited")
|
|
4184
|
+
|
|
4185
|
+
@overload
|
|
4186
|
+
def __init__(self, *, colors: list[str]): ...
|
|
4187
|
+
|
|
4188
|
+
@overload
|
|
4189
|
+
def __init__(self, mapping: Mapping[str, Any], /): ...
|
|
4190
|
+
|
|
4191
|
+
def __init__(self, *args, **kwargs):
|
|
4192
|
+
super().__init__(*args, **kwargs)
|
|
4193
|
+
|
|
4194
|
+
# Test serialization: list[str] -> space-delimited string
|
|
4195
|
+
model = SpaceDelimitedModel(colors=["blue", "red", "green"])
|
|
4196
|
+
assert model.colors == ["blue", "red", "green"]
|
|
4197
|
+
assert model["colors"] == "blue red green"
|
|
4198
|
+
|
|
4199
|
+
# Test deserialization: space-delimited string -> list[str]
|
|
4200
|
+
model = SpaceDelimitedModel({"colors": "blue red green"})
|
|
4201
|
+
assert model.colors == ["blue", "red", "green"]
|
|
4202
|
+
assert model["colors"] == "blue red green"
|
|
4203
|
+
|
|
4204
|
+
# Test with empty list
|
|
4205
|
+
model = SpaceDelimitedModel(colors=[])
|
|
4206
|
+
assert model.colors == []
|
|
4207
|
+
assert model["colors"] == ""
|
|
4208
|
+
|
|
4209
|
+
|
|
4210
|
+
def test_array_encode_newline_delimited():
|
|
4211
|
+
"""Test newlineDelimited format for array of strings"""
|
|
4212
|
+
|
|
4213
|
+
class NewlineDelimitedModel(Model):
|
|
4214
|
+
colors: list[str] = rest_field(format="newlineDelimited")
|
|
4215
|
+
|
|
4216
|
+
@overload
|
|
4217
|
+
def __init__(self, *, colors: list[str]): ...
|
|
4218
|
+
|
|
4219
|
+
@overload
|
|
4220
|
+
def __init__(self, mapping: Mapping[str, Any], /): ...
|
|
4221
|
+
|
|
4222
|
+
def __init__(self, *args, **kwargs):
|
|
4223
|
+
super().__init__(*args, **kwargs)
|
|
4224
|
+
|
|
4225
|
+
# Test serialization: list[str] -> newline-delimited string
|
|
4226
|
+
model = NewlineDelimitedModel(colors=["blue", "red", "green"])
|
|
4227
|
+
assert model.colors == ["blue", "red", "green"]
|
|
4228
|
+
assert model["colors"] == "blue\nred\ngreen"
|
|
4229
|
+
|
|
4230
|
+
# Test deserialization: newline-delimited string -> list[str]
|
|
4231
|
+
model = NewlineDelimitedModel({"colors": "blue\nred\ngreen"})
|
|
4232
|
+
assert model.colors == ["blue", "red", "green"]
|
|
4233
|
+
assert model["colors"] == "blue\nred\ngreen"
|
|
4234
|
+
|
|
4235
|
+
# Test with empty list
|
|
4236
|
+
model = NewlineDelimitedModel(colors=[])
|
|
4237
|
+
assert model.colors == []
|
|
4238
|
+
assert model["colors"] == ""
|
|
4239
|
+
|
|
4240
|
+
|
|
4241
|
+
def test_array_encode_optional():
|
|
4242
|
+
"""Test array encoding with optional fields"""
|
|
4243
|
+
|
|
4244
|
+
class OptionalEncodedModel(Model):
|
|
4245
|
+
colors: Optional[list[str]] = rest_field(default=None, format="commaDelimited")
|
|
4246
|
+
|
|
4247
|
+
@overload
|
|
4248
|
+
def __init__(self, *, colors: Optional[list[str]] = None): ...
|
|
4249
|
+
|
|
4250
|
+
@overload
|
|
4251
|
+
def __init__(self, mapping: Mapping[str, Any], /): ...
|
|
4252
|
+
|
|
4253
|
+
def __init__(self, *args, **kwargs):
|
|
4254
|
+
super().__init__(*args, **kwargs)
|
|
4255
|
+
|
|
4256
|
+
# Test with None
|
|
4257
|
+
model = OptionalEncodedModel(colors=None)
|
|
4258
|
+
assert model.colors is None
|
|
4259
|
+
assert model["colors"] is None
|
|
4260
|
+
|
|
4261
|
+
# Test with value
|
|
4262
|
+
model = OptionalEncodedModel(colors=["blue", "red"])
|
|
4263
|
+
assert model.colors == ["blue", "red"]
|
|
4264
|
+
assert model["colors"] == "blue,red"
|
|
4265
|
+
|
|
4266
|
+
# Test deserialization with None
|
|
4267
|
+
model = OptionalEncodedModel({"colors": None})
|
|
4268
|
+
assert model.colors is None
|
|
4269
|
+
|
|
4270
|
+
|
|
4271
|
+
def test_array_encode_modification():
|
|
4272
|
+
"""Test modifying array-encoded fields"""
|
|
4273
|
+
|
|
4274
|
+
class ModifiableModel(Model):
|
|
4275
|
+
colors: list[str] = rest_field(format="commaDelimited")
|
|
4276
|
+
|
|
4277
|
+
@overload
|
|
4278
|
+
def __init__(self, *, colors: list[str]): ...
|
|
4279
|
+
|
|
4280
|
+
@overload
|
|
4281
|
+
def __init__(self, mapping: Mapping[str, Any], /): ...
|
|
4282
|
+
|
|
4283
|
+
def __init__(self, *args, **kwargs):
|
|
4284
|
+
super().__init__(*args, **kwargs)
|
|
4285
|
+
|
|
4286
|
+
model = ModifiableModel(colors=["blue", "red"])
|
|
4287
|
+
assert model.colors == ["blue", "red"]
|
|
4288
|
+
assert model["colors"] == "blue,red"
|
|
4289
|
+
|
|
4290
|
+
# Modify through property
|
|
4291
|
+
model.colors = ["green", "yellow", "purple"]
|
|
4292
|
+
assert model.colors == ["green", "yellow", "purple"]
|
|
4293
|
+
assert model["colors"] == "green,yellow,purple"
|
|
4294
|
+
|
|
4295
|
+
# Modify through dict access
|
|
4296
|
+
model["colors"] = "orange,pink"
|
|
4297
|
+
assert model.colors == ["orange", "pink"]
|
|
4298
|
+
assert model["colors"] == "orange,pink"
|
|
4299
|
+
|
|
4300
|
+
|
|
4301
|
+
def test_array_encode_json_roundtrip():
|
|
4302
|
+
"""Test JSON serialization and deserialization with array encoding"""
|
|
4303
|
+
|
|
4304
|
+
class JsonModel(Model):
|
|
4305
|
+
pipe_colors: list[str] = rest_field(name="pipeColors", format="pipeDelimited")
|
|
4306
|
+
comma_colors: list[str] = rest_field(name="commaColors", format="commaDelimited")
|
|
4307
|
+
|
|
4308
|
+
@overload
|
|
4309
|
+
def __init__(
|
|
4310
|
+
self,
|
|
4311
|
+
*,
|
|
4312
|
+
pipe_colors: list[str],
|
|
4313
|
+
comma_colors: list[str],
|
|
4314
|
+
): ...
|
|
4315
|
+
|
|
4316
|
+
@overload
|
|
4317
|
+
def __init__(self, mapping: Mapping[str, Any], /): ...
|
|
4318
|
+
|
|
4319
|
+
def __init__(self, *args, **kwargs):
|
|
4320
|
+
super().__init__(*args, **kwargs)
|
|
4321
|
+
|
|
4322
|
+
model = JsonModel(
|
|
4323
|
+
pipe_colors=["blue", "red", "green"],
|
|
4324
|
+
comma_colors=["small", "medium", "large"],
|
|
4325
|
+
)
|
|
4326
|
+
|
|
4327
|
+
# Serialize to JSON
|
|
4328
|
+
json_str = json.dumps(dict(model), cls=SdkJSONEncoder)
|
|
4329
|
+
assert json.loads(json_str) == {
|
|
4330
|
+
"pipeColors": "blue|red|green",
|
|
4331
|
+
"commaColors": "small,medium,large",
|
|
4332
|
+
}
|
|
4333
|
+
|
|
4334
|
+
# Deserialize from JSON
|
|
4335
|
+
deserialized = JsonModel(json.loads(json_str))
|
|
4336
|
+
assert deserialized.pipe_colors == ["blue", "red", "green"]
|
|
4337
|
+
assert deserialized.comma_colors == ["small", "medium", "large"]
|
|
4338
|
+
|
|
4339
|
+
|
|
4340
|
+
def test_array_encode_with_special_characters():
|
|
4341
|
+
"""Test array encoding with strings containing special characters"""
|
|
4342
|
+
|
|
4343
|
+
class SpecialCharsModel(Model):
|
|
4344
|
+
comma_values: list[str] = rest_field(name="commaValues", format="commaDelimited")
|
|
4345
|
+
pipe_values: list[str] = rest_field(name="pipeValues", format="pipeDelimited")
|
|
4346
|
+
|
|
4347
|
+
@overload
|
|
4348
|
+
def __init__(
|
|
4349
|
+
self,
|
|
4350
|
+
*,
|
|
4351
|
+
comma_values: list[str],
|
|
4352
|
+
pipe_values: list[str],
|
|
4353
|
+
): ...
|
|
4354
|
+
|
|
4355
|
+
@overload
|
|
4356
|
+
def __init__(self, mapping: Mapping[str, Any], /): ...
|
|
4357
|
+
|
|
4358
|
+
def __init__(self, *args, **kwargs):
|
|
4359
|
+
super().__init__(*args, **kwargs)
|
|
4360
|
+
|
|
4361
|
+
# Test with strings that might contain delimiters
|
|
4362
|
+
# Note: In real usage, the strings should not contain the delimiter character
|
|
4363
|
+
# This test documents current behavior
|
|
4364
|
+
model = SpecialCharsModel(
|
|
4365
|
+
comma_values=["value with spaces", "another-value", "value_3"],
|
|
4366
|
+
pipe_values=["path/to/file", "another-path", "final.path"],
|
|
4367
|
+
)
|
|
4368
|
+
|
|
4369
|
+
assert model.comma_values == ["value with spaces", "another-value", "value_3"]
|
|
4370
|
+
assert model["commaValues"] == "value with spaces,another-value,value_3"
|
|
4371
|
+
|
|
4372
|
+
assert model.pipe_values == ["path/to/file", "another-path", "final.path"]
|
|
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)
|