@typespec/http-client-python 0.33.0 → 0.34.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 (61) hide show
  1. package/dist/emitter/types.d.ts.map +1 -1
  2. package/dist/emitter/types.js +2 -0
  3. package/dist/emitter/types.js.map +1 -1
  4. package/emitter/src/types.ts +2 -0
  5. package/emitter/temp/tsconfig.tsbuildinfo +1 -1
  6. package/eng/scripts/ci/regenerate-common.ts +40 -15
  7. package/eng/scripts/setup/__pycache__/package_manager.cpython-311.pyc +0 -0
  8. package/eng/scripts/setup/__pycache__/venvtools.cpython-311.pyc +0 -0
  9. package/generator/build/lib/pygen/__init__.py +3 -3
  10. package/generator/build/lib/pygen/codegen/models/__init__.py +4 -2
  11. package/generator/build/lib/pygen/codegen/models/code_model.py +27 -5
  12. package/generator/build/lib/pygen/codegen/models/combined_type.py +3 -3
  13. package/generator/build/lib/pygen/codegen/models/enum_type.py +75 -25
  14. package/generator/build/lib/pygen/codegen/models/list_type.py +10 -4
  15. package/generator/build/lib/pygen/codegen/models/model_type.py +145 -4
  16. package/generator/build/lib/pygen/codegen/models/operation.py +1 -0
  17. package/generator/build/lib/pygen/codegen/models/paging_operation.py +5 -1
  18. package/generator/build/lib/pygen/codegen/models/parameter.py +2 -2
  19. package/generator/build/lib/pygen/codegen/models/primitive_types.py +30 -6
  20. package/generator/build/lib/pygen/codegen/models/property.py +21 -8
  21. package/generator/build/lib/pygen/codegen/models/response.py +16 -3
  22. package/generator/build/lib/pygen/codegen/models/utils.py +3 -6
  23. package/generator/build/lib/pygen/codegen/serializers/__init__.py +46 -11
  24. package/generator/build/lib/pygen/codegen/serializers/builder_serializer.py +48 -15
  25. package/generator/build/lib/pygen/codegen/serializers/model_init_serializer.py +1 -1
  26. package/generator/build/lib/pygen/codegen/serializers/model_serializer.py +127 -3
  27. package/generator/build/lib/pygen/codegen/serializers/types_serializer.py +263 -12
  28. package/generator/build/lib/pygen/codegen/serializers/unions_serializer.py +44 -0
  29. package/generator/build/lib/pygen/codegen/templates/model_container.py.jinja2 +1 -1
  30. package/generator/build/lib/pygen/codegen/templates/types.py.jinja2 +51 -2
  31. package/generator/build/lib/pygen/codegen/templates/unions.py.jinja2 +9 -0
  32. package/generator/build/lib/pygen/preprocess/__init__.py +137 -162
  33. package/generator/build/lib/pygen/utils.py +6 -24
  34. package/generator/dist/pygen-0.1.0-py3-none-any.whl +0 -0
  35. package/generator/pygen/__init__.py +3 -3
  36. package/generator/pygen/codegen/models/__init__.py +4 -2
  37. package/generator/pygen/codegen/models/code_model.py +27 -5
  38. package/generator/pygen/codegen/models/combined_type.py +3 -3
  39. package/generator/pygen/codegen/models/enum_type.py +75 -25
  40. package/generator/pygen/codegen/models/list_type.py +10 -4
  41. package/generator/pygen/codegen/models/model_type.py +145 -4
  42. package/generator/pygen/codegen/models/operation.py +1 -0
  43. package/generator/pygen/codegen/models/paging_operation.py +5 -1
  44. package/generator/pygen/codegen/models/parameter.py +2 -2
  45. package/generator/pygen/codegen/models/primitive_types.py +30 -6
  46. package/generator/pygen/codegen/models/property.py +21 -8
  47. package/generator/pygen/codegen/models/response.py +16 -3
  48. package/generator/pygen/codegen/models/utils.py +3 -6
  49. package/generator/pygen/codegen/serializers/__init__.py +46 -11
  50. package/generator/pygen/codegen/serializers/builder_serializer.py +48 -15
  51. package/generator/pygen/codegen/serializers/model_init_serializer.py +1 -1
  52. package/generator/pygen/codegen/serializers/model_serializer.py +127 -3
  53. package/generator/pygen/codegen/serializers/types_serializer.py +263 -12
  54. package/generator/pygen/codegen/serializers/unions_serializer.py +44 -0
  55. package/generator/pygen/codegen/templates/model_container.py.jinja2 +1 -1
  56. package/generator/pygen/codegen/templates/types.py.jinja2 +51 -2
  57. package/generator/pygen/codegen/templates/unions.py.jinja2 +9 -0
  58. package/generator/pygen/preprocess/__init__.py +137 -162
  59. package/generator/pygen/utils.py +6 -24
  60. package/generator/pygen.egg-info/SOURCES.txt +2 -0
  61. package/package.json +5 -5
