@typespec/http-client-python 0.35.0 → 0.36.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 (103) hide show
  1. package/README.md +6 -0
  2. package/dist/emitter/code-model.d.ts +1 -1
  3. package/dist/emitter/code-model.d.ts.map +1 -1
  4. package/dist/emitter/code-model.js.map +1 -1
  5. package/dist/emitter/emitter.d.ts +2 -2
  6. package/dist/emitter/emitter.d.ts.map +1 -1
  7. package/dist/emitter/emitter.js +23 -2
  8. package/dist/emitter/emitter.js.map +1 -1
  9. package/dist/emitter/external-process.d.ts +1 -1
  10. package/dist/emitter/external-process.d.ts.map +1 -1
  11. package/dist/emitter/external-process.js.map +1 -1
  12. package/dist/emitter/http.d.ts +2 -2
  13. package/dist/emitter/http.d.ts.map +1 -1
  14. package/dist/emitter/http.js +1 -1
  15. package/dist/emitter/http.js.map +1 -1
  16. package/dist/emitter/index.d.ts +2 -1
  17. package/dist/emitter/index.d.ts.map +1 -1
  18. package/dist/emitter/index.js.map +1 -1
  19. package/dist/emitter/lib.d.ts +3 -2
  20. package/dist/emitter/lib.d.ts.map +1 -1
  21. package/dist/emitter/lib.js +6 -1
  22. package/dist/emitter/lib.js.map +1 -1
  23. package/dist/emitter/node-runner.browser.d.ts +1 -1
  24. package/dist/emitter/node-runner.browser.d.ts.map +1 -1
  25. package/dist/emitter/node-runner.browser.js.map +1 -1
  26. package/dist/emitter/node-runner.d.ts +3 -3
  27. package/dist/emitter/node-runner.d.ts.map +1 -1
  28. package/dist/emitter/node-runner.js.map +1 -1
  29. package/dist/emitter/system-requirements.d.ts.map +1 -1
  30. package/dist/emitter/system-requirements.js.map +1 -1
  31. package/dist/emitter/types.d.ts +4 -4
  32. package/dist/emitter/types.d.ts.map +1 -1
  33. package/dist/emitter/types.js +15 -3
  34. package/dist/emitter/types.js.map +1 -1
  35. package/dist/emitter/utils.d.ts +2 -2
  36. package/dist/emitter/utils.d.ts.map +1 -1
  37. package/dist/emitter/utils.js +1 -1
  38. package/dist/emitter/utils.js.map +1 -1
  39. package/emitter/src/code-model.ts +4 -2
  40. package/emitter/src/emitter.ts +30 -6
  41. package/emitter/src/external-process.ts +2 -1
  42. package/emitter/src/http.ts +5 -5
  43. package/emitter/src/index.ts +2 -1
  44. package/emitter/src/lib.ts +11 -6
  45. package/emitter/src/node-runner.browser.ts +2 -1
  46. package/emitter/src/node-runner.ts +6 -3
  47. package/emitter/src/system-requirements.ts +2 -1
  48. package/emitter/src/types.ts +23 -8
  49. package/emitter/src/utils.ts +5 -4
  50. package/emitter/temp/tsconfig.tsbuildinfo +1 -1
  51. package/emitter/test/browser-pyodide.test.ts +25 -0
  52. package/emitter/test/types.test.ts +72 -0
  53. package/eng/scripts/ci/dev_requirements.txt +1 -1
  54. package/eng/scripts/ci/regenerate-common.ts +29 -100
  55. package/eng/scripts/ci/regenerate.ts +3 -4
  56. package/eng/scripts/ci/run-tests.ts +2 -1
  57. package/eng/scripts/setup/__pycache__/package_manager.cpython-311.pyc +0 -0
  58. package/eng/scripts/setup/__pycache__/venvtools.cpython-311.pyc +0 -0
  59. package/eng/scripts/setup/system-requirements.ts +2 -1
  60. package/generator/build/lib/pygen/__init__.py +24 -5
  61. package/generator/build/lib/pygen/codegen/models/__init__.py +3 -1
  62. package/generator/build/lib/pygen/codegen/models/code_model.py +7 -1
  63. package/generator/build/lib/pygen/codegen/models/enum_type.py +7 -7
  64. package/generator/build/lib/pygen/codegen/models/list_type.py +1 -0
  65. package/generator/build/lib/pygen/codegen/models/model_type.py +1 -3
  66. package/generator/build/lib/pygen/codegen/models/operation.py +7 -2
  67. package/generator/build/lib/pygen/codegen/models/parameter.py +3 -1
  68. package/generator/build/lib/pygen/codegen/models/utils.py +14 -0
  69. package/generator/build/lib/pygen/codegen/serializers/__init__.py +6 -6
  70. package/generator/build/lib/pygen/codegen/serializers/builder_serializer.py +28 -14
  71. package/generator/build/lib/pygen/codegen/serializers/client_serializer.py +13 -6
  72. package/generator/build/lib/pygen/codegen/serializers/general_serializer.py +21 -1
  73. package/generator/build/lib/pygen/codegen/serializers/model_serializer.py +9 -3
  74. package/generator/build/lib/pygen/codegen/serializers/parameter_serializer.py +17 -3
  75. package/generator/build/lib/pygen/codegen/serializers/types_serializer.py +5 -12
  76. package/generator/build/lib/pygen/codegen/templates/validation.py.jinja2 +1 -1
  77. package/generator/build/lib/pygen/preprocess/__init__.py +68 -17
  78. package/generator/build/lib/pygen/utils.py +15 -0
  79. package/generator/component-detection-pip-report.json +7 -7
  80. package/generator/dist/pygen-0.1.0-py3-none-any.whl +0 -0
  81. package/generator/pygen/__init__.py +24 -5
  82. package/generator/pygen/codegen/models/__init__.py +3 -1
  83. package/generator/pygen/codegen/models/code_model.py +7 -1
  84. package/generator/pygen/codegen/models/enum_type.py +7 -7
  85. package/generator/pygen/codegen/models/list_type.py +1 -0
  86. package/generator/pygen/codegen/models/model_type.py +1 -3
  87. package/generator/pygen/codegen/models/operation.py +7 -2
  88. package/generator/pygen/codegen/models/parameter.py +3 -1
  89. package/generator/pygen/codegen/models/utils.py +14 -0
  90. package/generator/pygen/codegen/serializers/__init__.py +6 -6
  91. package/generator/pygen/codegen/serializers/builder_serializer.py +28 -14
  92. package/generator/pygen/codegen/serializers/client_serializer.py +13 -6
  93. package/generator/pygen/codegen/serializers/general_serializer.py +21 -1
  94. package/generator/pygen/codegen/serializers/model_serializer.py +9 -3
  95. package/generator/pygen/codegen/serializers/parameter_serializer.py +17 -3
  96. package/generator/pygen/codegen/serializers/types_serializer.py +5 -12
  97. package/generator/pygen/codegen/templates/validation.py.jinja2 +1 -1
  98. package/generator/pygen/preprocess/__init__.py +68 -17
  99. package/generator/pygen/utils.py +15 -0
  100. package/generator/pygen.egg-info/PKG-INFO +1 -1
  101. package/generator/pygen.egg-info/requires.txt +1 -1
  102. package/generator/setup.py +1 -1
  103. package/package.json +37 -35
