@typespec/http-client-python 0.35.1 → 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 (97) 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 +6 -2
  55. package/eng/scripts/ci/regenerate.ts +1 -2
  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 +25 -14
  71. package/generator/build/lib/pygen/codegen/serializers/client_serializer.py +13 -6
  72. package/generator/build/lib/pygen/codegen/serializers/model_serializer.py +4 -1
  73. package/generator/build/lib/pygen/codegen/serializers/types_serializer.py +4 -9
  74. package/generator/build/lib/pygen/preprocess/__init__.py +68 -17
  75. package/generator/build/lib/pygen/utils.py +15 -0
  76. package/generator/component-detection-pip-report.json +5 -5
  77. package/generator/dist/pygen-0.1.0-py3-none-any.whl +0 -0
  78. package/generator/pygen/__init__.py +24 -5
  79. package/generator/pygen/codegen/models/__init__.py +3 -1
  80. package/generator/pygen/codegen/models/code_model.py +7 -1
  81. package/generator/pygen/codegen/models/enum_type.py +7 -7
  82. package/generator/pygen/codegen/models/list_type.py +1 -0
  83. package/generator/pygen/codegen/models/model_type.py +1 -3
  84. package/generator/pygen/codegen/models/operation.py +7 -2
  85. package/generator/pygen/codegen/models/parameter.py +3 -1
  86. package/generator/pygen/codegen/models/utils.py +14 -0
  87. package/generator/pygen/codegen/serializers/__init__.py +6 -6
  88. package/generator/pygen/codegen/serializers/builder_serializer.py +25 -14
  89. package/generator/pygen/codegen/serializers/client_serializer.py +13 -6
  90. package/generator/pygen/codegen/serializers/model_serializer.py +4 -1
  91. package/generator/pygen/codegen/serializers/types_serializer.py +4 -9
  92. package/generator/pygen/preprocess/__init__.py +68 -17
  93. package/generator/pygen/utils.py +15 -0
  94. package/generator/pygen.egg-info/PKG-INFO +1 -1
  95. package/generator/pygen.egg-info/requires.txt +1 -1
  96. package/generator/setup.py +1 -1
  97. package/package.json +33 -33
@@ -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):
@@ -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.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
  ],