@@ -10,7 +10,7 @@ from .constant_type import ConstantType
10
10
  from .enum_type import EnumType
11
11
  from .base import BaseType
12
12
  from .imports import FileImport, ImportType
13
- from .utils import add_to_description, add_to_pylint_disable
13
+ from .utils import add_to_description, add_to_pylint_disable, NamespaceType
14
14
 
15
15
  if TYPE_CHECKING:
16
16
  from .code_model import CodeModel
@@ -29,6 +29,7 @@ class Property(BaseModel): # pylint: disable=too-many-instance-attributes
29
29
  self.client_name: str = self.yaml_data["clientName"]
30
30
  self.type = type
31
31
  self.optional: bool = self.yaml_data["optional"]
32
+ self.nullable: bool = self.yaml_data.get("nullable", False)
32
33
  self.readonly: bool = self.yaml_data.get("readonly", False)
33
34
  self.visibility: list[str] = self.yaml_data.get("visibility", [])
34
35
  self.is_polymorphic: bool = self.yaml_data.get("isPolymorphic", False)
@@ -110,6 +111,12 @@ class Property(BaseModel): # pylint: disable=too-many-instance-attributes
110
111
  if self.is_base_discriminator:
111
112
  return "str"
112
113
  types_type_annotation = self.type.type_annotation(is_operation_file=is_operation_file, **kwargs)
114
+ serialize_namespace_type = kwargs.get("serialize_namespace_type")
115
+ # In TypedDict types.py, Optional means nullable (not "not required" — that's handled by Required/total=False)
116
+ if serialize_namespace_type == NamespaceType.TYPES_FILE:
117
+ if self.nullable:
118
+ return f"Optional[{types_type_annotation}]"
119
+ return types_type_annotation
113
120
  if (self.optional and self.client_default_value is None) or self.readonly:
114
121
  return f"Optional[{types_type_annotation}]"
115
122
  return types_type_annotation
@@ -152,15 +159,21 @@ class Property(BaseModel): # pylint: disable=too-many-instance-attributes
152
159
  if self.is_discriminator and isinstance(self.type, EnumType):
153
160
  return file_import
154
161
  file_import.merge(self.type.imports(**kwargs))
155
- if (self.optional and self.client_default_value is None) or self.readonly:
162
+ serialize_namespace_type = kwargs.get("serialize_namespace_type")
163
+ if serialize_namespace_type == NamespaceType.TYPES_FILE:
164
+ # In TypedDict types.py, Optional means nullable
165
+ if self.nullable:
166
+ file_import.add_submodule_import("typing", "Optional", ImportType.STDLIB)
167
+ elif (self.optional and self.client_default_value is None) or self.readonly:
156
168
  file_import.add_submodule_import("typing", "Optional", ImportType.STDLIB)
157
169
  if self.code_model.options["models-mode"] == "dpg":