@@ -32,7 +32,7 @@ from ..models import (
32
32
  ParameterListType,
33
33
  ByteArraySchema,
34
34
  )
35
- from ..models.utils import NamespaceType
35
+ from ..models.utils import NamespaceType, escape_sphinx_field_name
36
36
  from .parameter_serializer import ParameterSerializer, PopKwargType, check_body_optional
37
37
  from ..models.parameter_list import ParameterType
38
38
  from . import utils
@@ -311,16 +311,15 @@ class _BuilderBaseSerializer(Generic[BuilderType]):
311
311
  or param.method_location == ParameterMethodLocation.KWARG
312
312
  ):
313
313
  continue
314
+ escaped_name = escape_sphinx_field_name(param.client_name)
314
315
  description_list.extend(
315
- f":{param.description_keyword} {param.client_name}: {param.description}".replace("\n", "\n ").split(
316
- "\n"
317
- )
316
+ f":{param.description_keyword} {escaped_name}: {param.description}".replace("\n", "\n ").split("\n")
318
317
  )
319
318
  docstring_type = param.docstring_type(
320
319
  async_mode=self.async_mode,
321
320
  serialize_namespace=self.serialize_namespace,
322
321
  )
323
- description_list.append(f":{param.docstring_type_keyword} {param.client_name}: {docstring_type}")
322
+ description_list.append(f":{param.docstring_type_keyword} {escaped_name}: {docstring_type}")
324
323
  return description_list
325
324
 
326
325
  def param_description_and_response_docstring(self, builder: BuilderType) -> list[str]:
@@ -462,6 +461,7 @@ class RequestBuilderSerializer(_BuilderBaseSerializer[RequestBuilderType]):
462
461
  check_kwarg_dict=True,
463
462
  pop_headers_kwarg=(PopKwargType.CASE_INSENSITIVE if bool(builder.parameters.headers) else PopKwargType.NO),
464
463
  pop_params_kwarg=(PopKwargType.CASE_INSENSITIVE if bool(builder.parameters.query) else PopKwargType.NO),
464
+ is_body_optional=builder.parameters.has_body and builder.parameters.body_parameter.optional,
465
465
  )
466
466
 
467
467
  @staticmethod
@@ -491,6 +491,7 @@ class RequestBuilderSerializer(_BuilderBaseSerializer[RequestBuilderType]):
491
491
  for h in builder.parameters.headers
492
492
  if not builder.has_form_data_body or h.wire_name.lower() != "content-type"
493
493
  ]
494
+ is_body_optional = builder.parameters.has_body and builder.parameters.body_parameter.optional
494
495
  retval = ["# Construct headers"] if headers else []
495
496
  for header in headers:
496
497
  retval.extend(
@@ -499,6 +500,7 @@ class RequestBuilderSerializer(_BuilderBaseSerializer[RequestBuilderType]):
499
500
  "headers",
500
501
  self.serializer_name,
501
502
  self.code_model.is_legacy,
503
+ is_body_optional=is_body_optional,
502
504
  )
503
505
  )
504
506
  return retval
@@ -712,7 +714,9 @@ class _OperationSerializer(_BuilderBaseSerializer[OperationType]):
712
714
  send_xml = builder.parameters.body_parameter.type.is_xml
713
715
  xml_serialization_ctxt = body_param.type.xml_serialization_ctxt if send_xml else None
714
716
  ser_ctxt_name = "serialization_ctxt"
715
- if xml_serialization_ctxt and self.code_model.options["models-mode"]:
717
+ if xml_serialization_ctxt and (
718
+ self.code_model.options["models-mode"] or self.code_model.generate_typeddict_only
719
+ ):
716
720
  retval.append(f'{ser_ctxt_name} = {{"xml": {{{xml_serialization_ctxt}}}}}')
717
721
  if self.code_model.options["models-mode"] == "msrest":
