@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
@@ -0,0 +1,25 @@
1
+ import { strictEqual } from "assert";
2
+ import { afterEach, describe, it, vi } from "vitest";
3
+
4
+ const loadPyodide = vi.hoisted(() => vi.fn());
5
+
6
+ vi.mock("../src/pyodide-loader.js", () => ({ loadPyodide }));
7
+
8
+ describe("typespec-python: browser pyodide bootstrap", () => {
9
+ afterEach(() => {
10
+ delete (globalThis as any).window;
11
+ loadPyodide.mockReset();
12
+ vi.resetModules();
13
+ });
14
+
15
+ // Hosts like the TypeSpec playground import every available emitter up front. Booting Pyodide at
16
+ // module scope downloaded a full CPython WebAssembly runtime on every page load, which pushed the
17
+ // page past the per-tab memory budget on mobile browsers and prevented it from loading.
18
+ it("does not boot pyodide when the emitter is imported in a browser", async () => {
19
+ (globalThis as any).window = globalThis;
20
+
21
+ await import("../src/emitter.js");
22
+
23
+ strictEqual(loadPyodide.mock.calls.length, 0);
24
+ });
25
+ });
@@ -0,0 +1,72 @@
1
+ import type { SdkBuiltInType } from "@azure-tools/typespec-client-generator-core";
2
+ import { deepStrictEqual } from "assert";
3
+ import { describe, it } from "vitest";
4
+ import type { PythonSdkContext } from "../src/lib.js";
5
+ import { getType } from "../src/types.js";
6
+
7
+ function createBuiltInType(
8
+ kind: SdkBuiltInType["kind"],
9
+ encode?: string,
10
+ wireType?: SdkBuiltInType,
11
+ ): SdkBuiltInType {
12
+ return {
13
+ kind,
14
+ name: kind,
15
+ crossLanguageDefinitionId: `TypeSpec.${kind}`,
16
+ decorators: [],
17
+ encode,
18
+ wireType,
19
+ };
20
+ }
21
+
22
+ function emitBuiltInType(type: SdkBuiltInType): Record<string, any> {
23
+ const context = {
24
+ __simpleTypesMap: new Map(),
25
+ } as PythonSdkContext;
26
+ return getType(context, type);
27
+ }
28
+
29
+ describe("typespec-python: built-in types", () => {
30
+ it("preserves boolean encoded as string", () => {
31
+ deepStrictEqual(
32
+ emitBuiltInType(createBuiltInType("boolean", "string", createBuiltInType("string"))),
33
+ {
34
+ type: "boolean",
35
+ encode: "string",
36
+ },
37
+ );
38
+ });
39
+
40
+ it.each(["safeint", "uint32", "uint8"] as const)(
41
+ "preserves %s encoded as string as an integer",
42
+ (kind) => {
43
+ deepStrictEqual(
44
+ emitBuiltInType(createBuiltInType(kind, "string", createBuiltInType("string"))),
45
+ {
46
+ type: "integer",
47
+ encode: "string",
48
+ },
49
+ );
50
+ },
51
+ );
52
+
53
+ it.each(["base64", "base64url"] as const)("preserves bytes encoded as %s", (encode) => {
54
+ deepStrictEqual(
55
+ emitBuiltInType(createBuiltInType("bytes", encode, createBuiltInType("string"))),
56
+ {
57
+ type: "bytes",
58
+ encode,
59
+ },
60
+ );
61
+ });
62
+
63
+ it("falls back to the wire type for a custom encoding", () => {
64
+ deepStrictEqual(
65
+ emitBuiltInType(createBuiltInType("string", "abc", createBuiltInType("int32"))),
66
+ {
67
+ type: "integer",
68
+ encode: "abc",
69
+ },
70
+ );
71
+ });
72
+ });
@@ -7,6 +7,6 @@ colorama==0.4.6
7
7
  debugpy==1.8.2
8
8
  pytest==9.0.3
9
9
  coverage==7.6.1
10
- black==24.4.0
10
+ black==26.3.1
11
11
  ptvsd==4.3.2
12
12
  types-PyYAML==6.0.12.8
