@typespec/http-client-python 0.32.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 (73) hide show
  1. package/dist/emitter/emitter.js +10 -2
  2. package/dist/emitter/emitter.js.map +1 -1
  3. package/dist/emitter/lib.d.ts +6 -0
  4. package/dist/emitter/lib.d.ts.map +1 -1
  5. package/dist/emitter/lib.js +29 -0
  6. package/dist/emitter/lib.js.map +1 -1
  7. package/dist/emitter/types.d.ts.map +1 -1
  8. package/dist/emitter/types.js +2 -0
  9. package/dist/emitter/types.js.map +1 -1
  10. package/emitter/src/emitter.ts +9 -1
  11. package/emitter/src/lib.ts +36 -0
  12. package/emitter/src/types.ts +2 -0
  13. package/emitter/temp/tsconfig.tsbuildinfo +1 -1
  14. package/eng/scripts/ci/regenerate-common.ts +40 -15
  15. package/eng/scripts/setup/__pycache__/package_manager.cpython-311.pyc +0 -0
  16. package/eng/scripts/setup/__pycache__/venvtools.cpython-311.pyc +0 -0
  17. package/generator/build/lib/pygen/__init__.py +4 -3
  18. package/generator/build/lib/pygen/codegen/models/__init__.py +4 -2
  19. package/generator/build/lib/pygen/codegen/models/code_model.py +27 -5
  20. package/generator/build/lib/pygen/codegen/models/combined_type.py +3 -3
  21. package/generator/build/lib/pygen/codegen/models/enum_type.py +75 -25
  22. package/generator/build/lib/pygen/codegen/models/list_type.py +10 -4
  23. package/generator/build/lib/pygen/codegen/models/model_type.py +145 -4
  24. package/generator/build/lib/pygen/codegen/models/operation.py +1 -0
  25. package/generator/build/lib/pygen/codegen/models/paging_operation.py +5 -1
  26. package/generator/build/lib/pygen/codegen/models/parameter.py +2 -2
  27. package/generator/build/lib/pygen/codegen/models/primitive_types.py +30 -6
  28. package/generator/build/lib/pygen/codegen/models/property.py +21 -8
  29. package/generator/build/lib/pygen/codegen/models/response.py +16 -3
  30. package/generator/build/lib/pygen/codegen/models/utils.py +12 -10
  31. package/generator/build/lib/pygen/codegen/serializers/__init__.py +46 -11
  32. package/generator/build/lib/pygen/codegen/serializers/builder_serializer.py +48 -15
  33. package/generator/build/lib/pygen/codegen/serializers/general_serializer.py +32 -2
  34. package/generator/build/lib/pygen/codegen/serializers/model_init_serializer.py +1 -1
  35. package/generator/build/lib/pygen/codegen/serializers/model_serializer.py +127 -3
  36. package/generator/build/lib/pygen/codegen/serializers/types_serializer.py +263 -12
  37. package/generator/build/lib/pygen/codegen/serializers/unions_serializer.py +44 -0
  38. package/generator/build/lib/pygen/codegen/templates/model_container.py.jinja2 +1 -1
  39. package/generator/build/lib/pygen/codegen/templates/packaging_templates/pyproject.toml.jinja2 +30 -2
  40. package/generator/build/lib/pygen/codegen/templates/types.py.jinja2 +51 -2
  41. package/generator/build/lib/pygen/codegen/templates/unions.py.jinja2 +9 -0
  42. package/generator/build/lib/pygen/preprocess/__init__.py +104 -13
  43. package/generator/build/lib/pygen/utils.py +16 -0
  44. package/generator/dist/pygen-0.1.0-py3-none-any.whl +0 -0
  45. package/generator/pygen/__init__.py +4 -3
  46. package/generator/pygen/codegen/models/__init__.py +4 -2
  47. package/generator/pygen/codegen/models/code_model.py +27 -5
  48. package/generator/pygen/codegen/models/combined_type.py +3 -3
  49. package/generator/pygen/codegen/models/enum_type.py +75 -25
  50. package/generator/pygen/codegen/models/list_type.py +10 -4
  51. package/generator/pygen/codegen/models/model_type.py +145 -4
  52. package/generator/pygen/codegen/models/operation.py +1 -0
  53. package/generator/pygen/codegen/models/paging_operation.py +5 -1
  54. package/generator/pygen/codegen/models/parameter.py +2 -2
  55. package/generator/pygen/codegen/models/primitive_types.py +30 -6
  56. package/generator/pygen/codegen/models/property.py +21 -8
  57. package/generator/pygen/codegen/models/response.py +16 -3
  58. package/generator/pygen/codegen/models/utils.py +12 -10
  59. package/generator/pygen/codegen/serializers/__init__.py +46 -11
  60. package/generator/pygen/codegen/serializers/builder_serializer.py +48 -15
  61. package/generator/pygen/codegen/serializers/general_serializer.py +32 -2
  62. package/generator/pygen/codegen/serializers/model_init_serializer.py +1 -1
  63. package/generator/pygen/codegen/serializers/model_serializer.py +127 -3
  64. package/generator/pygen/codegen/serializers/types_serializer.py +263 -12
  65. package/generator/pygen/codegen/serializers/unions_serializer.py +44 -0
  66. package/generator/pygen/codegen/templates/model_container.py.jinja2 +1 -1
  67. package/generator/pygen/codegen/templates/packaging_templates/pyproject.toml.jinja2 +30 -2
  68. package/generator/pygen/codegen/templates/types.py.jinja2 +51 -2
  69. package/generator/pygen/codegen/templates/unions.py.jinja2 +9 -0
  70. package/generator/pygen/preprocess/__init__.py +104 -13
  71. package/generator/pygen/utils.py +16 -0
  72. package/generator/pygen.egg-info/SOURCES.txt +2 -0
  73. package/package.json +5 -5