158
- serialize_namespace = kwargs.get("serialize_namespace", self.code_model.namespace)
159
- file_import.add_submodule_import(
160
- self.code_model.get_relative_import_path(serialize_namespace, module_name="_utils.model_base"),
161
- "rest_discriminator" if self.is_discriminator else "rest_field",
162
- ImportType.LOCAL,
163
- )
170
+ if serialize_namespace_type != NamespaceType.TYPES_FILE:
171
+ serialize_namespace = kwargs.get("serialize_namespace", self.code_model.namespace)
172
+ file_import.add_submodule_import(
173
+ self.code_model.get_relative_import_path(serialize_namespace, module_name="_utils.model_base"),
174
+ "rest_discriminator" if self.is_discriminator else "rest_field",
175
+ ImportType.LOCAL,
176
+ )
164
177
  return file_import
165
178
 
166
179
  @classmethod
@@ -68,7 +68,12 @@ class Response(BaseModel):
68
68
 
69
69
  def get_polymorphic_subtypes(self, polymorphic_subtypes: list["ModelType"]) -> None:
70
70
  if self.type:
71
- self.type.get_polymorphic_subtypes(polymorphic_subtypes)
71
+ if isinstance(self.type, CombinedType):
72
+ target = self.type.target_model_subtype((ModelType,))
73
+ if target:
74
+ target.get_polymorphic_subtypes(polymorphic_subtypes)
75
+ else:
76
+ self.type.get_polymorphic_subtypes(polymorphic_subtypes)
72
77
 
73
78
  def get_json_template_representation(self) -> Any:
74
79
  if not self.type:
@@ -95,6 +100,7 @@ class Response(BaseModel):
95
100
  def type_annotation(self, **kwargs: Any) -> str:
96
101
  if self.type:
97
102
  kwargs["is_operation_file"] = True
103
+ kwargs["is_response"] = True
98
104
  type_annotation = self.type.type_annotation(**kwargs)
99
105
  if self.nullable:
100
106
  return f"Optional[{type_annotation}]"
@@ -102,11 +108,13 @@ class Response(BaseModel):
102
108
  return "None"
103
109
 
104
110
  def docstring_text(self, **kwargs: Any) -> str:
111
+ kwargs["is_response"] = True
105
112
  if self.nullable and self.type:
106
113
  return f"{self.type.docstring_text(**kwargs)} or None"
107
114
  return self.type.docstring_text(**kwargs) if self.type else "None"
108
115
 
109
116
  def docstring_type(self, **kwargs: Any) -> str:
117
+ kwargs["is_response"] = True
110
118
  if self.nullable and self.type:
111
119
  return f"{self.type.docstring_type(**kwargs)} or None"
112
120
  return self.type.docstring_type(**kwargs) if self.type else "None"
@@ -121,7 +129,7 @@ class Response(BaseModel):
121
129
  serialize_namespace = kwargs.get("serialize_namespace", self.code_model.namespace)
122
130
  file_import.add_submodule_import(
123
131
  self.code_model.get_relative_import_path(serialize_namespace),
124
- "_types",
132
+ "_unions",
125
133
  ImportType.LOCAL,
126
134
  TypingSection.TYPING,
127
135
  )
@@ -165,7 +173,12 @@ class PagingResponse(Response):
165
173
  )
166
174
 
167
175
  def get_polymorphic_subtypes(self, polymorphic_subtypes: list["ModelType"]) -> None:
168
- return self.item_type.get_polymorphic_subtypes(polymorphic_subtypes)
176
+ if isinstance(self.item_type, CombinedType):
177
+ target = self.item_type.target_model_subtype((ModelType,))
178
+ if target:
179
+ target.get_polymorphic_subtypes(polymorphic_subtypes)
180
+ else:
181
+ self.item_type.get_polymorphic_subtypes(polymorphic_subtypes)
169
182
 
170
183
  def get_json_template_representation(self) -> Any:
171
184
  return self.item_type.get_json_template_representation()
@@ -41,14 +41,11 @@ class NamespaceType(str, Enum):
41
41
  OPERATION = "operation"
42
42
  CLIENT = "client"
43
43
  TYPES_FILE = "types_file"
44
+ UNIONS_FILE = "unions_file"
44
45
 
45
46
 
46
47
  LOCALS_LENGTH_LIMIT = 25
47
48
 