@@ -69,7 +69,11 @@ export interface BuildTaskGroupsOptions {
69
69
 
70
70
  // ---- Public constants ----
71
71
 
72
- export const SKIP_SPECS: string[] = ["type/file"];
72
+ export const SKIP_SPECS: string[] = [
73
+ "type/file",
74
+ // LroPaging.postPagingLroWithBody generates begin_post_paging_lro_with_body with pylint R0915 (52/50).
75
+ "azure/resource-manager/operation-templates",
76
+ ];
73
77
 
74
78
  export const SpecialFlags: Record<string, Record<string, any>> = {
75
79
  azure: {
@@ -320,7 +324,7 @@ export const EMITTER_OPTIONS: Record<string, Record<string, string> | Record<str
320
324
  {
321
325
  "package-name": "typetest-model-usage-typeddictonly",
322
326
  namespace: "typetest.model.usage.typeddictonly",
323
- "models-mode": "typeddict",
327
+ "models-mode": "none",
324
328
  },
325
329
  ],
326
330
  "type/model/visibility": [
@@ -20,13 +20,12 @@ import pc from "picocolors";
20
20
  import { fileURLToPath } from "url";
21
21
  import { parseArgs } from "util";
22
22
 
23
+ import type { RegenerateContext, RegenerateFlags } from "./regenerate-common.js";
23
24
  import {
24
25
  buildTaskGroups,
25
26
  cleanGeneratedCode,
26
27
  getSubdirectories,
27
28
  preprocess,
28
- RegenerateContext,
29
- RegenerateFlags,
30
29
  runParallel,
31
30
  } from "./regenerate-common.js";
32
31
 
@@ -1,5 +1,6 @@
1
1
  /* eslint-disable no-console */
2
- import { ChildProcess, spawn } from "child_process";
2
+ import type { ChildProcess } from "child_process";
3
+ import { spawn } from "child_process";
3
4
  import fs from "fs";
4
5
  import { cpus } from "os";
5
6
  import { dirname, join } from "path";
@@ -1,4 +1,5 @@
1
- import { ChildProcess, spawn, SpawnOptions } from "child_process";
1
+ import type { ChildProcess, SpawnOptions } from "child_process";
2
+ import { spawn } from "child_process";
2
3
 
3
4
  /*
4
5
  * Copied from @autorest/system-requirements
@@ -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
@@ -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]:
@@ -715,7 +714,9 @@ class _OperationSerializer(_BuilderBaseSerializer[OperationType]):
715
714
  send_xml = builder.parameters.body_parameter.type.is_xml
716
715
  xml_serialization_ctxt = body_param.type.xml_serialization_ctxt if send_xml else None
717
716
  ser_ctxt_name = "serialization_ctxt"
718
- 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
+ ):
719
720
  retval.append(f'{ser_ctxt_name} = {{"xml": {{{xml_serialization_ctxt}}}}}')
720
721
  if self.code_model.options["models-mode"] == "msrest":
721
722
  is_xml_cmd = _xml_config(send_xml, builder.parameters.body_parameter.content_types)
@@ -725,7 +726,7 @@ class _OperationSerializer(_BuilderBaseSerializer[OperationType]):
725
726
  f"_{body_kwarg_name} = self._serialize.body({body_param.client_name}, "
726
727
  f"'{serialization_type}'{is_xml_cmd}{serialization_ctxt_cmd})"
727
728
  )
728
- elif self.code_model.options["models-mode"] == "typeddict":
729
+ elif self.code_model.generate_typeddict_only:
729
730
  # TypedDict-only models are plain dicts — no serialization needed
730
731
  create_body_call = f"_{body_kwarg_name} = {body_param.client_name}"
731
732
  elif self.code_model.options["models-mode"] == "dpg":
@@ -844,7 +845,9 @@ class _OperationSerializer(_BuilderBaseSerializer[OperationType]):
844
845
  overload.request_builder.parameters.body_parameter.client_name for overload in builder.overloads
845
846
  ]
846
847
  all_dpg_model_overloads = False
847
- 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:
848
851
  all_dpg_model_overloads = all(
849
852
  _is_dpg_or_typeddict_body(o.parameters.body_parameter) for o in builder.overloads
850
853
  )
@@ -1131,8 +1134,8 @@ class _OperationSerializer(_BuilderBaseSerializer[OperationType]):
1131
1134
  retval.extend([f" {l}" for l in response_read])
1132
1135
  retval.append(" map_error(status_code=response.status_code, response=response, error_map=error_map)")
1133
1136
  error_model = ""
1134
- if ( # pylint: disable=too-many-nested-blocks
1135
- 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
1136
1139
  ):
1137
1140
  error_model = ", model=error"
1138
1141
  condition = "if"
@@ -1214,7 +1217,9 @@ class _OperationSerializer(_BuilderBaseSerializer[OperationType]):
1214
1217
  condition = "elif"
1215
1218
  # default error handling
1216
1219
  default_error_deserialization = builder.default_error_deserialization(self.serialize_namespace)
1217
- 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
+ ):
1218
1223
  error_model = ", model=error"
1219
1224
  indent = " " if builder.non_default_errors else " "
1220
1225
  if builder.non_default_errors:
@@ -1293,7 +1298,11 @@ class _OperationSerializer(_BuilderBaseSerializer[OperationType]):
1293
1298
  else:
1294
1299
  retval.extend(self.response_headers_and_deserialization(builder, builder.responses[0]))
1295
1300
  retval.append("")
1296
- 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
+ ):
1297
1306
  deserialized = "deserialized"
1298
1307
  else:
1299
1308
  deserialized = f"cast({builder.response_type_annotation(async_mode=self.async_mode)}, deserialized)"
@@ -1329,7 +1338,9 @@ class _OperationSerializer(_BuilderBaseSerializer[OperationType]):
1329
1338
 
1330
1339
  def error_map(self, builder: OperationType) -> list[str]:
1331
1340
  retval = ["error_map: MutableMapping = {"]
1332
- 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
+ ):
1333
1344
  # TODO: we should decide whether to add the build-in error map when there is a customized default error type
1334
1345
  if self._need_specific_error_map(401, builder):
1335
1346
  retval.append(" 401: ClientAuthenticationError,")
@@ -1693,7 +1704,7 @@ class _LROOperationSerializer(_OperationSerializer[LROOperationType]):
1693
1704
  if builder.lro_response.headers:
1694
1705
  retval.append(" response_headers = {}")
1695
1706
  if (
1696
- not self.code_model.options["models-mode"]
1707
+ (not self.code_model.options["models-mode"] and not self.code_model.generate_typeddict_only)
1697
1708
  or self.code_model.options["models-mode"] == "dpg"
1698
1709
  or builder.lro_response.headers
1699
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
@@ -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
@@ -80,6 +80,9 @@ def _documentation_string(
80
80
  doc_name = (
81
81
  prop.wire_name if kwargs.get("serialize_namespace_type") == NamespaceType.TYPES_FILE else prop.client_name
82
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)
83
86
  sphinx_prefix = f":{description_keyword} {doc_name}:"
84
87
  description = prop.description(is_operation_file=False).replace("\\", "\\\\")
85
88
  retval.append(f"{sphinx_prefix} {description}" if description else sphinx_prefix)