@@ -77,11 +77,19 @@ class ModelType(BaseType): # pylint: disable=too-many-instance-attributes, too-
77
77
  self.cross_language_definition_id: Optional[str] = self.yaml_data.get("crossLanguageDefinitionId")
78
78
  self.usage: int = self.yaml_data.get("usage", UsageFlags.Input.value | UsageFlags.Output.value)
79
79
  self.client_namespace: str = self.yaml_data.get("clientNamespace", code_model.namespace)
80
+ self.is_typed_dict_only: bool = (
81
+ self.yaml_data.get("typedDictOnly", False) or code_model.options["models-mode"] == "typeddict"
82
+ )
80
83
 
81
84
  @property
82
85
  def is_usage_output(self) -> bool:
83
86
  return bool(self.usage & UsageFlags.Output.value)
84
87
 
88
+ @property
89
+ def is_used_in_operations_via_types(self) -> bool:
90
+ """Whether this model would be imported from types.py (not models) in operations."""
91
+ return False
92
+
85
93
  @property
86
94
  def flattened_property(self) -> Optional[Property]:
87
95
  try:
@@ -275,12 +283,20 @@ class GeneratedModelType(ModelType):
275
283
  def type_annotation(self, **kwargs: Any) -> str:
276
284
  is_operation_file = kwargs.pop("is_operation_file", False)
277
285
  skip_quote = kwargs.get("skip_quote", False)
286
+ serialize_namespace_type = kwargs.get("serialize_namespace_type")
278
287
  module_name = ""
279
- if kwargs.get("need_model_alias", True):
288
+ # In types.py, use bare name to avoid pyright "variable in type expression" errors
289
+ if serialize_namespace_type == NamespaceType.TYPES_FILE:
290
+ pass # no module prefix, no internal file prefix
291
+ elif kwargs.get("need_model_alias", True):
280
292
  serialize_namespace = kwargs.get("serialize_namespace", self.code_model.namespace)
281
293
  model_alias = self.code_model.get_unique_models_alias(serialize_namespace, self.client_namespace)
282
294
  module_name = f"{model_alias}."