48
- REQUEST_BUILDER_BODY_VARIABLES_LENGTH = (
49
- 6 # how many body variables are present in a request builder
50
- )
49
+ REQUEST_BUILDER_BODY_VARIABLES_LENGTH = 6 # how many body variables are present in a request builder
51
50
 
52
- OPERATION_BODY_VARIABLES_LENGTH = (
53
- 14 # how many body variables are present in an operation
54
- )
51
+ OPERATION_BODY_VARIABLES_LENGTH = 14 # how many body variables are present in an operation
@@ -33,6 +33,7 @@ from .patch_serializer import PatchSerializer
33
33
  from .sample_serializer import SampleSerializer
34
34
  from .test_serializer import TestSerializer, TestGeneralSerializer
35
35
  from .types_serializer import TypesSerializer
36
+ from .unions_serializer import UnionsSerializer
36
37
  from ...utils import to_snake_case, VALID_PACKAGE_MODE
37
38
  from .utils import extract_sample_name, get_namespace_from_package_name, get_namespace_config, hash_file_import
38
39
 
@@ -120,7 +121,7 @@ class JinjaSerializer(ReaderAndWriter):
120
121
  # If parsing the version fails, we assume the version file is not valid and overwrite.
121
122
  return False
122
123
 
123
- # pylint: disable=too-many-branches
124
+ # pylint: disable=too-many-branches,too-many-statements
124
125
  def serialize(self) -> None:
125
126
  # remove existing folders when generate from tsp
126
127
  if self.code_model.is_tsp and self.code_model.options.get("clear-output-folder"):
@@ -200,12 +201,13 @@ class JinjaSerializer(ReaderAndWriter):
200
201
  general_serializer.serialize_pkgutil_init_file(),
201
202
  )
202
203
 
203
- # _utils/py.typed/_types.py/_validation.py
204
+ # _utils/py.typed/_unions.py/types.py/_validation.py
204
205
  # is always put in top level namespace
205
206
  if self.code_model.is_top_namespace(client_namespace):
206
207
  self._serialize_and_write_top_level_folder(env=env, namespace=client_namespace)
207
208
 
208
209
  # add models folder if there are models in this namespace
210
+ is_typeddict_mode = self.code_model.options["models-mode"] == "typeddict"
209
211
  if (
210
212
  self.code_model.has_non_json_models(client_namespace_type.models) or client_namespace_type.enums
211
213
  ) and self.code_model.options["models-mode"]:
@@ -213,7 +215,27 @@ class JinjaSerializer(ReaderAndWriter):
213
215
  env=env,
214
216
  namespace=client_namespace,
215
217
  models=client_namespace_type.models,
216
- enums=client_namespace_type.enums,
218
+ enums=[] if is_typeddict_mode else client_namespace_type.enums,
219
+ )
220
+
221
+ # write types.py per namespace (alongside models/)
222
+ # Only generate types.py if at least one model/enum would be imported via types
223
+ # in operations (the model itself volunteers this via is_used_in_operations_via_types)
224
+ has_types_models = any(
225
+ m.is_used_in_operations_via_types for m in client_namespace_type.models if m.base != "json"
226
+ )
227
+ has_types_enums = any(e.is_typeddict_mode for e in client_namespace_type.enums)
228
+ if has_types_models or has_types_enums:
229
+ generation_dir = self.code_model.get_generation_dir(client_namespace)
230
+ self.write_file(
231
+ generation_dir / Path("types.py"),
232
+ TypesSerializer(
233
+ code_model=self.code_model,
234
+ env=env,
235
+ client_namespace=client_namespace,
236
+ models=client_namespace_type.models,
237
+ enums=client_namespace_type.enums if is_typeddict_mode else None,
238
+ ).serialize(),
217
239
  )
218
240
 
219
241
  if not self.code_model.options["models-mode"]:
@@ -298,11 +320,19 @@ class JinjaSerializer(ReaderAndWriter):
298
320
  ) -> None:
299
321
  # Write the models folder
300
322
  models_path = self.code_model.get_generation_dir(namespace) / "models"