718
722
  is_xml_cmd = _xml_config(send_xml, builder.parameters.body_parameter.content_types)
@@ -722,7 +726,7 @@ class _OperationSerializer(_BuilderBaseSerializer[OperationType]):
722
726
  f"_{body_kwarg_name} = self._serialize.body({body_param.client_name}, "
723
727
  f"'{serialization_type}'{is_xml_cmd}{serialization_ctxt_cmd})"
724
728
  )
725
- elif self.code_model.options["models-mode"] == "typeddict":
729
+ elif self.code_model.generate_typeddict_only:
726
730
  # TypedDict-only models are plain dicts — no serialization needed
727
731
  create_body_call = f"_{body_kwarg_name} = {body_param.client_name}"
728
732
  elif self.code_model.options["models-mode"] == "dpg":
@@ -841,7 +845,9 @@ class _OperationSerializer(_BuilderBaseSerializer[OperationType]):
841
845
  overload.request_builder.parameters.body_parameter.client_name for overload in builder.overloads
842
846
  ]
843
847
  all_dpg_model_overloads = False
844
- if self.code_model.options["models-mode"] in ("dpg", "typeddict") and builder.overloads:
848
+ if (
849
+ self.code_model.options["models-mode"] == "dpg" or self.code_model.generate_typeddict_only
850
+ ) and builder.overloads:
845
851
  all_dpg_model_overloads = all(
846
852
  _is_dpg_or_typeddict_body(o.parameters.body_parameter) for o in builder.overloads
847
853
  )
@@ -1128,8 +1134,8 @@ class _OperationSerializer(_BuilderBaseSerializer[OperationType]):
1128
1134
  retval.extend([f" {l}" for l in response_read])
1129
1135
  retval.append(" map_error(status_code=response.status_code, response=response, error_map=error_map)")
1130
1136
  error_model = ""
