@typespec/http-client-python 0.34.0 → 0.34.2

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 (42) 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/utils.d.ts +10 -0
  15. package/dist/emitter/utils.d.ts.map +1 -1
  16. package/dist/emitter/utils.js +12 -0
  17. package/dist/emitter/utils.js.map +1 -1
  18. package/emitter/src/emitter.ts +2 -14
  19. package/emitter/src/external-process.ts +18 -1
  20. package/emitter/src/lib.ts +1 -1
  21. package/emitter/src/node-runner.ts +2 -1
  22. package/emitter/src/utils.ts +13 -0
  23. package/emitter/temp/tsconfig.tsbuildinfo +1 -1
  24. package/emitter/test/external-process.test.ts +28 -0
  25. package/emitter/test/utils.test.ts +12 -1
  26. package/eng/scripts/setup/__pycache__/package_manager.cpython-311.pyc +0 -0
  27. package/eng/scripts/setup/__pycache__/venvtools.cpython-311.pyc +0 -0
  28. package/generator/build/lib/pygen/__init__.py +2 -0
  29. package/generator/build/lib/pygen/codegen/models/enum_type.py +7 -0
  30. package/generator/build/lib/pygen/codegen/models/model_type.py +4 -0
  31. package/generator/build/lib/pygen/codegen/serializers/types_serializer.py +105 -12
  32. package/generator/build/lib/pygen/codegen/templates/model_base.py.jinja2 +10 -9
  33. package/generator/build/lib/pygen/preprocess/__init__.py +55 -17
  34. package/generator/component-detection-pip-report.json +9 -9
  35. package/generator/dist/pygen-0.1.0-py3-none-any.whl +0 -0
  36. package/generator/pygen/__init__.py +2 -0
  37. package/generator/pygen/codegen/models/enum_type.py +7 -0
  38. package/generator/pygen/codegen/models/model_type.py +4 -0
  39. package/generator/pygen/codegen/serializers/types_serializer.py +105 -12
  40. package/generator/pygen/codegen/templates/model_base.py.jinja2 +10 -9
  41. package/generator/pygen/preprocess/__init__.py +55 -17
  42. package/package.json +33 -33
@@ -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
  });
@@ -45,6 +45,8 @@ class OptionsDict(MutableMapping):
45
45
 
46
46
  def __init__(self, options: Optional[dict[str, Any]] = None) -> None:
47
47
  self._data = options.copy() if options else {}
48
+ for key in list(self._data):
49
+ self._data[key] = self._validate_and_transform(key, self._data[key])
48
50
  self._validate_combinations()
49
51
 
50
52
  def __getitem__(self, key: str) -> Any: # pylint: disable=too-many-return-statements
@@ -40,6 +40,10 @@ 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":
44
+ # A single constant enum value must be
45
+ # annotated with its literal value directly (e.g. ``Literal["red"]``).
46
+ return f"Literal[{self.value_type.get_declaration(self.value)}]"
43
47
  return f"Literal[{self.enum_type.name}.{self.name}]"
44
48
 
45
49
  def get_declaration(self, value=None):
@@ -79,6 +83,9 @@ class EnumValue(BaseType):
79
83
  file_import = FileImport(self.code_model)
80
84
  file_import.merge(self.value_type.imports(**kwargs))
81
85
  file_import.add_submodule_import("typing", "Literal", ImportType.STDLIB, TypingSection.REGULAR)
86
+ if self.code_model.options["models-mode"] == "typeddict":
87
+ # In typeddict mode the enums module (``_enums.py``) is never generated
88
+ return file_import
82
89
  serialize_namespace = kwargs.get("serialize_namespace", self.code_model.namespace)
