@typespec/http-client-python 0.33.0 → 0.34.1

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 (86) hide show
  1. package/README.md +25 -3
  2. package/dist/emitter/emitter.d.ts.map +1 -1
  3. package/dist/emitter/emitter.js +2 -11
  4. package/dist/emitter/emitter.js.map +1 -1
  5. package/dist/emitter/external-process.d.ts +14 -0
  6. package/dist/emitter/external-process.d.ts.map +1 -1
  7. package/dist/emitter/external-process.js +17 -1
  8. package/dist/emitter/external-process.js.map +1 -1
  9. package/dist/emitter/lib.js +1 -1
  10. package/dist/emitter/lib.js.map +1 -1
  11. package/dist/emitter/node-runner.d.ts.map +1 -1
  12. package/dist/emitter/node-runner.js +2 -1
  13. package/dist/emitter/node-runner.js.map +1 -1
  14. package/dist/emitter/types.d.ts.map +1 -1
  15. package/dist/emitter/types.js +2 -0
  16. package/dist/emitter/types.js.map +1 -1
  17. package/dist/emitter/utils.d.ts +10 -0
  18. package/dist/emitter/utils.d.ts.map +1 -1
  19. package/dist/emitter/utils.js +12 -0
  20. package/dist/emitter/utils.js.map +1 -1
  21. package/emitter/src/emitter.ts +2 -14
  22. package/emitter/src/external-process.ts +18 -1
  23. package/emitter/src/lib.ts +1 -1
  24. package/emitter/src/node-runner.ts +2 -1
  25. package/emitter/src/types.ts +2 -0
  26. package/emitter/src/utils.ts +13 -0
  27. package/emitter/temp/tsconfig.tsbuildinfo +1 -1
  28. package/emitter/test/external-process.test.ts +28 -0
  29. package/emitter/test/utils.test.ts +12 -1
  30. package/eng/scripts/ci/regenerate-common.ts +40 -15
  31. package/eng/scripts/setup/__pycache__/package_manager.cpython-311.pyc +0 -0
  32. package/eng/scripts/setup/__pycache__/venvtools.cpython-311.pyc +0 -0
  33. package/generator/build/lib/pygen/__init__.py +3 -3
  34. package/generator/build/lib/pygen/codegen/models/__init__.py +4 -2
  35. package/generator/build/lib/pygen/codegen/models/code_model.py +27 -5
  36. package/generator/build/lib/pygen/codegen/models/combined_type.py +3 -3
  37. package/generator/build/lib/pygen/codegen/models/enum_type.py +75 -25
  38. package/generator/build/lib/pygen/codegen/models/list_type.py +10 -4
  39. package/generator/build/lib/pygen/codegen/models/model_type.py +149 -4
  40. package/generator/build/lib/pygen/codegen/models/operation.py +1 -0
  41. package/generator/build/lib/pygen/codegen/models/paging_operation.py +5 -1
  42. package/generator/build/lib/pygen/codegen/models/parameter.py +2 -2
  43. package/generator/build/lib/pygen/codegen/models/primitive_types.py +30 -6
  44. package/generator/build/lib/pygen/codegen/models/property.py +21 -8
  45. package/generator/build/lib/pygen/codegen/models/response.py +16 -3
  46. package/generator/build/lib/pygen/codegen/models/utils.py +3 -6
  47. package/generator/build/lib/pygen/codegen/serializers/__init__.py +46 -11
  48. package/generator/build/lib/pygen/codegen/serializers/builder_serializer.py +48 -15
  49. package/generator/build/lib/pygen/codegen/serializers/model_init_serializer.py +1 -1
  50. package/generator/build/lib/pygen/codegen/serializers/model_serializer.py +127 -3
  51. package/generator/build/lib/pygen/codegen/serializers/types_serializer.py +356 -12
  52. package/generator/build/lib/pygen/codegen/serializers/unions_serializer.py +44 -0
  53. package/generator/build/lib/pygen/codegen/templates/model_container.py.jinja2 +1 -1
  54. package/generator/build/lib/pygen/codegen/templates/types.py.jinja2 +51 -2
  55. package/generator/build/lib/pygen/codegen/templates/unions.py.jinja2 +9 -0
  56. package/generator/build/lib/pygen/preprocess/__init__.py +175 -162
  57. package/generator/build/lib/pygen/utils.py +6 -24
  58. package/generator/component-detection-pip-report.json +9 -9
  59. package/generator/dist/pygen-0.1.0-py3-none-any.whl +0 -0
  60. package/generator/pygen/__init__.py +3 -3
  61. package/generator/pygen/codegen/models/__init__.py +4 -2
  62. package/generator/pygen/codegen/models/code_model.py +27 -5
  63. package/generator/pygen/codegen/models/combined_type.py +3 -3
  64. package/generator/pygen/codegen/models/enum_type.py +75 -25
  65. package/generator/pygen/codegen/models/list_type.py +10 -4
  66. package/generator/pygen/codegen/models/model_type.py +149 -4
  67. package/generator/pygen/codegen/models/operation.py +1 -0
  68. package/generator/pygen/codegen/models/paging_operation.py +5 -1
  69. package/generator/pygen/codegen/models/parameter.py +2 -2
  70. package/generator/pygen/codegen/models/primitive_types.py +30 -6
  71. package/generator/pygen/codegen/models/property.py +21 -8
  72. package/generator/pygen/codegen/models/response.py +16 -3
  73. package/generator/pygen/codegen/models/utils.py +3 -6
  74. package/generator/pygen/codegen/serializers/__init__.py +46 -11
  75. package/generator/pygen/codegen/serializers/builder_serializer.py +48 -15
  76. package/generator/pygen/codegen/serializers/model_init_serializer.py +1 -1
  77. package/generator/pygen/codegen/serializers/model_serializer.py +127 -3
  78. package/generator/pygen/codegen/serializers/types_serializer.py +356 -12
  79. package/generator/pygen/codegen/serializers/unions_serializer.py +44 -0
  80. package/generator/pygen/codegen/templates/model_container.py.jinja2 +1 -1
  81. package/generator/pygen/codegen/templates/types.py.jinja2 +51 -2
  82. package/generator/pygen/codegen/templates/unions.py.jinja2 +9 -0
  83. package/generator/pygen/preprocess/__init__.py +175 -162
  84. package/generator/pygen/utils.py +6 -24
  85. package/generator/pygen.egg-info/SOURCES.txt +2 -0
  86. package/package.json +5 -5
