@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.
Files changed (31) hide show
  1. package/dist/emitter/types.d.ts.map +1 -1
  2. package/dist/emitter/types.js +1 -0
  3. package/dist/emitter/types.js.map +1 -1
  4. package/dist/emitter/utils.js.map +1 -1
  5. package/emitter/src/types.ts +1 -0
  6. package/emitter/src/utils.ts +3 -3
  7. package/emitter/temp/tsconfig.tsbuildinfo +1 -1
  8. package/eng/scripts/ci/regenerate.ts +5 -1
  9. package/eng/scripts/setup/__pycache__/package_manager.cpython-311.pyc +0 -0
  10. package/eng/scripts/setup/__pycache__/venvtools.cpython-311.pyc +0 -0
  11. package/generator/build/lib/pygen/codegen/models/property.py +1 -0
  12. package/generator/build/lib/pygen/codegen/serializers/builder_serializer.py +6 -1
  13. package/generator/build/lib/pygen/codegen/serializers/model_serializer.py +2 -0
  14. package/generator/build/lib/pygen/codegen/templates/macros.jinja2 +12 -5
  15. package/generator/build/lib/pygen/codegen/templates/model_base.py.jinja2 +83 -2
  16. package/generator/build/lib/pygen/codegen/templates/serialization.py.jinja2 +14 -3
  17. package/generator/dist/pygen-0.1.0-py3-none-any.whl +0 -0
  18. package/generator/pygen/codegen/models/property.py +1 -0
  19. package/generator/pygen/codegen/serializers/builder_serializer.py +6 -1
  20. package/generator/pygen/codegen/serializers/model_serializer.py +2 -0
  21. package/generator/pygen/codegen/templates/macros.jinja2 +12 -5
  22. package/generator/pygen/codegen/templates/model_base.py.jinja2 +83 -2
  23. package/generator/pygen/codegen/templates/serialization.py.jinja2 +14 -3
  24. package/generator/test/azure/requirements.txt +1 -0
  25. package/generator/test/generic_mock_api_tests/asynctests/test_encode_array_async.py +43 -0
  26. package/generator/test/generic_mock_api_tests/asynctests/test_specs_documentation_async.py +60 -0
  27. package/generator/test/generic_mock_api_tests/test_encode_array.py +38 -0
  28. package/generator/test/generic_mock_api_tests/test_specs_documentation.py +52 -0
  29. package/generator/test/unbranded/requirements.txt +1 -0
  30. package/generator/test/unittests/test_model_base_serialization.py +521 -5
  31. 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()
@@ -7,6 +7,7 @@
7
7
  -e ./generated/authentication-oauth2
8
8
  -e ./generated/authentication-union
9
9
  -e ./generated/setuppy-authentication-union
10
+ -e ./generated/specs-documentation
10
11
  -e ./generated/encode-duration
11
12
  -e ./generated/encode-numeric
12
13
  -e ./generated/encode-array
@@ -2,6 +2,7 @@
2
2
  # Copyright (c) Microsoft Corporation.
3
3
  # Licensed under the MIT License.
4
4
  # ------------------------------------
5
+ from ast import Mod
5
6
  import copy
6
7
  import decimal
7
8
  import json
@@ -928,9 +929,7 @@ def test_deserialization_callback_override():
928
929
 
929
930
  model_with_callback = MyModel2(prop=[1.3, 2.4, 3.5])
930
931
  assert model_with_callback.prop == ["1.3", "2.4", "3.5"]
931
- # since the deserialize function is not roundtrip-able, once we deserialize
932
- # the serialized version is the same
933
- assert model_with_callback["prop"] == [1.3, 2.4, 3.5]
932
+ assert model_with_callback["prop"] == ["1.3", "2.4", "3.5"]
934
933
 
935
934
 
936
935
  def test_deserialization_callback_override_parent():
@@ -966,7 +965,7 @@ def test_deserialization_callback_override_parent():
966
965
 
967
966
  child_model = ChildWithCallback(prop=[1, 1, 2, 3])
968
967
  assert child_model.prop == set(["1", "1", "2", "3"])
969
- assert child_model["prop"] == [1, 1, 2, 3]
968
+ assert child_model["prop"] == set(["1", "1", "2", "3"])
970
969
 
971
970
 
972
971
  def test_inheritance_basic():
@@ -3267,7 +3266,7 @@ def test_complex_array_wrapper(model: ArrayWrapper):
3267
3266
 
3268
3267
  model["array"] = [1, 2, 3, 4, 5]
3269
3268
  assert model.array == ["1", "2", "3", "4", "5"]
3270
- assert model["array"] == [1, 2, 3, 4, 5]
3269
+ assert model["array"] == ["1", "2", "3", "4", "5"]
3271
3270
 
3272
3271
 
3273
3272
  @pytest.mark.parametrize("model", [ArrayWrapper(array=[]), ArrayWrapper({"array": []})])
@@ -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)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@typespec/http-client-python",
3
- "version": "0.22.0",
3
+ "version": "0.23.1",
4
4
  "author": "Microsoft Corporation",
5
5
  "description": "TypeSpec emitter for Python SDKs",
6
6
  "homepage": "https://typespec.io",