301
- serializer = DpgModelSerializer if self.code_model.options["models-mode"] == "dpg" else MsrestModelSerializer
302
- if self.code_model.has_non_json_models(models):
323
+ models_mode = self.code_model.options["models-mode"]
324
+ if models_mode in ("dpg", "typeddict"):
325
+ serializer = DpgModelSerializer
326
+ else:
327
+ serializer = MsrestModelSerializer
328
+ # Filter out typed-dict-only models and typeddict copies — they only appear in types.py, not as model classes
329
+ class_models = [m for m in models if not m.is_typed_dict_only and m.base != "typeddict"]
330
+ if self.code_model.has_non_json_models(class_models):
303
331
  self.write_file(
304
332
  models_path / Path(f"{self.code_model.models_filename}.py"),
305
- serializer(code_model=self.code_model, env=env, client_namespace=namespace, models=models).serialize(),
333
+ serializer(
334
+ code_model=self.code_model, env=env, client_namespace=namespace, models=class_models
335
+ ).serialize(),
306
336
  )
307
337
  if enums:
308
338
  self.write_file(
@@ -313,7 +343,7 @@ class JinjaSerializer(ReaderAndWriter):
313
343
  )
314
344
  self.write_file(
315
345
  models_path / Path("__init__.py"),
316
- ModelInitSerializer(code_model=self.code_model, env=env, models=models, enums=enums).serialize(),
346
+ ModelInitSerializer(code_model=self.code_model, env=env, models=class_models, enums=enums).serialize(),
317
347
  )
318
348
 
319
349
  self._keep_patch_file(models_path / Path("_patch.py"), env)
@@ -442,7 +472,9 @@ class JinjaSerializer(ReaderAndWriter):
442
472
  # when there is client.py, there must be __init__.py
443
473
  self.write_file(
444
474
  generation_path / Path(f"{async_path}__init__.py"),
445
- general_serializer.serialize_init_file([c for c in clients if c.has_operations]),
475
+ general_serializer.serialize_init_file(
476
+ [c for c in clients if c.has_operations],
477
+ ),
446
478
  )
447
479
 
448
480
  # if there was a patch file before, we keep it
@@ -519,11 +551,14 @@ class JinjaSerializer(ReaderAndWriter):
519
551
  general_serializer.serialize_validation_file(),
520
552
  )
521
553
 
522
- # write _types.py
554
+ # write _unions.py
523
555
  if self.code_model.named_unions:
524
556
  self.write_file(
525
- generation_dir / Path("_types.py"),
526
- TypesSerializer(code_model=self.code_model, env=env).serialize(),
557
+ generation_dir / Path("_unions.py"),
558
+ UnionsSerializer(
559
+ code_model=self.code_model,
560
+ env=env,
561
+ ).serialize(),
527
562
  )
528
563
 
529
564
  # pylint: disable=line-too-long
@@ -196,6 +196,16 @@ def is_json_model_type(parameters: ParameterListType) -> bool:
196
196
  )
197
197
 
198
198
 
199
+ def _is_dpg_or_typeddict_body(body_param: BodyParameter) -> bool:
200
+ """Check if a body parameter is a DPG model or a CombinedType wrapping one."""
201
+ body_type = body_param.type
202
+ if isinstance(body_type, DPGModelType):
203
+ return True
204
+ if isinstance(body_type, CombinedType):
205
+ return body_type.target_model_subtype((DPGModelType,)) is not None
206
+ return False
207
+
208
+
199
209
  class _BuilderBaseSerializer(Generic[BuilderType]):
200
210
  def __init__(self, code_model: CodeModel, async_mode: bool, client_namespace: str) -> None:
201
211
  self.code_model = code_model
@@ -712,8 +722,20 @@ class _OperationSerializer(_BuilderBaseSerializer[OperationType]):
712
722
  f"_{body_kwarg_name} = self._serialize.body({body_param.client_name}, "
713
723
  f"'{serialization_type}'{is_xml_cmd}{serialization_ctxt_cmd})"
714
724
  )