@@ -0,0 +1,28 @@
1
+ import { ok, strictEqual } from "assert";
2
+ import { load } from "js-yaml";
3
+ import { describe, it } from "vitest";
4
+ import { dumpCodeModelToYaml } from "../src/external-process.js";
5
+
6
+ describe("typespec-python: external-process", () => {
7
+ // The Python generator parses the emitted YAML with PyYAML (YAML 1.1), where a plain
8
+ // scalar such as `2020_01_01` is interpreted as the integer `20200101`. js-yaml dumps
9
+ // using YAML 1.2 rules and would otherwise leave such string scalars unquoted, so we
10
+ // must force-quote strings to keep enum member names (and other string values) intact.
11
+ it("force-quotes string scalars that YAML 1.1 would misinterpret", () => {
12
+ const yaml = dumpCodeModelToYaml({ name: "2020_01_01" });
13
+ // The scalar must be quoted, otherwise PyYAML reads it back as the integer 20200101.
14
+ ok(yaml.includes('"2020_01_01"'), `expected the underscore scalar to be quoted, got: ${yaml}`);
15
+ ok(
16
+ !/name:\s*2020_01_01\s*$/m.test(yaml),
17
+ `expected no unquoted underscore scalar, got: ${yaml}`,
18
+ );
19
+ });
20
+
21
+ it("keeps string values as strings after a round-trip", () => {
22
+ const codeModel = { name: "2020_01_01", value: "2020-01-01", plain: "hello" };
23
+ const roundTripped = load(dumpCodeModelToYaml(codeModel)) as Record<string, unknown>;
24
+ strictEqual(roundTripped.name, "2020_01_01");
25
+ strictEqual(roundTripped.value, "2020-01-01");
26
+ strictEqual(roundTripped.plain, "hello");
27
+ });
28
+ });
@@ -1,6 +1,6 @@
1
1
  import { strictEqual } from "assert";
2
2
  import { describe, it } from "vitest";
3
- import { camelToSnakeCase, md2Rst } from "../src/utils.js";
3
+ import { camelToSnakeCase, md2Rst, quoteShellArg } from "../src/utils.js";
4
4
 
5
5
  describe("typespec-python: utils", () => {
6
6
  it("camelToSnakeCase", async () => {
@@ -27,4 +27,15 @@ describe("typespec-python: utils", () => {
27
27
  const des = "Format: <MajorVersion>.<MinorVersion>.<Patch>";
28
28
  strictEqual(md2Rst(des), "Format: <MajorVersion>.<MinorVersion>.<Patch>");
29
29
  });
30
+
31
+ it("quoteShellArg wraps values in double quotes without doubling existing content", async () => {
32
+ // Regression test for https://github.com/microsoft/typespec Python SDK generation failure
33
+ // where a package-pprint-name with spaces (e.g. "Azure Web PubSub Chat Service") got quotes
34
+ // baked into the option value and then leaked into the generated setup.py as doubled quotes.
35
+ strictEqual(quoteShellArg("Azure Web PubSub Chat Service"), '"Azure Web PubSub Chat Service"');
36
+ strictEqual(quoteShellArg("simple"), '"simple"');
37
+ strictEqual(quoteShellArg('has "quotes"'), '"has \\"quotes\\""');
38
+ strictEqual(quoteShellArg("trailing\\"), '"trailing\\\\"');
39
+ strictEqual(quoteShellArg('back\\slash "q"'), '"back\\\\slash \\"q\\""');
40
+ });
30
41
  });
@@ -172,9 +172,15 @@ export const AZURE_EMITTER_OPTIONS: Record<
172
172
  "package-name": "client-structure-twooperationgroup",
173
173
  namespace: "client.structure.twooperationgroup",
174
174
  },
175
- "client/naming": {
176
- namespace: "client.naming.main",
177
- },
175
+ "client/naming": [
176
+ {
177
+ namespace: "client.naming.main",
178
+ },
179
+ {
180
+ "package-name": "client-naming-typeddict",
181
+ namespace: "client.naming.typeddict",
182
+ },
183
+ ],
178
184
  "client/overload": {
179
185
  namespace: "client.overload",
180
186
  },
@@ -271,14 +277,26 @@ export const EMITTER_OPTIONS: Record<string, Record<string, string> | Record<str
271
277
  "package-name": "typetest-model-nesteddiscriminator",
272
278
  namespace: "typetest.model.nesteddiscriminator",
273
279
  },