1131
- if ( # pylint: disable=too-many-nested-blocks
1132
- builder.non_default_errors and self.code_model.options["models-mode"]
1137
+ if builder.non_default_errors and ( # pylint: disable=too-many-nested-blocks
1138
+ self.code_model.options["models-mode"] or self.code_model.generate_typeddict_only
1133
1139
  ):
1134
1140
  error_model = ", model=error"
1135
1141
  condition = "if"
@@ -1211,7 +1217,9 @@ class _OperationSerializer(_BuilderBaseSerializer[OperationType]):
1211
1217
  condition = "elif"
1212
1218
  # default error handling
1213
1219
  default_error_deserialization = builder.default_error_deserialization(self.serialize_namespace)
1214
- if default_error_deserialization and self.code_model.options["models-mode"]:
1220
+ if default_error_deserialization and (
1221
+ self.code_model.options["models-mode"] or self.code_model.generate_typeddict_only
1222
+ ):
1215
1223
  error_model = ", model=error"
1216
1224
  indent = " " if builder.non_default_errors else " "
1217
1225
  if builder.non_default_errors:
@@ -1290,7 +1298,11 @@ class _OperationSerializer(_BuilderBaseSerializer[OperationType]):
1290
1298
  else:
1291
1299
  retval.extend(self.response_headers_and_deserialization(builder, builder.responses[0]))
1292
1300
  retval.append("")
1293
- if builder.has_optional_return_type or self.code_model.options["models-mode"]:
1301
+ if (
1302
+ builder.has_optional_return_type
1303
+ or self.code_model.options["models-mode"]
1304
+ or self.code_model.generate_typeddict_only
1305
+ ):
1294
1306
  deserialized = "deserialized"
1295
1307
  else:
1296
1308
  deserialized = f"cast({builder.response_type_annotation(async_mode=self.async_mode)}, deserialized)"
@@ -1326,7 +1338,9 @@ class _OperationSerializer(_BuilderBaseSerializer[OperationType]):
1326
1338
 
1327
1339
  def error_map(self, builder: OperationType) -> list[str]:
1328
1340
  retval = ["error_map: MutableMapping = {"]
1329
- if builder.non_default_errors and self.code_model.options["models-mode"]:
1341
+ if builder.non_default_errors and (
1342
+ self.code_model.options["models-mode"] or self.code_model.generate_typeddict_only
1343
+ ):
1330
1344
  # TODO: we should decide whether to add the build-in error map when there is a customized default error type
1331
1345
  if self._need_specific_error_map(401, builder):
1332
1346
  retval.append(" 401: ClientAuthenticationError,")
@@ -1690,7 +1704,7 @@ class _LROOperationSerializer(_OperationSerializer[LROOperationType]):
1690
1704
  if builder.lro_response.headers:
1691
1705
  retval.append(" response_headers = {}")
1692
1706
  if (
1693
- not self.code_model.options["models-mode"]
1707
+ (not self.code_model.options["models-mode"] and not self.code_model.generate_typeddict_only)
1694
1708
  or self.code_model.options["models-mode"] == "dpg"
1695
1709
  or builder.lro_response.headers
1696
1710
  ):
@@ -8,6 +8,7 @@ from typing import cast
8
8
  from . import utils
9
9
  from ..models import Client, ParameterMethodLocation, Parameter, ParameterLocation
10
10
  from .parameter_serializer import ParameterSerializer, PopKwargType
11
+ from ..models.utils import escape_sphinx_field_name
11
12
  from ...utils import build_policies
12
13
 
13
14
 
@@ -61,13 +62,16 @@ class ClientSerializer:
61
62
  retval: list[str] = []
62
63
  operations_folder = ".aio.operations." if async_mode else ".operations."
63
64
  for og in [og for og in self.client.operation_groups if not og.is_mixin]:
64
- retval.append(f":ivar {og.property_name}: {og.class_name} operations")
65
+ retval.append(f":ivar {escape_sphinx_field_name(og.property_name)}: {og.class_name} operations")
65
66
  property_type = f"{self.client.code_model.namespace}{operations_folder}{og.class_name}"
66
- retval.append(f":vartype {og.property_name}: {property_type}")
67
+ retval.append(f":vartype {escape_sphinx_field_name(og.property_name)}: {property_type}")
67
68
  for param in self.client.parameters.method:
68
- retval.append(f":{param.description_keyword} {param.client_name}: {param.description}")
69
69
  retval.append(
70
- f":{param.docstring_type_keyword} {param.client_name}: {param.docstring_type(async_mode=async_mode)}"
70
+ f":{param.description_keyword} {escape_sphinx_field_name(param.client_name)}: {param.description}"
71
+ )
72
+ retval.append(
73
+ f":{param.docstring_type_keyword} {escape_sphinx_field_name(param.client_name)}: "
74
+ f"{param.docstring_type(async_mode=async_mode)}"
71
75
  )
72
76
  if self.client.has_public_lro_operations:
73
77
  retval.append(
@@ -315,7 +319,10 @@ class ConfigSerializer:
315
319
  def property_descriptions(self, async_mode: bool) -> list[str]:
316
320
  retval: list[str] = []
317
321
  for p in self.client.config.parameters.method:
318
- retval.append(f":{p.description_keyword} {p.client_name}: {p.description}")
319
- retval.append(f":{p.docstring_type_keyword} {p.client_name}: {p.docstring_type(async_mode=async_mode)}")
322
+ retval.append(f":{p.description_keyword} {escape_sphinx_field_name(p.client_name)}: {p.description}")
323
+ retval.append(
324
+ f":{p.docstring_type_keyword} {escape_sphinx_field_name(p.client_name)}: "
325
+ f"{p.docstring_type(async_mode=async_mode)}"
326
+ )
320
327
  retval.append('"""')
321
328
  return retval
@@ -320,7 +320,27 @@ class GeneralSerializer(BaseSerializer):
320
320
 
321
321
  def serialize_validation_file(self) -> str:
322
322
  template = self.env.get_template("validation.py.jinja2")
323
- return template.render(code_model=self.code_model)
323
+ return template.render(
324
+ code_model=self.code_model,
325
+ client_api_version_name=self._api_version_config_attr_name(),
326
+ )
327
+
328
+ def _api_version_config_attr_name(self) -> str:
329
+ """Name of the config attribute that stores the API version.
330
+
331
+ The attribute name is derived from the API-version parameter's ``client_name``
332
+ (e.g. ``apiVersion`` -> ``api_version``, or Storage's ``version`` -> ``version``).
333
+ ``_validation.py`` holds a single shared decorator for the whole package, so when
334
+ every client agrees on the name we bake it in directly; otherwise we fall back to
335
+ the conventional ``api_version``.
336
+ """
337
+ names = {
338
+ parameter.client_name
339
+ for client in self.code_model.clients
340
+ for parameter in client.config.parameters.parameters
341
+ if parameter.is_api_version
342
+ }
343
+ return next(iter(names)) if len(names) == 1 else "api_version"
324
344
 
325
345
  def serialize_cross_language_definition_file(self) -> str:
326
346
  cross_langauge_def_dict = {
@@ -23,7 +23,7 @@ from ..models.primitive_types import (
23
23
  )
24
24
  from .import_serializer import FileImportSerializer
25
25
  from .base_serializer import BaseSerializer
26
- from ..models.utils import NamespaceType, add_to_pylint_disable
26
+ from ..models.utils import NamespaceType, add_to_pylint_disable, escape_sphinx_field_name
27
27
 
28
28
 
29
29
  def _get_xml_deserializer_name(prop: Property) -> Optional[str]: # pylint: disable=too-many-return-statements
@@ -77,7 +77,13 @@ def _documentation_string(
77
77
  prop: Property, description_keyword: str, docstring_type_keyword: str, **kwargs: Any
78
78
  ) -> list[str]:
79
79
  retval: list[str] = []
80
- sphinx_prefix = f":{description_keyword} {prop.client_name}:"
80
+ doc_name = (
81
+ prop.wire_name if kwargs.get("serialize_namespace_type") == NamespaceType.TYPES_FILE else prop.client_name
82
+ )
83
+ # Escape names that Sphinx would otherwise misinterpret in the info field target
84
+ # (e.g. a wire name with a leading "@" such as "@search.facets").
85
+ doc_name = escape_sphinx_field_name(doc_name)
86
+ sphinx_prefix = f":{description_keyword} {doc_name}:"
81
87
  description = prop.description(is_operation_file=False).replace("\\", "\\\\")
82
88
  retval.append(f"{sphinx_prefix} {description}" if description else sphinx_prefix)
83
89
  # In the types file, use type_annotation (the serialized form) for docstrings
@@ -85,7 +91,7 @@ def _documentation_string(
85
91
  doc_type = prop.type.type_annotation(**kwargs)
86
92
  else:
87
93
  doc_type = prop.type.docstring_type(**kwargs)
88
- retval.append(f":{docstring_type_keyword} {prop.client_name}: {doc_type}")
94
+ retval.append(f":{docstring_type_keyword} {doc_name}: {doc_type}")
89
95
  return retval
90
96
 
91
97
 
@@ -139,6 +139,8 @@ class ParameterSerializer:
139
139
  kwarg_name: str,
140
140
  serializer_name: str,
141
141
  is_legacy: bool,
142
+ *,
143
+ is_body_optional: bool = False,
142
144
  ) -> list[str]:
143
145
  if (
144
146
  not is_legacy
@@ -166,7 +168,11 @@ class ParameterSerializer:
166
168
  param.wire_name,
167
169
  self.serialize_parameter(param, serializer_name),
168
170
  )
169
- if not param.optional and (param.in_method_signature or param.constant):
171
+ # When the body is optional, the content-type may legitimately be ``None`` (no body
172
+ # was sent). In that case the header must be omitted instead of serialized, otherwise
173
+ # ``_SERIALIZER.header`` raises ``ValueError: No value for given attribute``.
174
+ is_content_type_optional = getattr(param, "is_content_type", False) and is_body_optional
175
+ if not param.optional and not is_content_type_optional and (param.in_method_signature or param.constant):
170
176
  retval = [set_parameter]
171
177
  else:
172
178
  retval = [
@@ -184,6 +190,7 @@ class ParameterSerializer:
184
190
  check_client_input: bool = False,
185
191
  *,
186
192
  body_parameter: Optional[BodyParameterType] = None,
193
+ is_body_optional: Optional[bool] = None,
187
194
  ) -> list[str]:
188
195
  retval = []
189
196
 
@@ -195,7 +202,8 @@ class ParameterSerializer:
195
202
 
196
203
  append_pop_kwarg("headers", pop_headers_kwarg)
197
204
  append_pop_kwarg("params", pop_params_kwarg)
198
- is_body_optional = check_body_optional(body_parameter)
205
+ if is_body_optional is None:
206
+ is_body_optional = check_body_optional(body_parameter)
199
207
  if pop_headers_kwarg != PopKwargType.NO or pop_params_kwarg != PopKwargType.NO:
200
208
  retval.append("")
201
209
  for kwarg in parameters:
@@ -206,7 +214,13 @@ class ParameterSerializer:
206
214
  if is_content_type_optional and not type_annotation.startswith("Optional[")
207
215
  else type_annotation
208
216
  )
209
- if kwarg.client_default_value is not None or kwarg.optional or kwarg.constant or kwarg.is_api_version:
217
+ if (
218
+ kwarg.client_default_value is not None
219
+ or kwarg.optional
220
+ or kwarg.constant
221
+ or kwarg.is_api_version
222
+ or is_content_type_optional
223
+ ):
210
224
  if check_client_input and kwarg.check_client_input:
211
225
  default_value = f"self._config.{kwarg.client_name}"
212
226
  elif kwarg.constant:
@@ -103,21 +103,16 @@ class TypesSerializer(BaseSerializer):
103
103
 
104
104
  TypedDicts (and discriminated-base union aliases) in ``types.py`` describe request-body
105
105
  (*input*) shapes. Output-only models already render as classes in ``models/`` and are
106
- referenced via ``_models.*``, so a response-only model that nothing input references would be
107
- dead code. A model seeds the input surface when it is:
108
-
109
- * a typeddict copy (``base == "typeddict"``) — these only exist as input body overloads
110
- (their ``usage`` may carry ``Spread``/``Json`` rather than ``Input``), or
111
- * ``is_typed_dict_only`` — includes every model in full ``typeddict`` mode (responses too),
112
- and input-only anonymous bodies, or
113
- * used as input (``is_usage_input``) — e.g. a model shared between request and response.
106
+ referenced via ``_models.*``, so a model that no operation references through ``types.py``
107
+ would be dead code. TypedDict copies and models in full TypedDict mode volunteer through
108
+ ``is_used_in_operations_via_types``.
114
109
 
115
110
  The full set rendered in types.py is the transitive closure of these seeds over base
116
111
  classes, discriminated subtypes and property types (see :attr:`_types_file_model_names`), so
117
112
  an output-only model that *is* referenced by an input model (e.g. ARM ``SystemData`` on
118
113
  ``Resource``) is still rendered.
119
114
  """
120
- return m.base == "typeddict" or m.is_typed_dict_only or m.is_usage_input
115
+ return m.is_used_in_operations_via_types
121
116
 
122
117
  @staticmethod
123
118
  def _iter_referenced_models(base_type: Any):
@@ -189,9 +184,7 @@ class TypesSerializer(BaseSerializer):
189
184
  property — are kept, ensuring no forward reference is left undefined.
190
185
  """
191
186
  needed = self._types_file_model_names
192
- candidates = [
193
- m for m in self._models if m.base != "json" and not m.discriminated_subtypes and m.name in needed
194
- ]
187
+ candidates = [m for m in self._models if m.base != "json" and not m.discriminated_subtypes and m.name in needed]
195
188
  seen_names: dict[str, "ModelType"] = {}
196
189
  result: list["ModelType"] = []
197
190
  for m in candidates:
@@ -29,7 +29,7 @@ def api_version_validation(**kwargs):
29
29
  try:
30
30
  # this assumes the client has an _api_version attribute
31
31
  client = args[0]
32
- client_api_version = client._config.api_version # pylint: disable=protected-access
32
+ client_api_version = client._config.{{ client_api_version_name }} # pylint: disable=protected-access
33
33
  except AttributeError:
34
34
  return func(*args, **kwargs)
35
35
 
@@ -81,8 +81,17 @@ def add_overload(yaml_data: dict[str, Any], body_type: dict[str, Any], for_flatt
81
81
  return overload
82
82
 
83
83
 
84
- def add_overloads_for_body_param(yaml_data: dict[str, Any]) -> None:
85
- """If we added a body parameter type, add overloads for that type"""
84
+ def add_overloads_for_body_param(yaml_data: dict[str, Any], skip_single_body_json: bool = False) -> None:
85
+ """If we added a body parameter type, add overloads for that type.
86
+
87
+ ``skip_single_body_json`` is the authoritative signal, computed by
88
+ ``add_body_param_type``, for whether a TypedDict-style overload was inserted
89
+ to replace the single-body raw-JSON overload on the spread (``base: json``)
90
+ path. It is True for both models-mode: dpg (generate-typeddict on) and
91
+ the TypedDict-only mode (models-mode: none with generate-typeddict on), and
92
+ False when TypedDict generation is disabled (in which case the single-body
93
+ raw-JSON overload is kept, matching pre-TypedDict behavior).
94
+ """
86
95
  body_parameter = yaml_data["bodyParameter"]
87
96
  if not (
88
97
  body_parameter["type"]["type"] == "combined"
@@ -94,8 +103,8 @@ def add_overloads_for_body_param(yaml_data: dict[str, Any]) -> None:
94
103
  continue
95
104
  if body_type.get("type") == "model" and body_type.get("base") == "json":
96
105
  yaml_data["overloads"].append(add_overload(yaml_data, body_type, for_flatten_params=True))
97
- # Skip single-body JSON overload; the TypedDict overload replaces it
98
- continue
106
+ if skip_single_body_json:
107
+ continue
99
108
  yaml_data["overloads"].append(add_overload(yaml_data, body_type))
100
109
  content_type_param = next(p for p in yaml_data["parameters"] if p["wireName"].lower() == "content-type")
101
110
  content_type_param["inOverload"] = False
@@ -306,6 +315,10 @@ class PreProcessPlugin(YamlUpdatePlugin):
306
315
  def is_tsp(self) -> bool:
307
316
  return self.options.get("tsp_file", False)
308
317
 
318
+ @property
319
+ def generate_typeddict(self) -> bool:
320
+ return self.options.get("generate-typeddict", True)
321
+
309
322
  @staticmethod
310
323
  def _find_existing_typeddict(
311
324
  code_model: dict[str, Any],
@@ -394,11 +407,39 @@ class PreProcessPlugin(YamlUpdatePlugin):
394
407
  if not existing_td:
395
408
  code_model["types"].append(td_elem)
396
409
 
410
+ @staticmethod
411
+ def _insert_json_overload(
412
+ body_parameter: dict[str, Any],
413
+ origin_type: str,
414
+ ) -> None:
415
+ """Insert a raw-JSON (any-object) type into the body parameter's combined types.
416
+
417
+ This restores the pre-TypedDict dict-body overload used when TypedDict
418
+ generation is disabled.
419
+ """
420
+ if origin_type == "model":
421
+ body_parameter["type"]["types"].insert(1, KNOWN_TYPES["any-object"])
422
+ else:
423
+ # dict or list: copy the original container type and swap its element
424
+ # type for the raw-JSON any-object.
425
+ any_obj_list_or_dict = copy.deepcopy(body_parameter["type"]["types"][0])
426
+ any_obj_list_or_dict["elementType"] = KNOWN_TYPES["any-object"]
427
+ body_parameter["type"]["types"].insert(1, any_obj_list_or_dict)
428
+
397
429
  def add_body_param_type(
398
430
  self,
399
431
  code_model: dict[str, Any],
400
432
  body_parameter: dict[str, Any],
401
- ):
433
+ ) -> bool:
434
+ """Build the combined body-parameter type and its overload variants.
435
+
436
+ Returns whether a TypedDict-style overload was inserted in place of the
437
+ single-body raw-JSON overload on the spread (``base: json``) path. The
438
+ caller passes this to ``add_overloads_for_body_param`` as
439
+ ``skip_single_body_json``. It is False when TypedDict generation is
440
+ disabled, so the pre-TypedDict single-body raw-JSON overload is kept.
441
+ """
442
+ skip_single_body_json = False
402
443
  # For a binary `bytes` body (e.g. content type application/octet-stream or a custom
403
444
  # binary media type), add an IO overload alongside the `bytes` one. This keeps backward
404
445
  # compatibility for services migrating from swagger, whose binary bodies were typed as IO.
@@ -408,7 +449,7 @@ class PreProcessPlugin(YamlUpdatePlugin):
408
449
  body_parameter
409
450
  and body_parameter["type"]["type"] == "bytes"
410
451
  and is_binary_body
411
- and self.options["models-mode"] != "typeddict"
452
+ and not self.options.generate_typeddict_only
412
453
  and not any(t for t in ["flattened", "groupedBy"] if body_parameter.get(t))
413
454
  ):
414
455
  body_parameter["type"] = {
@@ -416,7 +457,7 @@ class PreProcessPlugin(YamlUpdatePlugin):
416
457
  "types": [body_parameter["type"], KNOWN_TYPES["binary"]],
417
458
  }
418
459
  code_model["types"].append(body_parameter["type"])
419
- return
460
+ return skip_single_body_json
420
461
 
421
462
  # only add overload for special content type
422
463
  if ( # pylint: disable=too-many-boolean-expressions
@@ -432,7 +473,7 @@ class PreProcessPlugin(YamlUpdatePlugin):
432
473
  )
433
474
  is_dpg_model = model_type.get("base") == "dpg"
434
475
  is_json_model = model_type.get("base") == "json"
435
- is_typeddict_only = self.options["models-mode"] == "typeddict"
476
+ is_typeddict_only = self.options.generate_typeddict_only
436
477
 
437
478
  body_parameter["type"] = {
438
479
  "type": "combined",
@@ -442,11 +483,16 @@ class PreProcessPlugin(YamlUpdatePlugin):
442
483
  if not (self.is_tsp and has_multi_part_content_type(body_parameter)) and not is_typeddict_only:
443
484
  body_parameter["type"]["types"].append(KNOWN_TYPES["binary"])
444
485
 
445
- # Add typeddict overload for non-spread dpg models
486
+ # Add the dict-body overload for non-spread dpg models: a TypedDict when
487
+ # enabled, otherwise the raw-JSON overload (pre-TypedDict behavior).
446
488
  if self.options["models-mode"] == "dpg" and is_dpg_model:
447
- cross_lang_id = model_type.get("crossLanguageDefinitionId")
448
- existing_td = self._find_existing_typeddict(code_model, cross_lang_id, model_type.get("name"))
449
- self._insert_typeddict_overload(code_model, body_parameter, model_type, origin_type, existing_td)
489
+ if self.generate_typeddict:
490
+ cross_lang_id = model_type.get("crossLanguageDefinitionId")
491
+ existing_td = self._find_existing_typeddict(code_model, cross_lang_id, model_type.get("name"))
492
+ self._insert_typeddict_overload(code_model, body_parameter, model_type, origin_type, existing_td)
493
+ skip_single_body_json = True
494
+ else:
495
+ self._insert_json_overload(body_parameter, origin_type)
450
496
 
451
497
  # For spread bodies (json base), add a typeddict overload that references
452
498
  # the original model. This replaces the JSON single-body overload.
@@ -456,17 +502,20 @@ class PreProcessPlugin(YamlUpdatePlugin):
456
502
 
457
503
  if is_typeddict_only and original:
458
504
  # In typeddict-only mode, the original dpg model already renders
459
- # as a TypedDict — reference it directly, no copy needed.
505
+ # as a TypedDict — reference it directly, no copy needed. It also
506
+ # replaces the single-body JSON overload.
507
+ skip_single_body_json = True
460
508
  if origin_type == "model":
461
509
  body_parameter["type"]["types"].insert(1, original)
462
510
  else:
463
511
  td_list_or_dict = copy.deepcopy(body_parameter["type"]["types"][0])
464
512
  td_list_or_dict["elementType"] = original
465
513
  body_parameter["type"]["types"].insert(1, td_list_or_dict)
466
- else:
514
+ elif self.generate_typeddict:
467
515
  source = original or model_type
468
516
  existing_td = self._find_existing_typeddict(code_model, cross_lang_id, source.get("name"))
469
517
  self._insert_typeddict_overload(code_model, body_parameter, source, origin_type, existing_td)
518
+ skip_single_body_json = True
470
519
 
471
520
  if len(body_parameter["type"]["types"]) == 1:
472
521
  # Only one body variant remains (e.g. typeddict-only mode where the
@@ -474,10 +523,12 @@ class PreProcessPlugin(YamlUpdatePlugin):
474
523
  # wrapper back to the single type so we don't emit a lone
475
524
  # ``@overload`` (mypy rejects a single overload definition).
476
525
  body_parameter["type"] = body_parameter["type"]["types"][0]
477
- return
526
+ return skip_single_body_json
478
527
 
479
528
  code_model["types"].append(body_parameter["type"])
480
529
 
530
+ return skip_single_body_json
531
+
481
532
  def pad_reserved_words(self, name: str, pad_type: PadType, yaml_type: dict[str, Any]) -> str:
482
533
  # we want to pad hidden variables as well
483
534
  if not name:
@@ -649,8 +700,8 @@ class PreProcessPlugin(YamlUpdatePlugin):
649
700
  response["discriminator"] = "operation"
650
701
  if body_parameter and not is_overload:
651
702
  # if we have a JSON body, we add a binary overload
652
- self.add_body_param_type(code_model, body_parameter)
653
- add_overloads_for_body_param(yaml_data)
703
+ skip_single_body_json = self.add_body_param_type(code_model, body_parameter)
704
+ add_overloads_for_body_param(yaml_data, skip_single_body_json=skip_single_body_json)
654
705
 
655
706
  def _update_lro_operation_helper(self, yaml_data: dict[str, Any]) -> None:
656
707
  for response in yaml_data.get("responses", []):
@@ -28,6 +28,21 @@ def description_ends_with_code_block(description: str) -> bool:
28
28
  return all(not line.strip() or line.startswith((" ", "\t")) for line in lines[directives[-1] + 1 :])
29
29
 
30
30
 
31
+ def is_typeddict_only(options: Any) -> bool:
32
+ """Whether generation is TypedDict-only.
33
+
34
+ True for TypeSpec input where no concrete models mode is selected (``models-mode: none``,
35
+ normalized to a falsy value by :class:`OptionsDict`) and ``generate-typeddict`` is enabled.
36
+ Swagger ``models-mode: none`` is left as a plain no-models mode because it never sets
37
+ ``tsp_file``.
38
+ """
39
+ return (
40
+ bool(options.get("tsp_file"))
41
+ and not options.get("models-mode")
42
+ and bool(options.get("generate-typeddict", True))
43
+ )
44
+
45
+
31
46
  def update_enum_value(name: str, value: Any, description: str, enum_type: dict[str, Any]) -> dict[str, Any]:
32
47
  return {
33
48
  "name": name,
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "version": "1",
3
- "pip_version": "26.1.2",
3
+ "pip_version": "26.2.1",
4
4
  "install": [
5
5
  {
6
6
  "download_info": {
7
- "url": "https://files.pythonhosted.org/packages/5d/40/e1e72872c6354b306daef1703549e8e83b4d43cfea356311bf722a043752/setuptools-83.0.0-py3-none-any.whl",
7
+ "url": "https://files.pythonhosted.org/packages/95/9c/c510029fc6ef33a6275cd2c5d3cecd6613dfd6aa401d57c54f1c18852ccf/setuptools-84.0.0-py3-none-any.whl",
8
8
  "archive_info": {
9
9
  "hashes": {
10
- "sha256": "29b23c360f22f414dc7336bb39178cc7bcbf6021ed2733cde173f09dba19abb3"
10
+ "sha256": "51a52592b3b99e102b609654876bd65f19f999935166d1352678931132b0c670"
11
11
  },
12
- "hash": "sha256=29b23c360f22f414dc7336bb39178cc7bcbf6021ed2733cde173f09dba19abb3"
12
+ "hash": "sha256=51a52592b3b99e102b609654876bd65f19f999935166d1352678931132b0c670"
13
13
  }
14
14
  },
15
15
  "is_direct": false,
@@ -18,7 +18,7 @@
18
18
  "metadata": {
19
19
  "metadata_version": "2.4",
20
20
  "name": "setuptools",
21
- "version": "83.0.0",
21
+ "version": "84.0.0",
22
22
  "dynamic": [
23
23
  "license-file"
24
24
  ],
@@ -125,9 +125,9 @@
125
125
  "implementation_version": "3.11.15",
126
126
  "os_name": "posix",
127
127
  "platform_machine": "x86_64",
128
- "platform_release": "6.17.0-1018-azure",
128
+ "platform_release": "6.17.0-1020-azure",
129
129
  "platform_system": "Linux",
130
- "platform_version": "#18~24.04.1-Ubuntu SMP Thu May 28 16:39:11 UTC 2026",
130
+ "platform_version": "#20~24.04.1-Ubuntu SMP Fri Jun 19 20:09:14 UTC 2026",
131
131
  "python_full_version": "3.11.15",
132
132
  "platform_python_implementation": "CPython",
133
133
  "python_version": "3.11",
@@ -12,7 +12,7 @@ from abc import ABC, abstractmethod
12
12
  from typing import Any, Iterator, Optional, Union
13
13
 
14
14
  import yaml
15
- from .utils import TYPESPEC_PACKAGE_MODE, VALID_PACKAGE_MODE
15
+ from .utils import TYPESPEC_PACKAGE_MODE, VALID_PACKAGE_MODE, is_typeddict_only
16
16
 
17
17
  from ._version import VERSION
18
18
 
@@ -36,6 +36,7 @@ class OptionsDict(MutableMapping):
36
36
  "low-level-client": False,
37
37
  "no-async": False,
38
38
  "no-namespace-folders": False,
39
+ "generate-typeddict": True,
39
40
  "polymorphic-examples": 5,
40
41
  "validate-versioning": True,
41
42
  "version-tolerant": True,
@@ -45,10 +46,20 @@ class OptionsDict(MutableMapping):
45
46
 
46
47
  def __init__(self, options: Optional[dict[str, Any]] = None) -> None:
47
48
  self._data = options.copy() if options else {}
49
+ # 'models-mode: typeddict' is deprecated. Represent it internally as 'none' models-mode with
50
+ # TypedDict generation enabled, so the rest of the codebase reasons in terms of models-mode
51
+ # plus the generate-typeddict flag.
52
+ if self._data.get("models-mode") == "typeddict":
53
+ self._data["generate-typeddict"] = True
48
54
  for key in list(self._data):
49
55
  self._data[key] = self._validate_and_transform(key, self._data[key])
50
56
  self._validate_combinations()
51
57
 
58
+ @property
59
+ def generate_typeddict_only(self) -> bool:
60
+ """Whether this is TypedDict-only generation ('models-mode: none' + TypedDicts)."""
61
+ return is_typeddict_only(self)
62
+
52
63
  def __getitem__(self, key: str) -> Any: # pylint: disable=too-many-return-statements
53
64
  if key == "head-as-boolean" and self.get("azure-arm"):
54
65
  # override to always true if azure-arm is set
@@ -167,14 +178,22 @@ class OptionsDict(MutableMapping):
167
178
  if key == "builders-visibility" and value not in ["public", "hidden", "embedded"]:
168
179
  raise ValueError("The value of --builders-visibility must be either 'public', 'hidden', or 'embedded'")
169
180
 
181
+ if key == "models-mode" and value == "typeddict":
182
+ # Deprecated: keep accepting it for back-compat but store it as 'none' (falsy) with
183
+ # TypedDict generation enabled (see OptionsDict.__init__ and generate_typeddict_only).
184
+ _LOGGER.warning(
185
+ "'models-mode: typeddict' is deprecated. Use 'models-mode: none' with "
186
+ "'generate-typeddict: true' (the default) instead."
187
+ )
188
+ value = False
189
+
170
190
  if key == "models-mode" and value == "none":
171
191
  value = False # switch to falsy value for easier code writing
172
192
 
173
- if key == "models-mode" and value not in ["msrest", "dpg", "typeddict", False]:
193
+ if key == "models-mode" and value not in ["msrest", "dpg", False]:
174
194
  raise ValueError(
175
- "--models-mode can only be 'msrest', 'dpg', 'typeddict', or 'none'. "
176
- "Pass in 'msrest' if you want msrest models, 'typeddict' for TypedDict models, or "
177
- "'none' if you don't want any."
195
+ "--models-mode can only be 'dpg' or 'none'. "
196
+ "Pass in 'dpg' for DPG models, or 'none' if you don't want any."
178
197
  )
179
198
  if key == "package-mode":
180
199
  if (