283
- file_name = f"{self.code_model.models_filename}." if self.internal else ""
295
+ file_name = (
296
+ f"{self.code_model.models_filename}."
297
+ if self.internal and serialize_namespace_type != NamespaceType.TYPES_FILE
298
+ else ""
299
+ )
284
300
  retval = module_name + file_name + self.name
285
301
  return retval if is_operation_file or skip_quote else f'"{retval}"'
286
302
 
@@ -305,7 +321,7 @@ class GeneratedModelType(ModelType):
305
321
  alias = self.code_model.get_unique_models_alias(serialize_namespace, self.client_namespace)
306
322
  serialize_namespace_type = kwargs.get("serialize_namespace_type")
307
323
  called_by_property = kwargs.get("called_by_property", False)
308
- # add import for models in operations or _types file
324
+ # add import for models in operations, types, or unions file
309
325
  if serialize_namespace_type in [NamespaceType.OPERATION, NamespaceType.CLIENT]:
310
326
  file_import.add_submodule_import(
311
327
  relative_path,
@@ -320,7 +336,33 @@ class GeneratedModelType(ModelType):
320
336
  ImportType.LOCAL,
321
337
  alias="_Model",
322
338
  )
323
- elif serialize_namespace_type == NamespaceType.TYPES_FILE or (
339
+ elif serialize_namespace_type == NamespaceType.TYPES_FILE:
340
+ # Don't import models that will be defined in this namespace's types.py —
341
+ # either as TypedDict classes (non-discriminated) or as Union aliases (discriminated bases).
342
+ # Only same-namespace non-json models are in the same types.py file.
343
+ same_namespace = relative_path == "."
344
+ will_be_in_types_file = self.base != "json" and same_namespace
345
+ if not will_be_in_types_file:
346
+ if same_namespace:
347
+ # json models from same namespace — import from .models (or .models._models for internal)
348
+ import_path = f".models.{self.code_model.models_filename}" if self.internal else ".models"
349
+ file_import.add_submodule_import(
350
+ import_path,
351
+ self.name,
352
+ ImportType.LOCAL,
353
+ typing_section=TypingSection.TYPING,
354
+ )
355
+ else:
356
+ # Cross-namespace model — import from sibling namespace's types module
357
+ file_import.add_submodule_import(
358
+ self.code_model.get_relative_import_path(
359
+ serialize_namespace, self.client_namespace, module_name="types"
360
+ ),
361
+ self.name,
362
+ ImportType.LOCAL,
363
+ typing_section=TypingSection.TYPING,
364
+ )
365
+ elif serialize_namespace_type == NamespaceType.UNIONS_FILE or (
324
366
  serialize_namespace_type == NamespaceType.MODEL and called_by_property
325
367
  ):
326
368
  file_import.add_submodule_import(
@@ -352,6 +394,33 @@ class MsrestModelType(GeneratedModelType):
352
394
  class DPGModelType(GeneratedModelType):
353
395
  base = "dpg"
354
396
 
397
+ @property
398
+ def is_used_in_operations_via_types(self) -> bool:
399
+ return self.is_typed_dict_only
400
+
401
+ def type_annotation(self, **kwargs: Any) -> str:
402
+ if self.is_typed_dict_only:
403
+ is_operation_file = kwargs.pop("is_operation_file", False)
404
+ skip_quote = kwargs.get("skip_quote", False)
405
+ serialize_namespace_type = kwargs.get("serialize_namespace_type")
406
+ # Within types.py, use bare name (no module prefix)
407
+ if serialize_namespace_type == NamespaceType.TYPES_FILE:
408
+ retval = self.name
409
+ else:
410
+ serialize_namespace = kwargs.get("serialize_namespace", self.code_model.namespace)
411
+ types_alias = self.code_model.get_unique_types_alias(serialize_namespace, self.client_namespace)
412
+ retval = f"{types_alias}.{self.name}"
413
+ return retval if is_operation_file or skip_quote else f'"{retval}"'
414
+ return super().type_annotation(**kwargs)
415
+
416
+ def docstring_type(self, **kwargs: Any) -> str:
417
+ if self.is_typed_dict_only:
418
+ client_namespace = self.client_namespace
419
+ if self.code_model.options.get("generation-subdir"):
420
+ client_namespace += f".{self.code_model.options['generation-subdir']}"
421
+ return f"~{client_namespace}.types.{self.name}"
422
+ return super().docstring_type(**kwargs)
423
+
355
424
  def serialization_type(self, **kwargs: Any) -> str:
356
425
  return (
357
426
  self.type_annotation(skip_quote=True, **kwargs)
@@ -364,7 +433,79 @@ class DPGModelType(GeneratedModelType):
364
433
  return "isinstance({}, " + f"_models.{self.name})"
365
434
 
366
435
  def imports(self, **kwargs: Any) -> FileImport:
436
+ if self.is_typed_dict_only:
437
+ file_import = FileImport(self.code_model)
438
+ serialize_namespace_type = kwargs.get("serialize_namespace_type")
439
+ serialize_namespace = kwargs.get("serialize_namespace", self.code_model.namespace)
440
+ relative_path = self.code_model.get_relative_import_path(serialize_namespace, self.client_namespace)
441
+ alias = self.code_model.get_unique_types_alias(serialize_namespace, self.client_namespace)
442
+ same_namespace = relative_path == "."
443
+ if serialize_namespace_type in [NamespaceType.OPERATION, NamespaceType.CLIENT]:
444
+ file_import.add_submodule_import(
445
+ relative_path,
446
+ "types",
447
+ ImportType.LOCAL,
448
+ alias=alias,
449
+ )
450
+ elif serialize_namespace_type == NamespaceType.TYPES_FILE and same_namespace:
451
+ pass # model is defined in this types.py — no import needed
452
+ elif serialize_namespace_type in [NamespaceType.TYPES_FILE, NamespaceType.UNIONS_FILE] or (
453
+ serialize_namespace_type == NamespaceType.MODEL and kwargs.get("called_by_property", False)
454
+ ):
455
+ file_import.add_submodule_import(
456
+ relative_path,
457
+ "types",
458
+ ImportType.LOCAL,
459
+ alias=alias,
460
+ typing_section=TypingSection.TYPING,
461
+ )
462
+ return file_import
367
463
  file_import = super().imports(**kwargs)
368
464
  if self.flattened_property:
369
465
  file_import.add_submodule_import("typing", "Any", ImportType.STDLIB)
370
466
  return file_import
467
+
468
+
469
+ class TypedDictModelType(DPGModelType):
470
+ base = "typeddict"
471
+
472
+ @property
473
+ def is_used_in_operations_via_types(self) -> bool:
474
+ return True
475
+
476
+ def type_annotation(self, **kwargs: Any) -> str:
477
+ is_operation_file = kwargs.pop("is_operation_file", False)
478
+ skip_quote = kwargs.get("skip_quote", False)
479
+ serialize_namespace_type = kwargs.get("serialize_namespace_type")
480
+ if serialize_namespace_type == NamespaceType.TYPES_FILE:
481
+ retval = self.name
482
+ else:
483
+ serialize_namespace = kwargs.get("serialize_namespace", self.code_model.namespace)
484
+ types_alias = self.code_model.get_unique_types_alias(serialize_namespace, self.client_namespace)
485
+ retval = f"{types_alias}.{self.name}"
486
+ return retval if is_operation_file or skip_quote else f'"{retval}"'
487
+
488
+ def docstring_type(self, **kwargs: Any) -> str:
489
+ client_namespace = self.client_namespace
490
+ if self.code_model.options.get("generation-subdir"):
491
+ client_namespace += f".{self.code_model.options['generation-subdir']}"
492
+ return f"~{client_namespace}.types.{self.name}"
493
+
494
+ @property
495
+ def instance_check_template(self) -> str:
496
+ return "isinstance({}, MutableMapping)"
497
+
498
+ def imports(self, **kwargs: Any) -> FileImport:
499
+ file_import = FileImport(self.code_model)
500
+ serialize_namespace_type = kwargs.get("serialize_namespace_type")
501
+ serialize_namespace = kwargs.get("serialize_namespace", self.code_model.namespace)
502
+ relative_path = self.code_model.get_relative_import_path(serialize_namespace, self.client_namespace)
503
+ alias = self.code_model.get_unique_types_alias(serialize_namespace, self.client_namespace)
504
+ if serialize_namespace_type in [NamespaceType.OPERATION, NamespaceType.CLIENT]:
505
+ file_import.add_submodule_import(
506
+ relative_path,
507
+ "types",
508
+ ImportType.LOCAL,
509
+ alias=alias,
510
+ )
511
+ return file_import
@@ -458,6 +458,7 @@ class OperationBase( # pylint: disable=too-many-public-methods,too-many-instanc
458
458
  r.type
459
459
  and not isinstance(r.type, BinaryIteratorType)
460
460
  and not xml_serializable(str(r.default_content_type))
461
+ and not (isinstance(r.type, ModelType) and r.type.is_typed_dict_only)
461
462
  for r in self.responses
462
463
  ):
463
464
  file_import.add_submodule_import(relative_path, "_deserialize", ImportType.LOCAL)
@@ -186,7 +186,11 @@ class PagingOperationBase(OperationBase[PagingResponseType]):
186
186
  serialize_namespace, module_name="_utils.model_base"
187
187
  )
188
188
  file_import.merge(self.item_type.imports(**kwargs))
189
- if self.default_error_deserialization(serialize_namespace) or self.need_deserialize:
189
+ need_deserialize_import = self.default_error_deserialization(serialize_namespace) or (
190
+ self.need_deserialize
191
+ and not (isinstance(self.item_type, ModelType) and self.item_type.is_typed_dict_only)
192
+ )
193
+ if need_deserialize_import:
190
194
  file_import.add_submodule_import(relative_path, "_deserialize", ImportType.LOCAL)
191
195
  if self.is_xml_paging:
192
196
  file_import.add_submodule_import("xml.etree", "ElementTree", ImportType.STDLIB, alias="ET")
@@ -180,7 +180,7 @@ class _ParameterBase(BaseModel, abc.ABC): # pylint: disable=too-many-instance-a
180
180
  if isinstance(self.type, CombinedType) and self.type.name:
181
181
  file_import.add_submodule_import(
182
182
  self.code_model.get_relative_import_path(serialize_namespace),
183
- "_types",
183
+ "_unions",
184
184
  ImportType.LOCAL,
185
185
  TypingSection.TYPING,
186
186
  )
@@ -338,7 +338,7 @@ class Parameter(_ParameterBase):
338
338
  ) -> ParameterMethodLocation:
339
339
  if not self.in_method_signature:
340
340
  raise ValueError(f"Parameter '{self.client_name}' is not in the method.")
341
- if self.code_model.options["models-mode"] == "dpg" and self.in_flattened_body:
341
+ if self.code_model.options["models-mode"] in ("dpg", "typeddict") and self.in_flattened_body:
342
342
  return ParameterMethodLocation.KEYWORD_ONLY
343
343
  if self.grouper:
344
344
  return ParameterMethodLocation.POSITIONAL
@@ -9,6 +9,7 @@ from typing import Any, Optional, Union, TYPE_CHECKING
9
9
 
10
10
  from .base import BaseType
11
11
  from .imports import FileImport, ImportType, TypingSection
12
+ from .utils import NamespaceType
12
13
 
13
14
  if TYPE_CHECKING:
14
15
  from .code_model import CodeModel
@@ -271,6 +272,8 @@ class DecimalType(NumberType):
271
272
  return "~" + self.type_annotation()
272
273
 
273
274
  def type_annotation(self, **kwargs: Any) -> str:
275
+ if kwargs.get("serialize_namespace_type") == NamespaceType.TYPES_FILE:
276
+ return "float"
274
277
  return "decimal.Decimal"
275
278
 
276
279
  def docstring_text(self, **kwargs: Any) -> str:
@@ -281,7 +284,8 @@ class DecimalType(NumberType):
281
284
 
282
285
  def imports(self, **kwargs: Any) -> FileImport:
283
286
  file_import = FileImport(self.code_model)
284
- file_import.add_import("decimal", ImportType.STDLIB)
287
+ if kwargs.get("serialize_namespace_type") != NamespaceType.TYPES_FILE:
288
+ file_import.add_import("decimal", ImportType.STDLIB)
285
289
  return file_import
286
290
 
287
291
  @property
@@ -357,6 +361,8 @@ class DatetimeType(PrimitiveType):
357
361
  return "~" + self.type_annotation()
358
362
 
359
363
  def type_annotation(self, **kwargs: Any) -> str:
364
+ if kwargs.get("serialize_namespace_type") == NamespaceType.TYPES_FILE:
365
+ return "str"
360
366
  return "datetime.datetime"
361
367
 
362
368
  def docstring_text(self, **kwargs: Any) -> str:
@@ -370,7 +376,8 @@ class DatetimeType(PrimitiveType):
370
376
 
371
377
  def imports(self, **kwargs: Any) -> FileImport:
372
378
  file_import = FileImport(self.code_model)
373
- file_import.add_import("datetime", ImportType.STDLIB)
379
+ if kwargs.get("serialize_namespace_type") != NamespaceType.TYPES_FILE:
380
+ file_import.add_import("datetime", ImportType.STDLIB)
374
381
  return file_import
375
382
 
376
383
  @property
@@ -399,6 +406,8 @@ class TimeType(PrimitiveType):
399
406
  return "~" + self.type_annotation()
400
407
 
401
408
  def type_annotation(self, **kwargs: Any) -> str:
409
+ if kwargs.get("serialize_namespace_type") == NamespaceType.TYPES_FILE:
410
+ return "str"
402
411
  return "datetime.time"
403
412
 
404
413
  def docstring_text(self, **kwargs: Any) -> str:
@@ -412,7 +421,8 @@ class TimeType(PrimitiveType):
412
421
 
413
422
  def imports(self, **kwargs: Any) -> FileImport:
414
423
  file_import = FileImport(self.code_model)
415
- file_import.add_import("datetime", ImportType.STDLIB)
424
+ if kwargs.get("serialize_namespace_type") != NamespaceType.TYPES_FILE:
425
+ file_import.add_import("datetime", ImportType.STDLIB)
416
426
  return file_import
417
427
 
418
428
  @property
@@ -445,6 +455,8 @@ class UnixTimeType(PrimitiveType):
445
455
  return "~" + self.type_annotation()
446
456
 
447
457
  def type_annotation(self, **kwargs: Any) -> str:
458
+ if kwargs.get("serialize_namespace_type") == NamespaceType.TYPES_FILE:
459
+ return "int"
448
460
  return "datetime.datetime"
449
461
 
450
462
  def docstring_text(self, **kwargs: Any) -> str:
@@ -458,7 +470,8 @@ class UnixTimeType(PrimitiveType):
458
470
 
459
471
  def imports(self, **kwargs: Any) -> FileImport:
460
472
  file_import = FileImport(self.code_model)
461
- file_import.add_import("datetime", ImportType.STDLIB)
473
+ if kwargs.get("serialize_namespace_type") != NamespaceType.TYPES_FILE:
474
+ file_import.add_import("datetime", ImportType.STDLIB)
462
475
  return file_import
463
476
 
464
477
  @property
@@ -487,6 +500,8 @@ class DateType(PrimitiveType):
487
500
  return "~" + self.type_annotation()
488
501
 
489
502
  def type_annotation(self, **kwargs: Any) -> str:
503
+ if kwargs.get("serialize_namespace_type") == NamespaceType.TYPES_FILE:
504
+ return "str"
490
505
  return "datetime.date"
491
506
 
492
507
  def docstring_text(self, **kwargs: Any) -> str:
@@ -500,7 +515,8 @@ class DateType(PrimitiveType):
500
515
 
501
516
  def imports(self, **kwargs: Any) -> FileImport:
502
517
  file_import = FileImport(self.code_model)
503
- file_import.add_import("datetime", ImportType.STDLIB)
518
+ if kwargs.get("serialize_namespace_type") != NamespaceType.TYPES_FILE:
519
+ file_import.add_import("datetime", ImportType.STDLIB)
504
520
  return file_import
505
521
 
506
522
  @property
@@ -543,6 +559,8 @@ class DurationType(PrimitiveType):
543
559
  return "~" + self.type_annotation()
544
560
 
545
561
  def type_annotation(self, **kwargs: Any) -> str:
562
+ if kwargs.get("serialize_namespace_type") == NamespaceType.TYPES_FILE:
563
+ return "str"
546
564
  return "datetime.timedelta"
547
565
 
548
566
  def docstring_text(self, **kwargs: Any) -> str:
@@ -556,7 +574,8 @@ class DurationType(PrimitiveType):
556
574
 
557
575
  def imports(self, **kwargs: Any) -> FileImport:
558
576
  file_import = FileImport(self.code_model)
559
- file_import.add_import("datetime", ImportType.STDLIB)
577
+ if kwargs.get("serialize_namespace_type") != NamespaceType.TYPES_FILE:
578
+ file_import.add_import("datetime", ImportType.STDLIB)
560
579
  return file_import
561
580
 
562
581
  @property
@@ -590,6 +609,11 @@ class ByteArraySchema(PrimitiveType):
590
609
  def docstring_type(self, **kwargs: Any) -> str:
591
610
  return "bytes"
592
611
 
612
+ def type_annotation(self, **kwargs: Any) -> str:
613
+ if kwargs.get("serialize_namespace_type") == NamespaceType.TYPES_FILE:
614
+ return "str"
615
+ return "bytes"
616
+
593
617
  def get_declaration(self, value: str) -> str:
594
618
  return f'bytes("{value}", encoding="utf-8")'
595
619
 
@@ -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()
@@ -7,23 +7,24 @@ from typing import TypeVar
7
7
 
8
8
  from enum import Enum
9
9
 
10
+ from ...utils import description_ends_with_code_block
11
+
10
12
  T = TypeVar("T")
11
13
  OrderedSet = dict[T, None]
12
14
 
13
15
 
14
- CODE_BLOCK_MARKER = ".. code-block::"
15
-
16
-
17
16
  def add_to_description(description: str, entry: str) -> str:
18
17
  if not description:
19
18
  return entry
20
- # When the description contains a code block, the entry (e.g. "Required.") must be
21
- # inserted into the prose *before* the code block. Appending it after the code block
22
- # (e.g. "]. Required.") leaves it dangling at the end of the rendered block and breaks
23
- # Sphinx rendering.
24
- if CODE_BLOCK_MARKER in description:
25
- prose, _, code_block = description.partition(CODE_BLOCK_MARKER)
26
- return f"{prose.rstrip()} {entry}\n\n{CODE_BLOCK_MARKER}{code_block}"
19
+ if not entry:
20
+ return description
21
+ # When the description ends with a code block, prepend the entry (e.g. "Required.") so
22
+ # it appears before the prose instead of after the block. Appending inline (e.g.
23
+ # "]. Required.") would land it inside the rendered literal block and break Sphinx, and a
24
+ # trailing paragraph reads awkwardly. The code block stays at the very end where it renders
25
+ # cleanly. (Code-block descriptions only ever carry a single annotation in practice.)
26
+ if description_ends_with_code_block(description):
27
+ return f"{entry} {description.lstrip()}"
27
28
  return f"{description} {entry}"
28
29
 
29
30
 
@@ -40,6 +41,7 @@ class NamespaceType(str, Enum):
40
41
  OPERATION = "operation"
41
42
  CLIENT = "client"
42
43
  TYPES_FILE = "types_file"
44
+ UNIONS_FILE = "unions_file"
43
45
 
44
46
 
45
47
  LOCALS_LENGTH_LIMIT = 25
@@ -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