83
90
  file_import.add_submodule_import(
84
91
  self.code_model.get_relative_import_path(
@@ -85,6 +85,10 @@ class ModelType(BaseType): # pylint: disable=too-many-instance-attributes, too-
85
85
  def is_usage_output(self) -> bool:
86
86
  return bool(self.usage & UsageFlags.Output.value)
87
87
 
88
+ @property
89
+ def is_usage_input(self) -> bool:
90
+ return bool(self.usage & UsageFlags.Input.value)
91
+
88
92
  @property
89
93
  def is_used_in_operations_via_types(self) -> bool:
90
94
  """Whether this model would be imported from types.py (not models) in operations."""
@@ -5,7 +5,8 @@
5
5
  # --------------------------------------------------------------------------
6
6
  import keyword
7
7
  import re
8
- from typing import Optional
8
+ from functools import cached_property
9
+ from typing import Any, Optional
9
10
  from ..models import ModelType, CodeModel
10
11
  from ..models.enum_type import EnumType
11
12
  from ..models.imports import FileImport, ImportType
@@ -71,28 +72,114 @@ class TypesSerializer(BaseSerializer):
71
72
  values = [enum.get_declaration(v.value) for v in enum.values]
72
73
  return f"{enum.name} = Literal[{', '.join(values)}]"
73
74
 
75
+ @staticmethod
76
+ def _renders_as_input_typeddict(m: "ModelType") -> bool:
77
+ """Whether a non-json model is a *seed* for the types.py input surface.
78
+
79
+ TypedDicts (and discriminated-base union aliases) in ``types.py`` describe request-body
80
+ (*input*) shapes. Output-only models already render as classes in ``models/`` and are
81
+ referenced via ``_models.*``, so a response-only model that nothing input references would be
82
+ dead code. A model seeds the input surface when it is:
83
+
84
+ * a typeddict copy (``base == "typeddict"``) — these only exist as input body overloads
85
+ (their ``usage`` may carry ``Spread``/``Json`` rather than ``Input``), or
86
+ * ``is_typed_dict_only`` — includes every model in full ``typeddict`` mode (responses too),
87
+ and input-only anonymous bodies, or
88
+ * used as input (``is_usage_input``) — e.g. a model shared between request and response.
89
+
90
+ The full set rendered in types.py is the transitive closure of these seeds over base
91
+ classes, discriminated subtypes and property types (see :attr:`_types_file_model_names`), so
92
+ an output-only model that *is* referenced by an input model (e.g. ARM ``SystemData`` on
93
+ ``Resource``) is still rendered.
94
+ """
95
+ return m.base == "typeddict" or m.is_typed_dict_only or m.is_usage_input
96
+
97
+ @staticmethod
98
+ def _iter_referenced_models(base_type: Any):
99
+ """Yield ModelType instances directly referenced by a type, recursing through containers.
100
+
101
+ Handles list/dict ``element_type``, constant/enum ``value_type`` and combined ``types``.
102
+ A referenced ModelType is yielded but not descended into — the closure walk descends into a
103
+ model's own properties/parents/subtypes when that model is itself visited.
104
+ """
105
+ stack = [base_type]
106
+ seen: set[int] = set()
107
+ while stack:
108
+ t = stack.pop()
109
+ if t is None or id(t) in seen:
110
+ continue
111
+ seen.add(id(t))
112
+ if isinstance(t, ModelType):
113
+ yield t
114
+ continue
115
+ for attr in ("element_type", "value_type"):
116
+ child = getattr(t, attr, None)
117
+ if child is not None:
118
+ stack.append(child)
119
+ for child in getattr(t, "types", []) or []:
120
+ stack.append(child)
121
+
122
+ @cached_property
123
+ def _types_file_model_names(self) -> set[str]:
124
+ """Names of every model that must be rendered in types.py.
125
+
126
+ Starts from the input seeds (:meth:`_renders_as_input_typeddict`) and takes the transitive
127
+ closure over base classes, discriminated subtypes and property types. Keyed on model
128
+ ``name`` (dpg models and their typeddict copies share a name and render as one TypedDict), so
129
+ the result is stable regardless of which copy a reference points at.
130
+
131
+ Cached: the closure is a full walk over the model graph and is read several times per file
132
+ (via :attr:`typeddict_models` and :attr:`discriminated_base_models`). ``self._models`` is set
133
+ once at construction and never mutated, so memoizing on the instance is safe.
134
+ """
135
+ needed: set[str] = set()
136
+ stack = [m for m in self._models if m.base != "json" and self._renders_as_input_typeddict(m)]
137
+ while stack:
138
+ m = stack.pop()
139
+ if m.base == "json" or m.name in needed:
140
+ continue
141
+ needed.add(m.name)
142
+ stack.extend(m.parents)
143
+ stack.extend(m.discriminated_subtypes.values())
144
+ for prop in m.properties:
145
+ stack.extend(self._iter_referenced_models(prop.type))
146
+ return needed
147
+
74
148
  @property
75
149
  def typeddict_models(self) -> list[ModelType]:
76
150
  """Models that should be rendered as TypedDicts (excluding discriminated bases which become unions).
77
151
 
78
- When both a dpg model and its typeddict copy exist (same crossLanguageDefinitionId),
152
+ When both a dpg model and its typeddict copy exist for the same model,
79
153
  prefer the dpg model (it already renders as a TypedDict in types.py) and skip the copy.
154
+
155
+ The pairing is keyed on the model ``name`` (the copy is a shallow copy of the source, so it
156
+ shares the source's name). ``crossLanguageDefinitionId`` cannot be used here: template
157
+ instantiated models such as ``ResourceUpdateModel<Foo, FooProperties>`` all share the
158
+ template's cross-language id, so keying on it would wrongly collapse distinct models
159
+ (e.g. ``CacheUpdate`` and ``VolumeUpdate``) into one and drop the rest from types.py.
160
+
161
+ Only models in the input-surface closure (:attr:`_types_file_model_names`) are rendered, so
162
+ response-only models (e.g. ``GetResponse``) are dropped while models reachable from an input
163
+ model — including output-only ones such as a discriminated subtype or an ARM ``SystemData``
164
+ property — are kept, ensuring no forward reference is left undefined.
80
165
  """
81
- candidates = [m for m in self._models if m.base != "json" and not m.discriminated_subtypes]
82
- seen_ids: dict[str, "ModelType"] = {}
166
+ needed = self._types_file_model_names
167
+ candidates = [
168
+ m for m in self._models if m.base != "json" and not m.discriminated_subtypes and m.name in needed
169
+ ]
170
+ seen_names: dict[str, "ModelType"] = {}
83
171
  result: list["ModelType"] = []
84
172
  for m in candidates:
85
- clid = m.yaml_data.get("crossLanguageDefinitionId")
86
- if clid and clid in seen_ids:
173
+ name = m.name
174
+ if name in seen_names:
87
175
  # Prefer the dpg model over the typeddict copy
88
- if m.base == "dpg" and seen_ids[clid].base == "typeddict":
176
+ if m.base == "dpg" and seen_names[name].base == "typeddict":
89
177
  # Replace the typeddict copy with the dpg model
90
- result = [r if r is not seen_ids[clid] else m for r in result]
91
- seen_ids[clid] = m
178
+ result = [r if r is not seen_names[name] else m for r in result]
179
+ seen_names[name] = m
92
180
  # Otherwise skip this duplicate
93
181
  continue
94
- if clid:
95
- seen_ids[clid] = m
182
+ seen_names[name] = m
96
183
  result.append(m)
97
184
  return result
98
185
 
@@ -100,10 +187,16 @@ class TypesSerializer(BaseSerializer):
100
187
  def discriminated_base_models(self) -> list[ModelType]:
101
188
  """Discriminated base models that become Union type aliases in types.py.
102
189
 
190
+ Only bases in the input-surface closure (:attr:`_types_file_model_names`) are emitted: an
191
+ output-only ``Dinosaur = Union[TRex]`` alias is dead code and would reference subtype
192
+ TypedDicts that are themselves (correctly) omitted from types.py, causing a ``NameError`` at
193
+ import time.
194
+
103
195
  Topologically sorted so that nested discriminated bases (e.g. Shark)
104
196
  are defined before their parents (e.g. Fish = Union[Salmon, Shark]).
105
197
  """
106
- bases = [m for m in self._models if m.base != "json" and m.discriminated_subtypes]
198
+ needed = self._types_file_model_names
199
+ bases = [m for m in self._models if m.base != "json" and m.discriminated_subtypes and m.name in needed]
107
200
  base_names = {m.name for m in bases}
108
201
  sorted_bases: list[ModelType] = []
109
202
  visited: set[str] = set()
@@ -470,21 +470,21 @@ class _MyMutableMapping(MutableMapping[str, typing.Any]):
470
470
 
471
471
  def keys(self) -> typing.KeysView[str]:
472
472
  """
473
- :returns: a set-like object providing a view on D's keys
473
+ :returns: a set-like object providing a view on the mapping's keys
474
474
  :rtype: ~typing.KeysView
475
475
  """
476
476
  return self._data.keys()
477
477
 
478
478
  def values(self) -> typing.ValuesView[typing.Any]:
479
479
  """
480
- :returns: an object providing a view on D's values
480
+ :returns: an object providing a view on the mapping's values
481
481
  :rtype: ~typing.ValuesView
482
482
  """
483
483
  return self._data.values()
484
484
 
485
485
  def items(self) -> typing.ItemsView[str, typing.Any]:
486
486
  """
487
- :returns: set-like object providing a view on D's items
487
+ :returns: a set-like object providing a view on the mapping's items
488
488
  :rtype: ~typing.ItemsView
489
489
  """
490
490
  return self._data.items()
@@ -494,7 +494,7 @@ class _MyMutableMapping(MutableMapping[str, typing.Any]):
494
494
  Get the value for key if key is in the dictionary, else default.
495
495
  :param str key: The key to look up.
496
496
  :param any default: The value to return if key is not in the dictionary. Defaults to None
497
- :returns: D[k] if k in D, else d.
497
+ :returns: The value for key if key is in the dictionary, else default.
498
498
  :rtype: any
499
499
  """
500
500
  try:
@@ -529,19 +529,19 @@ class _MyMutableMapping(MutableMapping[str, typing.Any]):
529
529
  Removes and returns some (key, value) pair
530
530
  :returns: The (key, value) pair.
531
531
  :rtype: tuple
532
- :raises KeyError: if D is empty.
532
+ :raises KeyError: if the dictionary is empty.
533
533
  """
534
534
  return self._data.popitem()
535
535
 
536
536
  def clear(self) -> None:
537
537
  """
538
- Remove all items from D.
538
+ Remove all items from the dictionary.
539
539
  """
540
540
  self._data.clear()
541
541
 
542
542
  def update(self, *args: typing.Any, **kwargs: typing.Any) -> None: # pylint: disable=arguments-differ
543
543
  """
544
- Updates D from mapping/iterable E and F.
544
+ Update the dictionary from a mapping or an iterable of key-value pairs.
545
545
  :param any args: Either a mapping object or an iterable of key-value pairs.
546
546
  """
547
547
  self._data.update(*args, **kwargs)
@@ -554,10 +554,11 @@ class _MyMutableMapping(MutableMapping[str, typing.Any]):
554
554
 
555
555
  def setdefault(self, key: str, default: typing.Any = _UNSET) -> typing.Any:
556
556
  """
557
- Same as calling D.get(k, d), and setting D[k]=d if k not found
557
+ Return the value for key if key is in the dictionary; otherwise set the key to
558
+ default and return default.
558
559
  :param str key: The key to look up.
559
560
  :param any default: The value to set if key is not in the dictionary
560
- :returns: D[k] if k in D, else d.
561
+ :returns: The value for key if key is in the dictionary, else default.
561
562
  :rtype: any
562
563
  """
563
564
  if default is _UNSET:
@@ -307,8 +307,18 @@ class PreProcessPlugin(YamlUpdatePlugin):
307
307
  return self.options.get("tsp_file", False)
308
308
 
309
309
  @staticmethod
310
- def _find_existing_typeddict(code_model: dict[str, Any], cross_lang_id: Optional[str]) -> Optional[dict[str, Any]]:
311
- """Find an existing typeddict copy with the given crossLanguageDefinitionId."""
310
+ def _find_existing_typeddict(
311
+ code_model: dict[str, Any],
312
+ cross_lang_id: Optional[str],
313
+ name: Optional[str] = None,
314
+ ) -> Optional[dict[str, Any]]:
315
+ """Find an existing typeddict copy for the given model.
316
+
317
+ Matches on both ``crossLanguageDefinitionId`` and ``name``. The name is required because
318
+ template-instantiated models (e.g. ``ResourceUpdateModel<Foo, FooProperties>``) all share
319
+ the template's cross-language id, so matching on the id alone would reuse one model's copy
320
+ (e.g. ``CacheUpdate``) for a different model (e.g. ``VolumeUpdate``).
321
+ """
312
322
  if not cross_lang_id:
313
323
  return None
314
324
  return next(
@@ -318,10 +328,50 @@ class PreProcessPlugin(YamlUpdatePlugin):
318
328
  if t.get("type") == "model"
319
329
  and t.get("base") == "typeddict"
320
330
  and t.get("crossLanguageDefinitionId") == cross_lang_id
331
+ and (name is None or t.get("name") == name)
321
332
  ),
322
333
  None,
323
334
  )
324
335
 
336
+ @staticmethod
337
+ def _find_spread_original(code_model: dict[str, Any], json_model: dict[str, Any]) -> Optional[dict[str, Any]]:
338
+ """Recover the dpg model that a spread (json) body was cloned from.
339
+
340
+ When a spread body type is also used elsewhere, the emitter clones it, renames the clone to
341
+ ``<Method>Request`` and sets ``base = "json"`` while keeping the original
342
+ ``crossLanguageDefinitionId``. To reference the real model's TypedDict we look the clone up
343
+ by that id.
344
+
345
+ Distinct template-instantiated models share a single ``crossLanguageDefinitionId``, so an id
346
+ match alone is ambiguous. We only reuse an original when the choice is unambiguous:
347
+
348
+ * a dpg candidate whose ``name`` equals the json model's name (the body was not renamed), or
349
+ * exactly one dpg candidate carries the id.
350
+
351
+ Otherwise we return ``None`` so the caller falls back to the json model itself, avoiding a
352
+ reference to the wrong model's TypedDict.
353
+ """
354
+ cross_lang_id = json_model.get("crossLanguageDefinitionId")
355
+ if not cross_lang_id:
356
+ return None
357
+ candidates = [
358
+ t
359
+ for t in code_model["types"]
360
+ if t.get("type") == "model"
361
+ and t.get("base") == "dpg"
362
+ and t.get("crossLanguageDefinitionId") == cross_lang_id
363
+ and t is not json_model
364
+ ]
365
+ if not candidates:
366
+ return None
367
+ name = json_model.get("name")
368
+ for candidate in candidates:
369
+ if candidate.get("name") == name:
370
+ return candidate
371
+ if len(candidates) == 1:
372
+ return candidates[0]
373
+ return None
374
+
325
375
  @staticmethod
326
376
  def _insert_typeddict_overload(
327
377
  code_model: dict[str, Any],
@@ -376,25 +426,14 @@ class PreProcessPlugin(YamlUpdatePlugin):
376
426
  # Add typeddict overload for non-spread dpg models
377
427
  if self.options["models-mode"] == "dpg" and is_dpg_model:
378
428
  cross_lang_id = model_type.get("crossLanguageDefinitionId")
379
- existing_td = self._find_existing_typeddict(code_model, cross_lang_id)
429
+ existing_td = self._find_existing_typeddict(code_model, cross_lang_id, model_type.get("name"))
380
430
  self._insert_typeddict_overload(code_model, body_parameter, model_type, origin_type, existing_td)
381
431
 
382
432
  # For spread bodies (json base), add a typeddict overload that references
383
433
  # the original model. This replaces the JSON single-body overload.
384
434
  if is_json_model:
385
435
  cross_lang_id = model_type.get("crossLanguageDefinitionId")
386
- original = None
387
- if cross_lang_id:
388
- original = next(
389
- (
390
- t
391
- for t in code_model["types"]
392
- if t.get("type") == "model"
393
- and t.get("crossLanguageDefinitionId") == cross_lang_id
394
- and t is not model_type
395
- ),
396
- None,
397
- )
436
+ original = self._find_spread_original(code_model, model_type)
398
437
 
399
438
  if is_typeddict_only and original:
400
439
  # In typeddict-only mode, the original dpg model already renders
@@ -407,7 +446,7 @@ class PreProcessPlugin(YamlUpdatePlugin):
407
446
  body_parameter["type"]["types"].insert(1, td_list_or_dict)
408
447
  else:
409
448
  source = original or model_type
410
- existing_td = self._find_existing_typeddict(code_model, cross_lang_id)
449
+ existing_td = self._find_existing_typeddict(code_model, cross_lang_id, source.get("name"))
411
450
  self._insert_typeddict_overload(code_model, body_parameter, source, origin_type, existing_td)
412
451
 
413
452
  if len(body_parameter["type"]["types"]) == 1:
@@ -420,7 +459,6 @@ class PreProcessPlugin(YamlUpdatePlugin):
420
459
 
421
460
  code_model["types"].append(body_parameter["type"])
422
461
 
423
-
424
462
  def pad_reserved_words(self, name: str, pad_type: PadType, yaml_type: dict[str, Any]) -> str:
425
463
  # we want to pad hidden variables as well
426
464
  if not name:
@@ -4,12 +4,12 @@
4
4
  "install": [
5
5
  {
6
6
  "download_info": {
7
- "url": "https://files.pythonhosted.org/packages/9d/76/f789f7a86709c6b087c5a2f52f911838cad707cc613162401badc665acfe/setuptools-82.0.1-py3-none-any.whl",
7
+ "url": "https://files.pythonhosted.org/packages/5d/40/e1e72872c6354b306daef1703549e8e83b4d43cfea356311bf722a043752/setuptools-83.0.0-py3-none-any.whl",
8
8
  "archive_info": {
9
9
  "hashes": {
10
- "sha256": "a59e362652f08dcd477c78bb6e7bd9d80a7995bc73ce773050228a348ce2e5bb"
10
+ "sha256": "29b23c360f22f414dc7336bb39178cc7bcbf6021ed2733cde173f09dba19abb3"
11
11
  },
12
- "hash": "sha256=a59e362652f08dcd477c78bb6e7bd9d80a7995bc73ce773050228a348ce2e5bb"
12
+ "hash": "sha256=29b23c360f22f414dc7336bb39178cc7bcbf6021ed2733cde173f09dba19abb3"
13
13
  }
14
14
  },
15
15
  "is_direct": false,
@@ -18,12 +18,12 @@
18
18
  "metadata": {
19
19
  "metadata_version": "2.4",
20
20
  "name": "setuptools",
21
- "version": "82.0.1",
21
+ "version": "83.0.0",
22
22
  "dynamic": [
23
23
  "license-file"
24
24
  ],
25
25
  "summary": "Most extensible Python build backend with support for C/C++ extension modules",
26
- "description": ".. |pypi-version| image:: https://img.shields.io/pypi/v/setuptools.svg\n :target: https://pypi.org/project/setuptools\n\n.. |py-version| image:: https://img.shields.io/pypi/pyversions/setuptools.svg\n\n.. |test-badge| image:: https://github.com/pypa/setuptools/actions/workflows/main.yml/badge.svg\n :target: https://github.com/pypa/setuptools/actions?query=workflow%3A%22tests%22\n :alt: tests\n\n.. |ruff-badge| image:: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/charliermarsh/ruff/main/assets/badge/v2.json\n :target: https://github.com/astral-sh/ruff\n :alt: Ruff\n\n.. |docs-badge| image:: https://img.shields.io/readthedocs/setuptools/latest.svg\n :target: https://setuptools.pypa.io\n\n.. |skeleton-badge| image:: https://img.shields.io/badge/skeleton-2025-informational\n :target: https://blog.jaraco.com/skeleton\n\n.. |codecov-badge| image:: https://img.shields.io/codecov/c/github/pypa/setuptools/master.svg?logo=codecov&logoColor=white\n :target: https://codecov.io/gh/pypa/setuptools\n\n.. |tidelift-badge| image:: https://tidelift.com/badges/github/pypa/setuptools?style=flat\n :target: https://tidelift.com/subscription/pkg/pypi-setuptools?utm_source=pypi-setuptools&utm_medium=readme\n\n.. |discord-badge| image:: https://img.shields.io/discord/803025117553754132\n :target: https://discord.com/channels/803025117553754132/815945031150993468\n :alt: Discord\n\n|pypi-version| |py-version| |test-badge| |ruff-badge| |docs-badge| |skeleton-badge| |codecov-badge| |discord-badge|\n\nSee the `Quickstart <https://setuptools.pypa.io/en/latest/userguide/quickstart.html>`_\nand the `User's Guide <https://setuptools.pypa.io/en/latest/userguide/>`_ for\ninstructions on how to use Setuptools.\n\nQuestions and comments should be directed to `GitHub Discussions\n<https://github.com/pypa/setuptools/discussions>`_.\nBug reports and especially tested patches may be\nsubmitted directly to the `bug tracker\n<https://github.com/pypa/setuptools/issues>`_.\n\n\nCode of Conduct\n===============\n\nEveryone interacting in the setuptools project's codebases, issue trackers,\nchat rooms, and fora is expected to follow the\n`PSF Code of Conduct <https://github.com/pypa/.github/blob/main/CODE_OF_CONDUCT.md>`_.\n\n\nFor Enterprise\n==============\n\nAvailable as part of the Tidelift Subscription.\n\nSetuptools and the maintainers of thousands of other packages are working with Tidelift to deliver one enterprise subscription that covers all of the open source you use.\n\n`Learn more <https://tidelift.com/subscription/pkg/pypi-setuptools?utm_source=pypi-setuptools&utm_medium=referral&utm_campaign=github>`_.\n",
26
+ "description": ".. |pypi-version| image:: https://img.shields.io/pypi/v/setuptools.svg\n :target: https://pypi.org/project/setuptools\n\n.. |py-version| image:: https://img.shields.io/pypi/pyversions/setuptools.svg\n\n.. |test-badge| image:: https://github.com/pypa/setuptools/actions/workflows/main.yml/badge.svg\n :target: https://github.com/pypa/setuptools/actions?query=workflow%3A%22tests%22\n :alt: tests\n\n.. |ruff-badge| image:: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/charliermarsh/ruff/main/assets/badge/v2.json\n :target: https://github.com/astral-sh/ruff\n :alt: Ruff\n\n.. |docs-badge| image:: https://img.shields.io/readthedocs/setuptools/latest.svg\n :target: https://setuptools.pypa.io\n\n.. |skeleton-badge| image:: https://img.shields.io/badge/skeleton-2026-informational\n :target: https://blog.jaraco.com/skeleton\n\n.. |codecov-badge| image:: https://img.shields.io/codecov/c/github/pypa/setuptools/master.svg?logo=codecov&logoColor=white\n :target: https://codecov.io/gh/pypa/setuptools\n\n.. |tidelift-badge| image:: https://tidelift.com/badges/github/pypa/setuptools?style=flat\n :target: https://tidelift.com/subscription/pkg/pypi-setuptools?utm_source=pypi-setuptools&utm_medium=readme\n\n.. |discord-badge| image:: https://img.shields.io/discord/803025117553754132\n :target: https://discord.com/channels/803025117553754132/815945031150993468\n :alt: Discord\n\n|pypi-version| |py-version| |test-badge| |ruff-badge| |docs-badge| |skeleton-badge| |codecov-badge| |discord-badge|\n\nSee the `Quickstart <https://setuptools.pypa.io/en/latest/userguide/quickstart.html>`_\nand the `User's Guide <https://setuptools.pypa.io/en/latest/userguide/>`_ for\ninstructions on how to use Setuptools.\n\nQuestions and comments should be directed to `GitHub Discussions\n<https://github.com/pypa/setuptools/discussions>`_.\nBug reports and especially tested patches may be\nsubmitted directly to the `bug tracker\n<https://github.com/pypa/setuptools/issues>`_.\n\n\nCode of Conduct\n===============\n\nEveryone interacting in the setuptools project's codebases, issue trackers,\nchat rooms, and fora is expected to follow the\n`PSF Code of Conduct <https://github.com/pypa/.github/blob/main/CODE_OF_CONDUCT.md>`_.\n\n\nFor Enterprise\n==============\n\nAvailable as part of the Tidelift Subscription.\n\nSetuptools and the maintainers of thousands of other packages are working with Tidelift to deliver one enterprise subscription that covers all of the open source you use.\n\n`Learn more <https://tidelift.com/subscription/pkg/pypi-setuptools?utm_source=pypi-setuptools&utm_medium=referral&utm_campaign=github>`_.\n",
27
27
  "description_content_type": "text/x-rst",
28
28
  "keywords": [
29
29
  "CPAN",
@@ -90,17 +90,17 @@
90
90
  "wheel>=0.43.0; extra == \"core\"",
91
91
  "jaraco.functools>=4; extra == \"core\"",
92
92
  "more_itertools; extra == \"core\"",
93
- "pytest-checkdocs>=2.4; extra == \"check\"",
93
+ "pytest-checkdocs>=2.14; extra == \"check\"",
94
94
  "pytest-ruff>=0.2.1; sys_platform != \"cygwin\" and extra == \"check\"",
95
95
  "ruff>=0.13.0; sys_platform != \"cygwin\" and extra == \"check\"",
96
96
  "pytest-cov; extra == \"cover\"",
97
- "pytest-enabler>=2.2; extra == \"enabler\"",
98
- "pytest-mypy; extra == \"type\"",
97
+ "pytest-enabler>=3.4; extra == \"enabler\"",
98
+ "pytest-mypy>=1.0.1; platform_python_implementation != \"PyPy\" and extra == \"type\"",
99
99
  "mypy==1.18.*; extra == \"type\"",
100
100
  "importlib_metadata>=7.0.2; python_version < \"3.10\" and extra == \"type\"",
101
101
  "jaraco.develop>=7.21; sys_platform != \"cygwin\" and extra == \"type\""
102
102
  ],
103
- "requires_python": ">=3.9",
103
+ "requires_python": ">=3.10",
104
104
  "project_url": [
105
105
  "Source, https://github.com/pypa/setuptools",
106
106
  "Documentation, https://setuptools.pypa.io/",
@@ -45,6 +45,8 @@ class OptionsDict(MutableMapping):
45
45
 
46
46
  def __init__(self, options: Optional[dict[str, Any]] = None) -> None:
47
47
  self._data = options.copy() if options else {}
48
+ for key in list(self._data):
49
+ self._data[key] = self._validate_and_transform(key, self._data[key])
48
50
  self._validate_combinations()
49
51
 
50
52
  def __getitem__(self, key: str) -> Any: # pylint: disable=too-many-return-statements
@@ -40,6 +40,10 @@ 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":
44
+ # A single constant enum value must be
45
+ # annotated with its literal value directly (e.g. ``Literal["red"]``).
46
+ return f"Literal[{self.value_type.get_declaration(self.value)}]"
43
47
  return f"Literal[{self.enum_type.name}.{self.name}]"
44
48
 
45
49
  def get_declaration(self, value=None):
@@ -79,6 +83,9 @@ class EnumValue(BaseType):
79
83
  file_import = FileImport(self.code_model)
80
84
  file_import.merge(self.value_type.imports(**kwargs))
81
85
  file_import.add_submodule_import("typing", "Literal", ImportType.STDLIB, TypingSection.REGULAR)
86
+ if self.code_model.options["models-mode"] == "typeddict":
87
+ # In typeddict mode the enums module (``_enums.py``) is never generated
88
+ return file_import
82
89
  serialize_namespace = kwargs.get("serialize_namespace", self.code_model.namespace)
83
90
  file_import.add_submodule_import(
84
91
  self.code_model.get_relative_import_path(
@@ -85,6 +85,10 @@ class ModelType(BaseType): # pylint: disable=too-many-instance-attributes, too-
85
85
  def is_usage_output(self) -> bool:
86
86
  return bool(self.usage & UsageFlags.Output.value)
87
87
 
88
+ @property
89
+ def is_usage_input(self) -> bool:
90
+ return bool(self.usage & UsageFlags.Input.value)
91
+
88
92
  @property
89
93
  def is_used_in_operations_via_types(self) -> bool:
90
94
  """Whether this model would be imported from types.py (not models) in operations."""