274
- "type/model/inheritance/not-discriminated": {
275
- "package-name": "typetest-model-notdiscriminated",
276
- namespace: "typetest.model.notdiscriminated",
277
- },
278
- "type/model/inheritance/single-discriminator": {
279
- "package-name": "typetest-model-singlediscriminator",
280
- namespace: "typetest.model.singlediscriminator",
281
- },
280
+ "type/model/inheritance/not-discriminated": [
281
+ {
282
+ "package-name": "typetest-model-notdiscriminated",
283
+ namespace: "typetest.model.notdiscriminated",
284
+ },
285
+ {
286
+ "package-name": "typetest-model-notdiscriminated-typeddict",
287
+ namespace: "typetest.model.notdiscriminated.typeddict",
288
+ },
289
+ ],
290
+ "type/model/inheritance/single-discriminator": [
291
+ {
292
+ "package-name": "typetest-model-singlediscriminator",
293
+ namespace: "typetest.model.singlediscriminator",
294
+ },
295
+ {
296
+ "package-name": "typetest-model-singlediscriminator-typeddict",
297
+ namespace: "typetest.model.singlediscriminator.typeddict",
298
+ },
299
+ ],
282
300
  "type/model/inheritance/recursive": [
283
301
  {
284
302
  "package-name": "typetest-model-recursive",
@@ -292,10 +310,17 @@ export const EMITTER_OPTIONS: Record<string, Record<string, string> | Record<str
292
310
  "clear-output-folder": "true",
293
311
  },
294
312
  ],