@@ -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 (
@@ -169,7 +169,9 @@ def build_type(yaml_data: dict[str, Any], code_model: CodeModel) -> BaseType:
169
169
  # need to special case model to avoid recursion
170
170
  if yaml_data["base"] == "typeddict":
171
171
  model_type = TypedDictModelType # type: ignore
172
- elif yaml_data["base"] == "json" or not code_model.options["models-mode"]:
172
+ elif yaml_data["base"] == "json" or (
173
+ not code_model.options["models-mode"] and not code_model.generate_typeddict_only
174
+ ):
173
175
  model_type = JSONModelType
174
176
  elif yaml_data["base"] == "dpg":
175
177
  model_type = DPGModelType # type: ignore
@@ -17,6 +17,7 @@ from .operation_group import OperationGroup
17
17
  from .utils import NamespaceType
18
18
  from .._utils import DEFAULT_HEADER_TEXT, DEFAULT_LICENSE_DESCRIPTION
19
19
  from ... import OptionsDict
20
+ from ...utils import is_typeddict_only
20
21
 
21
22
 
22
23
  def _is_legacy(options) -> bool:
@@ -89,7 +90,7 @@ class CodeModel: # pylint: disable=too-many-public-methods, disable=too-many-in
89
90
  self.clients: list[Client] = [
90
91
  Client.from_yaml(client_yaml_data, self) for client_yaml_data in yaml_data["clients"]
91
92
  ]
92
- if self.options["models-mode"] and self.model_types:
93
+ if (self.options["models-mode"] or self.generate_typeddict_only) and self.model_types:
93
94
  self.sort_model_types()
94
95
  self.named_unions: list[CombinedType] = [
95
96
  t for t in self.types_map.values() if isinstance(t, CombinedType) and t.name
@@ -174,6 +175,11 @@ class CodeModel: # pylint: disable=too-many-public-methods, disable=too-many-in
174
175
  def get_unique_types_alias(self, serialize_namespace: str, imported_namespace: str) -> str:
175
176
  return self._get_unique_import_alias(serialize_namespace, imported_namespace, "types")
176
177
 
178
+ @property
179
+ def generate_typeddict_only(self) -> bool:
180
+ """Whether this is TypedDict-only generation ('models-mode: none' + TypedDicts)."""
181
+ return is_typeddict_only(self.options)
182
+
177
183
  @property
178
184
  def client_namespace_types(self) -> dict[str, ClientNamespaceType]:
179
185
  if not self._client_namespace_types:
@@ -40,7 +40,7 @@ class EnumValue(BaseType):
40
40
 
41
41
  def type_annotation(self, **kwargs: Any) -> str:
42
42
  """The python type used for type annotation"""
43
- if self.code_model.options["models-mode"] == "typeddict":
43
+ if self.code_model.generate_typeddict_only:
44
44
  # A single constant enum value must be
45
45
  # annotated with its literal value directly (e.g. ``Literal["red"]``).
46
46
  return f"Literal[{self.value_type.get_declaration(self.value)}]"
@@ -83,7 +83,7 @@ class EnumValue(BaseType):
83
83
  file_import = FileImport(self.code_model)
84
84
  file_import.merge(self.value_type.imports(**kwargs))
85
85
  file_import.add_submodule_import("typing", "Literal", ImportType.STDLIB, TypingSection.REGULAR)
86
- if self.code_model.options["models-mode"] == "typeddict":
86
+ if self.code_model.generate_typeddict_only:
87
87
  # In typeddict mode the enums module (``_enums.py``) is never generated
88
88
  return file_import
89
89
  serialize_namespace = kwargs.get("serialize_namespace", self.code_model.namespace)
@@ -176,7 +176,7 @@ class EnumType(BaseType):
176
176
 
177
177
  @property
178
178
  def is_typeddict_mode(self) -> bool:
179
- return self.code_model.options["models-mode"] == "typeddict"
179
+ return self.code_model.generate_typeddict_only
180
180
 
181
181
  def type_annotation(self, **kwargs: Any) -> str:
182
182
  """The python type used for type annotation
@@ -184,7 +184,7 @@ class EnumType(BaseType):
184
184
  :return: The type annotation for this schema
185
185
  :rtype: str
186
186
  """
187
- if self.code_model.options["models-mode"]:
187
+ if self.code_model.options["models-mode"] or self.code_model.generate_typeddict_only:
188
188
  if self.is_typeddict_mode:
189
189
  # In typeddict mode, enums are Literal aliases defined in types.py
190
190
  serialize_namespace_type = kwargs.get("serialize_namespace_type")
@@ -221,13 +221,13 @@ class EnumType(BaseType):
221
221
  return self.value_type.get_declaration(value)
222
222
 
223
223
  def docstring_text(self, **kwargs: Any) -> str:
224
- if self.code_model.options["models-mode"]:
224
+ if self.code_model.options["models-mode"] or self.code_model.generate_typeddict_only:
225
225
  return self.name
226
226
  return self.value_type.type_annotation(**kwargs)
227
227
 
228
228
  def docstring_type(self, **kwargs: Any) -> str:
229
229
  """The python type used for RST syntax input and type annotation."""
230
- if self.code_model.options["models-mode"]:
230
+ if self.code_model.options["models-mode"] or self.code_model.generate_typeddict_only:
231
231
  type_annotation = self.value_type.type_annotation(**kwargs)
232
232
  enum_type_annotation = f"{self.client_namespace}.models.{self.name}"
233
233
  return f"{type_annotation} or ~{enum_type_annotation}"
@@ -261,7 +261,7 @@ class EnumType(BaseType):
261
261
  def imports(self, **kwargs: Any) -> FileImport:
262
262
  file_import = FileImport(self.code_model)
263
263
  file_import.merge(self.value_type.imports(**kwargs))
264
- if self.code_model.options["models-mode"]:
264
+ if self.code_model.options["models-mode"] or self.code_model.generate_typeddict_only:
265
265
  if self.is_typeddict_mode:
266
266
  # In typeddict mode, enums are Literal aliases in types.py — no Union needed
267
267
  serialize_namespace_type = kwargs.get("serialize_namespace_type")
@@ -38,6 +38,7 @@ class ListType(BaseType):
38
38
  self.code_model.options["version-tolerant"]
39
39
  and self.element_type.is_xml
40
40
  and not self.code_model.options["models-mode"]
41
+ and not self.code_model.generate_typeddict_only
41
42
  ):
42
43
  # this means we're version tolerant XML, we just return the XML element
43
44
  return self.element_type.type_annotation(**kwargs)
@@ -77,9 +77,7 @@ 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
+ self.is_typed_dict_only: bool = self.yaml_data.get("typedDictOnly", False) or code_model.generate_typeddict_only
83
81
 
84
82
  @property
85
83
  def is_usage_output(self) -> bool:
@@ -334,7 +334,7 @@ class OperationBase( # pylint: disable=too-many-public-methods,too-many-instanc
334
334
  file_import.merge(
335
335
  response.imports(async_mode=async_mode, need_import_iobase=self.need_import_iobase, **kwargs)
336
336
  )
337
- if self.code_model.options["models-mode"]:
337
+ if self.code_model.options["models-mode"] or self.code_model.generate_typeddict_only:
338
338
  for exception in self.exceptions:
339
339
  file_import.merge(exception.imports(async_mode=async_mode, **kwargs))
340
340
 
@@ -552,7 +552,12 @@ class Operation(OperationBase[Response]):
552
552
  "distributed_trace_async",
553
553
  ImportType.SDKCORE,
554
554
  )
555
- if self.has_response_body and not self.has_optional_return_type and not self.code_model.options["models-mode"]:
555
+ if (
556
+ self.has_response_body
557
+ and not self.has_optional_return_type
558
+ and not self.code_model.options["models-mode"]
559
+ and not self.code_model.generate_typeddict_only
560
+ ):
556
561
  file_import.add_submodule_import("typing", "cast", ImportType.STDLIB)
557
562
 
558
563
  return file_import
@@ -338,7 +338,9 @@ 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"] in ("dpg", "typeddict") and self.in_flattened_body:
341
+ if (
342
+ self.code_model.options["models-mode"] == "dpg" or self.code_model.generate_typeddict_only
343
+ ) and self.in_flattened_body:
342
344
  return ParameterMethodLocation.KEYWORD_ONLY
343
345
  if self.grouper:
344
346
  return ParameterMethodLocation.POSITIONAL
@@ -34,6 +34,20 @@ def add_to_pylint_disable(curr_str: str, entry: str) -> str:
34
34
  return f" # pylint: disable={entry}"
35
35
 
36
36
 
37
+ def escape_sphinx_field_name(name: str) -> str:
38
+ """Escape a name used as the target of a Sphinx info field (``:ivar``, ``:vartype``,
39
+ ``:keyword``, ``:paramtype``, ``:param``, ``:type``).
40
+
41
+ A name containing ``@`` (e.g. a wire name such as ``@search.facets``) is wrapped in
42
+ double backticks so it is treated as an inline literal. This renders cleanly in Sphinx
43
+ without a leading ``@`` being misinterpreted, and — unlike a ``\\@`` escape — introduces
44
+ no invalid escape sequence into the generated Python docstring.
45
+ """
46
+ if "@" in name:
47
+ return f"``{name}``"
48
+ return name
49
+
50
+
37
51
  class NamespaceType(str, Enum):
38
52
  """Special signal for impports"""
39
53
 
@@ -207,10 +207,10 @@ class JinjaSerializer(ReaderAndWriter):
207
207
  self._serialize_and_write_top_level_folder(env=env, namespace=client_namespace)
208
208
 
209
209
  # add models folder if there are models in this namespace
210
- is_typeddict_mode = self.code_model.options["models-mode"] == "typeddict"
211
- if (
212
- self.code_model.has_non_json_models(client_namespace_type.models) or client_namespace_type.enums
213
- ) and self.code_model.options["models-mode"]:
210
+ is_typeddict_mode = self.code_model.generate_typeddict_only
211
+ if (self.code_model.has_non_json_models(client_namespace_type.models) or client_namespace_type.enums) and (
212
+ self.code_model.options["models-mode"] or self.code_model.generate_typeddict_only
213
+ ):
214
214
  self._serialize_and_write_models_folder(
215
215
  env=env,
216
216
  namespace=client_namespace,
@@ -238,7 +238,7 @@ class JinjaSerializer(ReaderAndWriter):
238
238
  ).serialize(),
239
239
  )
240
240
 
241
- if not self.code_model.options["models-mode"]:
241
+ if not self.code_model.options["models-mode"] and not self.code_model.generate_typeddict_only:
242
242
  # keep models file if users ended up just writing a models file
243
243
  model_path = generation_path / Path("models.py")
244
244
  if self.read_file(model_path):
@@ -321,7 +321,7 @@ class JinjaSerializer(ReaderAndWriter):
321
321
  # Write the models folder
322
322
  models_path = self.code_model.get_generation_dir(namespace) / "models"
323
323
  models_mode = self.code_model.options["models-mode"]
324
- if models_mode in ("dpg", "typeddict"):
324
+ if models_mode == "dpg" or self.code_model.generate_typeddict_only:
325
325
  serializer = DpgModelSerializer
326
326
  else:
327
327
  serializer = MsrestModelSerializer