725
+ elif self.code_model.options["models-mode"] == "typeddict":
726
+ # TypedDict-only models are plain dicts — no serialization needed
727
+ create_body_call = f"_{body_kwarg_name} = {body_param.client_name}"
715
728
  elif self.code_model.options["models-mode"] == "dpg":
716
- if json_serializable(body_param.default_content_type):
729
+ # Check if this is a typeddict-only model within dpg mode — skip serialization
730
+ body_model_type = body_param.type
731
+ if isinstance(body_model_type, CombinedType):
732
+ body_model_type = body_model_type.target_model_subtype((DPGModelType,))
733
+ is_typeddict_only_body = isinstance(body_model_type, DPGModelType) and getattr(
734
+ body_model_type, "is_typed_dict_only", False
735
+ )
736
+ if is_typeddict_only_body:
737
+ create_body_call = f"_{body_kwarg_name} = {body_param.client_name}"
738
+ elif json_serializable(body_param.default_content_type):
717
739
  if hasattr(body_param.type, "encode") and body_param.type.encode: # type: ignore
718
740
  create_body_call = (
719
741
  f"_{body_kwarg_name} = json.dumps({body_param.client_name}, "
@@ -791,9 +813,9 @@ class _OperationSerializer(_BuilderBaseSerializer[OperationType]):
791
813
  overload.request_builder.parameters.body_parameter.client_name for overload in builder.overloads
792
814
  ]
793
815
  all_dpg_model_overloads = False
794
- if self.code_model.options["models-mode"] == "dpg" and builder.overloads:
816
+ if self.code_model.options["models-mode"] in ("dpg", "typeddict") and builder.overloads:
795
817
  all_dpg_model_overloads = all(
796
- isinstance(o.parameters.body_parameter.type, DPGModelType) for o in builder.overloads
818
+ _is_dpg_or_typeddict_body(o.parameters.body_parameter) for o in builder.overloads
797
819
  )
798
820
  if not all_dpg_model_overloads:
799
821
  for v in sorted(set(client_names), key=client_names.index):
@@ -1002,6 +1024,12 @@ class _OperationSerializer(_BuilderBaseSerializer[OperationType]):
1002
1024
  elif self.code_model.options["models-mode"] == "dpg":
1003
1025
  if builder.has_stream_response:
1004
1026
  deserialize_code.append("deserialized = response.content")
1027
+ elif isinstance(response.type, ModelType) and response.type.is_typed_dict_only:
1028
+ # Typed-dict-only models skip deserialization — return raw JSON
1029
+ deserialize_code.append("if response.content:")
1030
+ deserialize_code.append(" deserialized = response.json()")
1031
+ deserialize_code.append("else:")
1032
+ deserialize_code.append(" deserialized = None")
1005
1033
  else:
1006
1034
  format_filed = (
1007
1035
  f', format="{response.type.encode}"'
@@ -1437,18 +1465,23 @@ class _PagingOperationSerializer(_OperationSerializer[PagingOperationType]):
1437
1465
  )
1438
1466
  pylint_disable = ""
1439
1467
  if self.code_model.options["models-mode"] == "dpg":
1440
- item_type = builder.item_type.type_annotation(
1441
- is_operation_file=True, serialize_namespace=self.serialize_namespace
1442
- )
1443
- pylint_disable = (
1444
- " # pylint: disable=protected-access" if getattr(builder.item_type, "internal", False) else ""
1445
- )
1446
- list_of_elem_deserialized = [
1447
- "_deserialize(",
1448
- f"{item_type},{pylint_disable}",
1449
- f"deserialized{access},",
1450
- ")",
1451
- ]
1468
+ is_item_typed_dict_only = isinstance(builder.item_type, ModelType) and builder.item_type.is_typed_dict_only
1469
+ if is_item_typed_dict_only:
1470
+ # Typed-dict-only models skip deserialization — return raw JSON items
1471
+ list_of_elem_deserialized = [f"deserialized{access}"]
1472
+ else:
1473
+ item_type = builder.item_type.type_annotation(
1474
+ is_operation_file=True, serialize_namespace=self.serialize_namespace
1475
+ )
1476
+ pylint_disable = (
1477
+ " # pylint: disable=protected-access" if getattr(builder.item_type, "internal", False) else ""
1478
+ )
1479
+ list_of_elem_deserialized = [
1480
+ "_deserialize(",
1481
+ f"{item_type},{pylint_disable}",
1482
+ f"deserialized{access},",
1483
+ ")",
1484
+ ]
1452
1485
  else:
1453
1486
  list_of_elem_deserialized = [f"deserialized{access}"]
1454
1487
  list_of_elem_deserialized_str = "\n ".join(list_of_elem_deserialized)
@@ -32,7 +32,7 @@ class ModelInitSerializer:
32
32
  ", ".join(model_enum_name_intersection)
33
33
  )
34
34
  )
35
- has_models = self.models
35
+ has_models = self.code_model.has_non_json_models(self.models)
36
36
  has_enums = self.enums
37
37
  template = self.env.get_template("model_init.py.jinja2")
38
38
  return template.render(
@@ -3,7 +3,7 @@
3
3
  # Licensed under the MIT License. See License.txt in the project root for
4
4
  # license information.
5
5
  # --------------------------------------------------------------------------
6
- from typing import Optional
6
+ from typing import Any, Optional
7
7
  from abc import ABC, abstractmethod
8
8
 
9
9
  from ..models import ModelType, Property, ConstantType, EnumValue, EnumType
@@ -73,12 +73,19 @@ def _get_xml_deserializer_enum_type(prop: Property) -> Optional[EnumType]:
73
73
  return prop_type if isinstance(prop_type, EnumType) else None
74
74
 
75
75
 
76
- def _documentation_string(prop: Property, description_keyword: str, docstring_type_keyword: str) -> list[str]:
76
+ def _documentation_string(
77
+ prop: Property, description_keyword: str, docstring_type_keyword: str, **kwargs: Any
78
+ ) -> list[str]:
77
79
  retval: list[str] = []
78
80
  sphinx_prefix = f":{description_keyword} {prop.client_name}:"
79
81
  description = prop.description(is_operation_file=False).replace("\\", "\\\\")
80
82
  retval.append(f"{sphinx_prefix} {description}" if description else sphinx_prefix)
81
- retval.append(f":{docstring_type_keyword} {prop.client_name}: {prop.type.docstring_type()}")
83
+ # In the types file, use type_annotation (the serialized form) for docstrings
84
+ if kwargs.get("serialize_namespace_type") == NamespaceType.TYPES_FILE:
85
+ doc_type = prop.type.type_annotation(**kwargs)
86
+ else:
87
+ doc_type = prop.type.docstring_type(**kwargs)
88
+ retval.append(f":{docstring_type_keyword} {prop.client_name}: {doc_type}")
82
89
  return retval
83
90
 
84
91
 
@@ -494,3 +501,120 @@ class DpgModelSerializer(_ModelSerializer):
494
501
  if final_result:
495
502
  return "# pylint: disable=" + ", ".join(final_result)
496
503
  return ""
504
+
505
+
506
+ class TypedDictModelSerializer(_ModelSerializer):
507
+ def _is_parent_discriminated_base(self, model: ModelType) -> bool:
508
+ """Check if any parent of this model is a discriminated base (has discriminated_subtypes)."""
509
+ return any(p.discriminated_subtypes for p in model.parents)
510
+
511
+ def _reorder_models(self, models: list[ModelType]) -> list[ModelType]:
512
+ """Reorder so discriminated base Union aliases come after all their subtypes."""
513
+ bases = [m for m in models if m.discriminated_subtypes]
514
+ non_bases = [m for m in models if not m.discriminated_subtypes]
515
+ return non_bases + bases
516
+
517
+ def serialize(self) -> str:
518
+ template = self.env.get_template("model_container.py.jinja2")
519
+ return template.render(
520
+ code_model=self.code_model,
521
+ imports=FileImportSerializer(self.imports()),
522
+ str=str,
523
+ serializer=self,
524
+ models=self._reorder_models(self.models),
525
+ )
526
+
527
+ def imports(self) -> FileImport:
528
+ file_import = FileImport(self.code_model)
529
+ has_required = False
530
+ has_discriminated_union = False
531
+ for model in self.models:
532
+ if model.base == "json":
533
+ continue
534
+ if model.discriminated_subtypes:
535
+ has_discriminated_union = True
536
+ file_import.merge(
537
+ model.imports(
538
+ is_operation_file=False,
539
+ serialize_namespace=self.serialize_namespace,
540
+ serialize_namespace_type=NamespaceType.MODEL,
541
+ )
542
+ )
543
+ for prop in model.properties:
544
+ file_import.merge(
545
+ prop.imports(
546
+ serialize_namespace=self.serialize_namespace,
547
+ serialize_namespace_type=NamespaceType.MODEL,
548
+ called_by_property=True,
549
+ )
550
+ )
551
+ if not (prop.optional or prop.client_default_value is not None):
552
+ has_required = True
553
+ for parent in model.parents:
554
+ if parent.client_namespace != model.client_namespace and not parent.discriminated_subtypes:
555
+ file_import.add_submodule_import(
556
+ self.code_model.get_relative_import_path(
557
+ self.serialize_namespace,
558
+ self.code_model.get_imported_namespace_for_model(parent.client_namespace),
559
+ ),
560
+ parent.name,
561
+ ImportType.LOCAL,
562
+ )
563
+ file_import.add_submodule_import("typing_extensions", "TypedDict", ImportType.STDLIB)
564
+ if has_required:
565
+ file_import.add_submodule_import("typing_extensions", "Required", ImportType.STDLIB)
566
+ if has_discriminated_union:
567
+ file_import.add_submodule_import("typing", "Union", ImportType.STDLIB)
568
+ return file_import
569
+
570
+ def declare_model(self, model: ModelType) -> str:
571
+ # If the model's parent is a discriminated base, don't inherit from it
572
+ non_discriminated_parents = [p for p in model.parents if not p.discriminated_subtypes]
573
+ if non_discriminated_parents:
574
+ basename = ", ".join([m.name for m in non_discriminated_parents])
575
+ return f"class {model.name}({basename}):{model.pylint_disable()}"
576
+ return f"class {model.name}(TypedDict, total=False):{model.pylint_disable()}"
577
+
578
+ @staticmethod
579
+ def get_properties_to_declare(model: ModelType) -> list[Property]:
580
+ # Only exclude inherited properties from non-discriminated parents
581
+ non_discriminated_parents = [p for p in model.parents if not p.discriminated_subtypes]
582
+ if non_discriminated_parents:
583
+ parent_properties = [p for bm in non_discriminated_parents for p in bm.properties]
584
+ properties_to_declare = [
585
+ p
586
+ for p in model.properties
587
+ if not any(
588
+ p.client_name == pp.client_name
589
+ and p.type_annotation() == pp.type_annotation()
590
+ and not p.is_base_discriminator
591
+ for pp in parent_properties
592
+ )
593
+ ]
594
+ else:
595
+ properties_to_declare = model.properties
596
+ return properties_to_declare
597
+
598
+ def declare_property(self, prop: Property) -> str:
599
+ type_annotation = prop.type_annotation(serialize_namespace=self.serialize_namespace)
600
+ is_optional = prop.optional or prop.client_default_value is not None
601
+ if is_optional:
602
+ return f"{prop.wire_name}: {type_annotation}"
603
+ return f"{prop.wire_name}: Required[{type_annotation}]"
604
+
605
+ def initialize_properties(self, model: ModelType) -> list[str]:
606
+ return []
607
+
608
+ def need_init(self, model: ModelType) -> bool:
609
+ return False
610
+
611
+ def discriminated_subtypes_union(self, model: ModelType) -> str:
612
+ subtypes = list(model.discriminated_subtypes.values())
613
+ subtype_names = [s.name for s in subtypes]
614
+ return f"{model.name} = Union[{', '.join(subtype_names)}]"
615
+
616
+ def is_discriminated_base(self, model: ModelType) -> bool:
617
+ return bool(model.discriminated_subtypes)
618
+
619
+ def global_pylint_disables(self) -> str:
620
+ return ""