295
- "type/model/usage": {
296
- "package-name": "typetest-model-usage",
297
- namespace: "typetest.model.usage",
298
- },
313
+ "type/model/usage": [
314
+ {
315
+ "package-name": "typetest-model-usage",
316
+ namespace: "typetest.model.usage",
317
+ },
318
+ {
319
+ "package-name": "typetest-model-usage-typeddictonly",
320
+ namespace: "typetest.model.usage.typeddictonly",
321
+ "models-mode": "typeddict",
322
+ },
323
+ ],
299
324
  "type/model/visibility": [
300
325
  {
301
326
  "package-name": "typetest-model-visibility",
@@ -168,10 +168,10 @@ class OptionsDict(MutableMapping):
168
168
  if key == "models-mode" and value == "none":
169
169
  value = False # switch to falsy value for easier code writing
170
170
 
171
- if key == "models-mode" and value not in ["msrest", "dpg", False]:
171
+ if key == "models-mode" and value not in ["msrest", "dpg", "typeddict", False]:
172
172
  raise ValueError(
173
- "--models-mode can only be 'msrest', 'dpg' or 'none'. "
174
- "Pass in 'msrest' if you want msrest models, or "
173
+ "--models-mode can only be 'msrest', 'dpg', 'typeddict', or 'none'. "
174
+ "Pass in 'msrest' if you want msrest models, 'typeddict' for TypedDict models, or "
175
175
  "'none' if you don't want any."
176
176
  )
177
177
  if key == "package-mode":
@@ -9,7 +9,7 @@ from .base import BaseModel
9
9
  from .base_builder import BaseBuilder, ParameterListType
10
10
  from .code_model import CodeModel
11
11
  from .client import Client
12
- from .model_type import ModelType, JSONModelType, DPGModelType, MsrestModelType
12
+ from .model_type import ModelType, JSONModelType, DPGModelType, MsrestModelType, TypedDictModelType
13
13
  from .dictionary_type import DictionaryType
14
14
  from .list_type import ListType
15
15
  from .combined_type import CombinedType
@@ -167,7 +167,9 @@ def build_type(yaml_data: dict[str, Any], code_model: CodeModel) -> BaseType:
167
167
  response: Optional[BaseType] = None
168
168
  if yaml_data["type"] == "model":
169
169
  # need to special case model to avoid recursion
170
- if yaml_data["base"] == "json" or not code_model.options["models-mode"]:
170
+ if yaml_data["base"] == "typeddict":
171
+ model_type = TypedDictModelType # type: ignore
172
+ elif yaml_data["base"] == "json" or not code_model.options["models-mode"]:
171
173
  model_type = JSONModelType
172
174
  elif yaml_data["base"] == "dpg":
173
175
  model_type = DPGModelType # type: ignore
@@ -152,16 +152,28 @@ class CodeModel: # pylint: disable=too-many-public-methods, disable=too-many-in
152
152
  return result
153
153
  return f"{result}{module_name}" if result.endswith(".") else f"{result}.{module_name}"
154
154
 
155
- def get_unique_models_alias(self, serialize_namespace: str, imported_namespace: str) -> str:
155
+ def _get_unique_import_alias(self, serialize_namespace: str, imported_namespace: str, module_name: str) -> str:
156
156
  if not self.has_subnamespace:
157
- return "_models"
157
+ return f"_{module_name}"
158
158
  relative_path = self.get_relative_import_path(
159
159
  serialize_namespace, self.get_imported_namespace_for_model(imported_namespace)
160
160
  )
161
161
  dot_num = max(relative_path.count(".") - 1, 0)
162
- parts = [""] + ([p for p in relative_path.split(".") if p] or ["models"])
162
+ path_parts = [p for p in relative_path.split(".") if p]
163
+ # For "models", keep existing format: _<path_parts><dot_num> (e.g. _models1, _firstnamespace_models2)
164
+ # For other modules like "types", prefix with module name: _types_<path_parts><dot_num>
165
+ if module_name == "models":
166
+ parts = [""] + (path_parts or [module_name])
167
+ else:
168
+ parts = [f"_{module_name}"] + (path_parts or [])
163
169
  return "_".join(parts) + (str(dot_num) if dot_num > 0 else "")
164
170
 
171
+ def get_unique_models_alias(self, serialize_namespace: str, imported_namespace: str) -> str:
172
+ return self._get_unique_import_alias(serialize_namespace, imported_namespace, "models")
173
+
174
+ def get_unique_types_alias(self, serialize_namespace: str, imported_namespace: str) -> str:
175
+ return self._get_unique_import_alias(serialize_namespace, imported_namespace, "types")
176
+
165
177
  @property
166
178
  def client_namespace_types(self) -> dict[str, ClientNamespaceType]:
167
179
  if not self._client_namespace_types:
@@ -174,6 +186,14 @@ class CodeModel: # pylint: disable=too-many-public-methods, disable=too-many-in
174
186
  if model.client_namespace not in self._client_namespace_types:
175
187
  self._client_namespace_types[model.client_namespace] = ClientNamespaceType()
176
188
  self._client_namespace_types[model.client_namespace].models.append(model)
189
+ # TypedDict copies (base="typeddict") are excluded from model_types to keep
190
+ # them out of _models.py, but they need to be in the namespace model list
191
+ # so the TypesSerializer can render them in types.py.
192
+ for t in self.types_map.values():
193
+ if isinstance(t, ModelType) and t.base == "typeddict" and t.usage != UsageFlags.Default.value:
194
+ if t.client_namespace not in self._client_namespace_types:
195
+ self._client_namespace_types[t.client_namespace] = ClientNamespaceType()
196
+ self._client_namespace_types[t.client_namespace].models.append(t)
177
197
  for enum in self.enums:
178
198
  if enum.client_namespace not in self._client_namespace_types:
179
199
  self._client_namespace_types[enum.client_namespace] = ClientNamespaceType()
@@ -339,7 +359,9 @@ class CodeModel: # pylint: disable=too-many-public-methods, disable=too-many-in
339
359
  """All of the model types in this class"""
340
360
  if not self._model_types:
341
361
  self._model_types = [
342
- t for t in self.types_map.values() if isinstance(t, ModelType) and t.usage != UsageFlags.Default.value
362
+ t
363
+ for t in self.types_map.values()
364
+ if isinstance(t, ModelType) and t.usage != UsageFlags.Default.value and t.base != "typeddict"
343
365
  ]
344
366
  return self._model_types
345
367
 
@@ -349,7 +371,7 @@ class CodeModel: # pylint: disable=too-many-public-methods, disable=too-many-in
349
371
 
350
372
  @staticmethod
351
373
  def get_public_model_types(models: list[ModelType]) -> list[ModelType]:
352
- return [m for m in models if not m.internal and not m.base == "json"]
374
+ return [m for m in models if not m.internal and not m.base == "json" and not m.is_typed_dict_only]
353
375
 
354
376
  @property
355
377
  def public_model_types(self) -> list[ModelType]:
@@ -66,7 +66,7 @@ class CombinedType(BaseType):
66
66
 
67
67
  def type_annotation(self, **kwargs: Any) -> str:
68
68
  if self.name:
69
- return f'"_types.{self.name}"'
69
+ return f'"_unions.{self.name}"'
70
70
  return self.type_definition(**kwargs)
71
71
 
72
72
  def type_definition(self, **kwargs: Any) -> str:
@@ -116,10 +116,10 @@ class CombinedType(BaseType):
116
116
  file_import = FileImport(self.code_model)
117
117
  serialize_namespace = kwargs.get("serialize_namespace", self.code_model.namespace)
118
118
  serialize_namespace_type = kwargs.get("serialize_namespace_type")
119
- if self.name and serialize_namespace_type != NamespaceType.TYPES_FILE:
119
+ if self.name and serialize_namespace_type != NamespaceType.UNIONS_FILE:
120
120
  file_import.add_submodule_import(
121
121
  self.code_model.get_relative_import_path(serialize_namespace),
122
- "_types",
122
+ "_unions",
123
123
  ImportType.LOCAL,
124
124
  TypingSection.TYPING,
125
125
  )
@@ -167,6 +167,10 @@ class EnumType(BaseType):
167
167
  enum_description = f"Known values are: {possible_values_str}."
168
168
  return enum_description
169
169
 
170
+ @property
171
+ def is_typeddict_mode(self) -> bool:
172
+ return self.code_model.options["models-mode"] == "typeddict"
173
+
170
174
  def type_annotation(self, **kwargs: Any) -> str:
171
175
  """The python type used for type annotation
172
176
 
@@ -174,9 +178,20 @@ class EnumType(BaseType):
174
178
  :rtype: str
175
179
  """
176
180
  if self.code_model.options["models-mode"]:
181
+ if self.is_typeddict_mode:
182
+ # In typeddict mode, enums are Literal aliases defined in types.py
183
+ serialize_namespace_type = kwargs.get("serialize_namespace_type")
184
+ if serialize_namespace_type == NamespaceType.TYPES_FILE:
185
+ # Same file — just the name, no module prefix
186
+ return self.name
187
+ # From operation/client files, use types.EnumName (matching model pattern)
188
+ return f"types.{self.name}"
177
189
 
178
190
  module_name = ""
179
- if kwargs.get("need_model_alias", True):
191
+ serialize_namespace_type = kwargs.get("serialize_namespace_type")
192
+ if serialize_namespace_type == NamespaceType.TYPES_FILE:
193
+ pass # no module prefix for types.py
194
+ elif kwargs.get("need_model_alias", True):
180
195
  serialize_namespace = kwargs.get("serialize_namespace", self.code_model.namespace)
181
196
  model_alias = self.code_model.get_unique_models_alias(serialize_namespace, self.client_namespace)
182
197
  module_name = f"{model_alias}."
@@ -240,31 +255,66 @@ class EnumType(BaseType):
240
255
  file_import = FileImport(self.code_model)
241
256
  file_import.merge(self.value_type.imports(**kwargs))
242
257
  if self.code_model.options["models-mode"]:
243
- file_import.add_submodule_import("typing", "Union", ImportType.STDLIB, TypingSection.REGULAR)
258
+ if self.is_typeddict_mode:
259
+ # In typeddict mode, enums are Literal aliases in types.py — no Union needed
260
+ serialize_namespace_type = kwargs.get("serialize_namespace_type")
261
+ if serialize_namespace_type == NamespaceType.TYPES_FILE:
262
+ # Same file — no import needed for same-namespace enums
263
+ serialize_namespace = kwargs.get("serialize_namespace", self.code_model.namespace)
264
+ if self.client_namespace != serialize_namespace:
265
+ # Cross-namespace: import from sibling types module
266
+ relative_path = self.code_model.get_relative_import_path(
267
+ serialize_namespace, self.client_namespace
268
+ )
269
+ file_import.add_submodule_import(
270
+ f"{relative_path}types" if relative_path != "." else ".types",
271
+ self.name,
272
+ ImportType.LOCAL,
273
+ typing_section=TypingSection.REGULAR,
274
+ )
275
+ elif serialize_namespace_type in [NamespaceType.OPERATION, NamespaceType.CLIENT]:
276
+ # Import types module directly (matching model pattern)
277
+ serialize_namespace = kwargs.get("serialize_namespace", self.code_model.namespace)
278
+ relative_path = self.code_model.get_relative_import_path(serialize_namespace, self.client_namespace)
279
+ file_import.add_submodule_import(
280
+ relative_path,
281
+ "types",
282
+ ImportType.LOCAL,
283
+ )
284
+ else:
285
+ file_import.add_submodule_import("typing", "Union", ImportType.STDLIB, TypingSection.REGULAR)
244
286
 
245
- serialize_namespace = kwargs.get("serialize_namespace", self.code_model.namespace)
246
- relative_path = self.code_model.get_relative_import_path(serialize_namespace, self.client_namespace)
247
- alias = self.code_model.get_unique_models_alias(serialize_namespace, self.client_namespace)
248
- serialize_namespace_type = kwargs.get("serialize_namespace_type")
249
- called_by_property = kwargs.get("called_by_property", False)
250
- if serialize_namespace_type in [NamespaceType.OPERATION, NamespaceType.CLIENT]:
251
- file_import.add_submodule_import(
252
- relative_path,
253
- "models",
254
- ImportType.LOCAL,
255
- alias=alias,
256
- typing_section=TypingSection.REGULAR,
257
- )
258
- elif serialize_namespace_type == NamespaceType.TYPES_FILE or (
259
- serialize_namespace_type == NamespaceType.MODEL and called_by_property
260
- ):
261
- file_import.add_submodule_import(
262
- relative_path,
263
- "models",
264
- ImportType.LOCAL,
265
- alias=alias,
266
- typing_section=TypingSection.TYPING,
267
- )
287
+ serialize_namespace = kwargs.get("serialize_namespace", self.code_model.namespace)
288
+ relative_path = self.code_model.get_relative_import_path(serialize_namespace, self.client_namespace)
289
+ alias = self.code_model.get_unique_models_alias(serialize_namespace, self.client_namespace)
290
+ serialize_namespace_type = kwargs.get("serialize_namespace_type")
291
+ called_by_property = kwargs.get("called_by_property", False)
292
+ if serialize_namespace_type in [NamespaceType.OPERATION, NamespaceType.CLIENT]:
293
+ file_import.add_submodule_import(
294
+ relative_path,
295
+ "models",
296
+ ImportType.LOCAL,
297
+ alias=alias,
298
+ typing_section=TypingSection.REGULAR,
299
+ )
300
+ elif serialize_namespace_type == NamespaceType.TYPES_FILE:
301
+ # Import enum name directly to avoid dotted forward refs in TypedDict annotations
302
+ file_import.add_submodule_import(
303
+ f"{relative_path}models" if relative_path != "." else ".models",
304
+ self.name,
305
+ ImportType.LOCAL,
306
+ typing_section=TypingSection.TYPING,
307
+ )
308
+ elif serialize_namespace_type == NamespaceType.UNIONS_FILE or (
309
+ serialize_namespace_type == NamespaceType.MODEL and called_by_property
310
+ ):
311
+ file_import.add_submodule_import(
312
+ relative_path,
313
+ "models",
314
+ ImportType.LOCAL,
315
+ alias=alias,
316
+ typing_section=TypingSection.TYPING,
317
+ )
268
318
 
269
319
  file_import.merge(self.value_type.imports(**kwargs))
270
320
  return file_import
@@ -6,6 +6,7 @@
6
6
  from typing import Any, Optional, Union, TYPE_CHECKING
7
7
  from .base import BaseType
8
8
  from .imports import FileImport
9
+ from .utils import NamespaceType
9
10
 
10
11
  if TYPE_CHECKING:
11
12
  from .code_model import CodeModel
@@ -41,12 +42,17 @@ class ListType(BaseType):
41
42
  # this means we're version tolerant XML, we just return the XML element
42
43
  return self.element_type.type_annotation(**kwargs)
43
44
 
44
- # if there is a function/property named `list` we have to make sure there's no conflict with the built-in `list`
45
+ # if there is a function named `list` we have to make sure there's no conflict with the built-in `list`
46
+ # in operation files. The operation_groups_serializer defines `List = list` alias for this case.
47
+ serialize_namespace_type = kwargs.get("serialize_namespace_type")
45
48
  is_operation_file = kwargs.get("is_operation_file", False)
46
- use_list_import = (self.code_model.has_operation_named_list and is_operation_file) or (
47
- self.code_model.has_property_named_list and not is_operation_file
49
+ in_operation_context = (
50
+ serialize_namespace_type in (NamespaceType.OPERATION, NamespaceType.CLIENT) or is_operation_file
48
51
  )
49
- list_type = "List" if use_list_import else "list"
52
+ if in_operation_context and self.code_model.has_operation_named_list:
53
+ list_type = "List"
54
+ else:
55
+ list_type = "list"
50
56
  return f"{list_type}[{self.element_type.type_annotation(**kwargs)}]"
51
57
 
52
58
  def description(self, *, is_operation_file: bool) -> str:
@@ -77,11 +77,23 @@ class ModelType(BaseType): # pylint: disable=too-many-instance-attributes, too-
77
77
  self.cross_language_definition_id: Optional[str] = self.yaml_data.get("crossLanguageDefinitionId")
78
78
  self.usage: int = self.yaml_data.get("usage", UsageFlags.Input.value | UsageFlags.Output.value)
79
79
  self.client_namespace: str = self.yaml_data.get("clientNamespace", code_model.namespace)
80
+ self.is_typed_dict_only: bool = (
81
+ self.yaml_data.get("typedDictOnly", False) or code_model.options["models-mode"] == "typeddict"
82
+ )
80
83
 
81
84
  @property
82
85
  def is_usage_output(self) -> bool:
83
86
  return bool(self.usage & UsageFlags.Output.value)
84
87
 
88
+ @property
89
+ def is_usage_input(self) -> bool:
90
+ return bool(self.usage & UsageFlags.Input.value)
91
+
92
+ @property
93
+ def is_used_in_operations_via_types(self) -> bool:
94
+ """Whether this model would be imported from types.py (not models) in operations."""
95
+ return False
96
+
85
97
  @property
86
98
  def flattened_property(self) -> Optional[Property]:
87
99
  try:
@@ -275,12 +287,20 @@ class GeneratedModelType(ModelType):
275
287
  def type_annotation(self, **kwargs: Any) -> str:
276
288
  is_operation_file = kwargs.pop("is_operation_file", False)
277
289
  skip_quote = kwargs.get("skip_quote", False)
290
+ serialize_namespace_type = kwargs.get("serialize_namespace_type")
278
291
  module_name = ""
279
- if kwargs.get("need_model_alias", True):
292
+ # In types.py, use bare name to avoid pyright "variable in type expression" errors
293
+ if serialize_namespace_type == NamespaceType.TYPES_FILE:
294
+ pass # no module prefix, no internal file prefix
295
+ elif kwargs.get("need_model_alias", True):
280
296
  serialize_namespace = kwargs.get("serialize_namespace", self.code_model.namespace)
281
297
  model_alias = self.code_model.get_unique_models_alias(serialize_namespace, self.client_namespace)
282
298
  module_name = f"{model_alias}."
283
- file_name = f"{self.code_model.models_filename}." if self.internal else ""
299
+ file_name = (
300
+ f"{self.code_model.models_filename}."
301
+ if self.internal and serialize_namespace_type != NamespaceType.TYPES_FILE
302
+ else ""
303
+ )
284
304
  retval = module_name + file_name + self.name
285
305
  return retval if is_operation_file or skip_quote else f'"{retval}"'
286
306
 
@@ -305,7 +325,7 @@ class GeneratedModelType(ModelType):
305
325
  alias = self.code_model.get_unique_models_alias(serialize_namespace, self.client_namespace)
306
326
  serialize_namespace_type = kwargs.get("serialize_namespace_type")
307
327
  called_by_property = kwargs.get("called_by_property", False)
308
- # add import for models in operations or _types file
328
+ # add import for models in operations, types, or unions file
309
329
  if serialize_namespace_type in [NamespaceType.OPERATION, NamespaceType.CLIENT]:
310
330
  file_import.add_submodule_import(
311
331
  relative_path,
@@ -320,7 +340,33 @@ class GeneratedModelType(ModelType):
320
340
  ImportType.LOCAL,
321
341
  alias="_Model",
322
342
  )
323
- elif serialize_namespace_type == NamespaceType.TYPES_FILE or (
343
+ elif serialize_namespace_type == NamespaceType.TYPES_FILE:
344
+ # Don't import models that will be defined in this namespace's types.py —
345
+ # either as TypedDict classes (non-discriminated) or as Union aliases (discriminated bases).
346
+ # Only same-namespace non-json models are in the same types.py file.
347
+ same_namespace = relative_path == "."
348
+ will_be_in_types_file = self.base != "json" and same_namespace
349
+ if not will_be_in_types_file:
350
+ if same_namespace:
351
+ # json models from same namespace — import from .models (or .models._models for internal)
352
+ import_path = f".models.{self.code_model.models_filename}" if self.internal else ".models"
353
+ file_import.add_submodule_import(
354
+ import_path,
355
+ self.name,
356
+ ImportType.LOCAL,
357
+ typing_section=TypingSection.TYPING,
358
+ )
359
+ else:
360
+ # Cross-namespace model — import from sibling namespace's types module
361
+ file_import.add_submodule_import(
362
+ self.code_model.get_relative_import_path(
363
+ serialize_namespace, self.client_namespace, module_name="types"
364
+ ),
365
+ self.name,
366
+ ImportType.LOCAL,
367
+ typing_section=TypingSection.TYPING,
368
+ )
369
+ elif serialize_namespace_type == NamespaceType.UNIONS_FILE or (
324
370
  serialize_namespace_type == NamespaceType.MODEL and called_by_property
325
371
  ):
326
372
  file_import.add_submodule_import(
@@ -352,6 +398,33 @@ class MsrestModelType(GeneratedModelType):
352
398
  class DPGModelType(GeneratedModelType):
353
399
  base = "dpg"
354
400
 
401
+ @property
402
+ def is_used_in_operations_via_types(self) -> bool:
403
+ return self.is_typed_dict_only
404
+
405
+ def type_annotation(self, **kwargs: Any) -> str:
406
+ if self.is_typed_dict_only:
407
+ is_operation_file = kwargs.pop("is_operation_file", False)
408
+ skip_quote = kwargs.get("skip_quote", False)
409
+ serialize_namespace_type = kwargs.get("serialize_namespace_type")
410
+ # Within types.py, use bare name (no module prefix)
411
+ if serialize_namespace_type == NamespaceType.TYPES_FILE:
412
+ retval = self.name
413
+ else:
414
+ serialize_namespace = kwargs.get("serialize_namespace", self.code_model.namespace)
415
+ types_alias = self.code_model.get_unique_types_alias(serialize_namespace, self.client_namespace)
416
+ retval = f"{types_alias}.{self.name}"
417
+ return retval if is_operation_file or skip_quote else f'"{retval}"'
418
+ return super().type_annotation(**kwargs)
419
+
420
+ def docstring_type(self, **kwargs: Any) -> str:
421
+ if self.is_typed_dict_only:
422
+ client_namespace = self.client_namespace
423
+ if self.code_model.options.get("generation-subdir"):
424
+ client_namespace += f".{self.code_model.options['generation-subdir']}"
425
+ return f"~{client_namespace}.types.{self.name}"
426
+ return super().docstring_type(**kwargs)
427
+
355
428
  def serialization_type(self, **kwargs: Any) -> str:
356
429
  return (
357
430
  self.type_annotation(skip_quote=True, **kwargs)
@@ -364,7 +437,79 @@ class DPGModelType(GeneratedModelType):
364
437
  return "isinstance({}, " + f"_models.{self.name})"
365
438
 
366
439
  def imports(self, **kwargs: Any) -> FileImport:
440
+ if self.is_typed_dict_only:
441
+ file_import = FileImport(self.code_model)
442
+ serialize_namespace_type = kwargs.get("serialize_namespace_type")
443
+ serialize_namespace = kwargs.get("serialize_namespace", self.code_model.namespace)
444
+ relative_path = self.code_model.get_relative_import_path(serialize_namespace, self.client_namespace)
445
+ alias = self.code_model.get_unique_types_alias(serialize_namespace, self.client_namespace)
446
+ same_namespace = relative_path == "."
447
+ if serialize_namespace_type in [NamespaceType.OPERATION, NamespaceType.CLIENT]:
448
+ file_import.add_submodule_import(
449
+ relative_path,
450
+ "types",
451
+ ImportType.LOCAL,
452
+ alias=alias,
453
+ )
454
+ elif serialize_namespace_type == NamespaceType.TYPES_FILE and same_namespace:
455
+ pass # model is defined in this types.py — no import needed
456
+ elif serialize_namespace_type in [NamespaceType.TYPES_FILE, NamespaceType.UNIONS_FILE] or (
457
+ serialize_namespace_type == NamespaceType.MODEL and kwargs.get("called_by_property", False)
458
+ ):
459
+ file_import.add_submodule_import(
460
+ relative_path,
461
+ "types",
462
+ ImportType.LOCAL,
463
+ alias=alias,
464
+ typing_section=TypingSection.TYPING,
465
+ )
466
+ return file_import
367
467
  file_import = super().imports(**kwargs)
368
468
  if self.flattened_property:
369
469
  file_import.add_submodule_import("typing", "Any", ImportType.STDLIB)
370
470
  return file_import
471
+
472
+
473
+ class TypedDictModelType(DPGModelType):
474
+ base = "typeddict"
475
+
476
+ @property
477
+ def is_used_in_operations_via_types(self) -> bool:
478
+ return True
479
+
480
+ def type_annotation(self, **kwargs: Any) -> str:
481
+ is_operation_file = kwargs.pop("is_operation_file", False)
482
+ skip_quote = kwargs.get("skip_quote", False)
483
+ serialize_namespace_type = kwargs.get("serialize_namespace_type")
484
+ if serialize_namespace_type == NamespaceType.TYPES_FILE:
485
+ retval = self.name
486
+ else:
487
+ serialize_namespace = kwargs.get("serialize_namespace", self.code_model.namespace)
488
+ types_alias = self.code_model.get_unique_types_alias(serialize_namespace, self.client_namespace)
489
+ retval = f"{types_alias}.{self.name}"
490
+ return retval if is_operation_file or skip_quote else f'"{retval}"'
491
+
492
+ def docstring_type(self, **kwargs: Any) -> str:
493
+ client_namespace = self.client_namespace
494
+ if self.code_model.options.get("generation-subdir"):
495
+ client_namespace += f".{self.code_model.options['generation-subdir']}"
496
+ return f"~{client_namespace}.types.{self.name}"
497
+
498
+ @property
499
+ def instance_check_template(self) -> str:
500
+ return "isinstance({}, MutableMapping)"
501
+
502
+ def imports(self, **kwargs: Any) -> FileImport:
503
+ file_import = FileImport(self.code_model)
504
+ serialize_namespace_type = kwargs.get("serialize_namespace_type")
505
+ serialize_namespace = kwargs.get("serialize_namespace", self.code_model.namespace)
506
+ relative_path = self.code_model.get_relative_import_path(serialize_namespace, self.client_namespace)
507
+ alias = self.code_model.get_unique_types_alias(serialize_namespace, self.client_namespace)
508
+ if serialize_namespace_type in [NamespaceType.OPERATION, NamespaceType.CLIENT]:
509
+ file_import.add_submodule_import(
510
+ relative_path,
511
+ "types",
512
+ ImportType.LOCAL,
513
+ alias=alias,
514
+ )
515
+ return file_import
@@ -458,6 +458,7 @@ class OperationBase( # pylint: disable=too-many-public-methods,too-many-instanc
458
458
  r.type
459
459
  and not isinstance(r.type, BinaryIteratorType)
460
460
  and not xml_serializable(str(r.default_content_type))
461
+ and not (isinstance(r.type, ModelType) and r.type.is_typed_dict_only)
461
462
  for r in self.responses
462
463
  ):
463
464
  file_import.add_submodule_import(relative_path, "_deserialize", ImportType.LOCAL)
@@ -186,7 +186,11 @@ class PagingOperationBase(OperationBase[PagingResponseType]):
186
186
  serialize_namespace, module_name="_utils.model_base"
187
187
  )
188
188
  file_import.merge(self.item_type.imports(**kwargs))
189
- if self.default_error_deserialization(serialize_namespace) or self.need_deserialize:
189
+ need_deserialize_import = self.default_error_deserialization(serialize_namespace) or (
190
+ self.need_deserialize
191
+ and not (isinstance(self.item_type, ModelType) and self.item_type.is_typed_dict_only)
192
+ )
193
+ if need_deserialize_import:
190
194
  file_import.add_submodule_import(relative_path, "_deserialize", ImportType.LOCAL)
191
195
  if self.is_xml_paging:
192
196
  file_import.add_submodule_import("xml.etree", "ElementTree", ImportType.STDLIB, alias="ET")