@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
@@ -3,7 +3,7 @@
3
3
  # Licensed under the MIT License. See License.txt in the project root for
4
4
  # license information.
5
5
  # --------------------------------------------------------------------------
6
- from typing import Optional
6
+ from typing import Any, Optional
7
7
  from abc import ABC, abstractmethod
8
8
 
9
9
  from ..models import ModelType, Property, ConstantType, EnumValue, EnumType
@@ -73,12 +73,19 @@ def _get_xml_deserializer_enum_type(prop: Property) -> Optional[EnumType]:
73
73
  return prop_type if isinstance(prop_type, EnumType) else None
74
74
 
75
75
 
76
- def _documentation_string(prop: Property, description_keyword: str, docstring_type_keyword: str) -> list[str]:
76
+ def _documentation_string(
77
+ prop: Property, description_keyword: str, docstring_type_keyword: str, **kwargs: Any
78
+ ) -> list[str]:
77
79
  retval: list[str] = []
78
80
  sphinx_prefix = f":{description_keyword} {prop.client_name}:"
79
81
  description = prop.description(is_operation_file=False).replace("\\", "\\\\")
80
82
  retval.append(f"{sphinx_prefix} {description}" if description else sphinx_prefix)
81
- retval.append(f":{docstring_type_keyword} {prop.client_name}: {prop.type.docstring_type()}")
83
+ # In the types file, use type_annotation (the serialized form) for docstrings
84
+ if kwargs.get("serialize_namespace_type") == NamespaceType.TYPES_FILE:
85
+ doc_type = prop.type.type_annotation(**kwargs)
86
+ else:
87
+ doc_type = prop.type.docstring_type(**kwargs)
88
+ retval.append(f":{docstring_type_keyword} {prop.client_name}: {doc_type}")
82
89
  return retval
83
90
 
84
91
 
@@ -494,3 +501,120 @@ class DpgModelSerializer(_ModelSerializer):
494
501
  if final_result:
495
502
  return "# pylint: disable=" + ", ".join(final_result)
496
503
  return ""
504
+
505
+
506
+ class TypedDictModelSerializer(_ModelSerializer):
507
+ def _is_parent_discriminated_base(self, model: ModelType) -> bool:
508
+ """Check if any parent of this model is a discriminated base (has discriminated_subtypes)."""
509
+ return any(p.discriminated_subtypes for p in model.parents)
510
+
511
+ def _reorder_models(self, models: list[ModelType]) -> list[ModelType]:
512
+ """Reorder so discriminated base Union aliases come after all their subtypes."""
513
+ bases = [m for m in models if m.discriminated_subtypes]
514
+ non_bases = [m for m in models if not m.discriminated_subtypes]
515
+ return non_bases + bases
516
+
517
+ def serialize(self) -> str:
518
+ template = self.env.get_template("model_container.py.jinja2")
519
+ return template.render(
520
+ code_model=self.code_model,
521
+ imports=FileImportSerializer(self.imports()),
522
+ str=str,
523
+ serializer=self,
524
+ models=self._reorder_models(self.models),
525
+ )
526
+
527
+ def imports(self) -> FileImport:
528
+ file_import = FileImport(self.code_model)
529
+ has_required = False
530
+ has_discriminated_union = False
531
+ for model in self.models:
532
+ if model.base == "json":
533
+ continue
534
+ if model.discriminated_subtypes:
535
+ has_discriminated_union = True
536
+ file_import.merge(
537
+ model.imports(
538
+ is_operation_file=False,
539
+ serialize_namespace=self.serialize_namespace,
540
+ serialize_namespace_type=NamespaceType.MODEL,
541
+ )
542
+ )
543
+ for prop in model.properties:
544
+ file_import.merge(
545
+ prop.imports(
546
+ serialize_namespace=self.serialize_namespace,
547
+ serialize_namespace_type=NamespaceType.MODEL,
548
+ called_by_property=True,
549
+ )
550
+ )
551
+ if not (prop.optional or prop.client_default_value is not None):
552
+ has_required = True
553
+ for parent in model.parents:
554
+ if parent.client_namespace != model.client_namespace and not parent.discriminated_subtypes:
555
+ file_import.add_submodule_import(
556
+ self.code_model.get_relative_import_path(
557
+ self.serialize_namespace,
558
+ self.code_model.get_imported_namespace_for_model(parent.client_namespace),
559
+ ),
560
+ parent.name,
561
+ ImportType.LOCAL,
562
+ )
563
+ file_import.add_submodule_import("typing_extensions", "TypedDict", ImportType.STDLIB)
564
+ if has_required:
565
+ file_import.add_submodule_import("typing_extensions", "Required", ImportType.STDLIB)
566
+ if has_discriminated_union:
567
+ file_import.add_submodule_import("typing", "Union", ImportType.STDLIB)
568
+ return file_import
569
+
570
+ def declare_model(self, model: ModelType) -> str:
571
+ # If the model's parent is a discriminated base, don't inherit from it
572
+ non_discriminated_parents = [p for p in model.parents if not p.discriminated_subtypes]
573
+ if non_discriminated_parents:
574
+ basename = ", ".join([m.name for m in non_discriminated_parents])
575
+ return f"class {model.name}({basename}):{model.pylint_disable()}"
576
+ return f"class {model.name}(TypedDict, total=False):{model.pylint_disable()}"
577
+
578
+ @staticmethod
579
+ def get_properties_to_declare(model: ModelType) -> list[Property]:
580
+ # Only exclude inherited properties from non-discriminated parents
581
+ non_discriminated_parents = [p for p in model.parents if not p.discriminated_subtypes]
582
+ if non_discriminated_parents:
583
+ parent_properties = [p for bm in non_discriminated_parents for p in bm.properties]
584
+ properties_to_declare = [
585
+ p
586
+ for p in model.properties
587
+ if not any(
588
+ p.client_name == pp.client_name
589
+ and p.type_annotation() == pp.type_annotation()
590
+ and not p.is_base_discriminator
591
+ for pp in parent_properties
592
+ )
593
+ ]
594
+ else:
595
+ properties_to_declare = model.properties
596
+ return properties_to_declare
597
+
598
+ def declare_property(self, prop: Property) -> str:
599
+ type_annotation = prop.type_annotation(serialize_namespace=self.serialize_namespace)
600
+ is_optional = prop.optional or prop.client_default_value is not None
601
+ if is_optional:
602
+ return f"{prop.wire_name}: {type_annotation}"
603
+ return f"{prop.wire_name}: Required[{type_annotation}]"
604
+
605
+ def initialize_properties(self, model: ModelType) -> list[str]:
606
+ return []
607
+
608
+ def need_init(self, model: ModelType) -> bool:
609
+ return False
610
+
611
+ def discriminated_subtypes_union(self, model: ModelType) -> str:
612
+ subtypes = list(model.discriminated_subtypes.values())
613
+ subtype_names = [s.name for s in subtypes]
614
+ return f"{model.name} = Union[{', '.join(subtype_names)}]"
615
+
616
+ def is_discriminated_base(self, model: ModelType) -> bool:
617
+ return bool(model.discriminated_subtypes)
618
+
619
+ def global_pylint_disables(self) -> str:
620
+ return ""
@@ -3,34 +3,378 @@
3
3
  # Licensed under the MIT License. See License.txt in the project root for
4
4
  # license information.
5
5
  # --------------------------------------------------------------------------
6
+ import keyword
7
+ import re
8
+ from functools import cached_property
9
+ from typing import Any, Optional
10
+ from ..models import ModelType, CodeModel
11
+ from ..models.enum_type import EnumType
6
12
  from ..models.imports import FileImport, ImportType
7
13
  from ..models.utils import NamespaceType
14
+ from ..models.property import Property
15
+ from .model_serializer import _documentation_string
8
16
  from .import_serializer import FileImportSerializer
9
17
  from .base_serializer import BaseSerializer
10
18
 
19
+ # Python builtin type names that can be shadowed by TypedDict field wire_names.
20
+ # When a field name matches one of these, all references to that builtin in type
21
+ # annotations within the same class are qualified as builtins.X.
22
+ _BUILTIN_TYPE_NAMES = frozenset(
23
+ {
24
+ "int",
25
+ "str",
26
+ "float",
27
+ "bool",
28
+ "list",
29
+ "dict",
30
+ "tuple",
31
+ "set",
32
+ "bytes",
33
+ "type",
34
+ "object",
35
+ "complex",
36
+ "frozenset",
37
+ "bytearray",
38
+ "memoryview",
39
+ }
40
+ )
41
+
42
+
43
+ def _qualify_shadowed_builtins(annotation: str, shadowed: frozenset[str]) -> str:
44
+ """Replace bare builtin type references with builtins.X when shadowed by a field name."""
45
+ if not shadowed:
46
+ return annotation
47
+ for name in shadowed:
48
+ annotation = re.sub(rf"\b{name}\b", f"builtins.{name}", annotation)
49
+ return annotation
50
+
11
51
 
12
52
  class TypesSerializer(BaseSerializer):
53
+ def __init__(
54
+ self,
55
+ code_model: CodeModel,
56
+ env,
57
+ client_namespace: Optional[str] = None,
58
+ models: Optional[list[ModelType]] = None,
59
+ enums: Optional[list["EnumType"]] = None,
60
+ ):
61
+ super().__init__(code_model=code_model, env=env, client_namespace=client_namespace)
62
+ self._models = models or []
63
+ self._enums = enums or []
64
+
65
+ @property
66
+ def literal_enums(self) -> list[EnumType]:
67
+ """Enums to render as Literal type aliases in typeddict mode."""
68
+ return sorted(self._enums)
69
+
70
+ def declare_literal_enum(self, enum: EnumType) -> str:
71
+ """Generate a Literal type alias for an enum, e.g. MyColor = Literal["red", "blue"]."""
72
+ values = [enum.get_declaration(v.value) for v in enum.values]
73
+ return f"{enum.name} = Literal[{', '.join(values)}]"
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
+
148
+ @property
149
+ def typeddict_models(self) -> list[ModelType]:
150
+ """Models that should be rendered as TypedDicts (excluding discriminated bases which become unions).
151
+
152
+ When both a dpg model and its typeddict copy exist for the same model,
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.
165
+ """
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"] = {}
171
+ result: list["ModelType"] = []
172
+ for m in candidates:
173
+ name = m.name
174
+ if name in seen_names:
175
+ # Prefer the dpg model over the typeddict copy
176
+ if m.base == "dpg" and seen_names[name].base == "typeddict":
177
+ # Replace the typeddict copy with the dpg model
178
+ result = [r if r is not seen_names[name] else m for r in result]
179
+ seen_names[name] = m
180
+ # Otherwise skip this duplicate
181
+ continue
182
+ seen_names[name] = m
183
+ result.append(m)
184
+ return result
185
+
186
+ @property
187
+ def discriminated_base_models(self) -> list[ModelType]:
188
+ """Discriminated base models that become Union type aliases in types.py.
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
+
195
+ Topologically sorted so that nested discriminated bases (e.g. Shark)
196
+ are defined before their parents (e.g. Fish = Union[Salmon, Shark]).
197
+ """
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]
200
+ base_names = {m.name for m in bases}
201
+ sorted_bases: list[ModelType] = []
202
+ visited: set[str] = set()
203
+
204
+ def visit(model: ModelType) -> None:
205
+ if model.name in visited:
206
+ return
207
+ visited.add(model.name)
208
+ for subtype in model.discriminated_subtypes.values():
209
+ if subtype.name in base_names:
210
+ visit(subtype)
211
+ sorted_bases.append(model)
212
+
213
+ for m in bases:
214
+ visit(m)
215
+ return sorted_bases
216
+
217
+ def discriminated_subtypes_union(self, model: ModelType) -> str:
218
+ """Generate a Union alias for a discriminated base using TypedDict subtype names."""
219
+ subtypes = list(model.discriminated_subtypes.values())
220
+ subtype_names = [s.name for s in subtypes]
221
+ return f"{model.name} = Union[{', '.join(subtype_names)}]"
222
+
223
+ @staticmethod
224
+ def has_keyword_wire_names(model: ModelType) -> bool:
225
+ """Whether any property wire_name is a Python keyword or requires functional TypedDict form."""
226
+ return any(keyword.iskeyword(p.wire_name) or not p.wire_name.isidentifier() for p in model.properties)
227
+
228
+ @staticmethod
229
+ def get_shadowed_builtins(model: ModelType) -> frozenset[str]:
230
+ """Return the set of builtin type names shadowed by property wire_names in this model.
231
+
232
+ Only includes a builtin if it is both used as a wire_name AND referenced
233
+ in a type annotation within the same model (otherwise no shadowing occurs).
234
+ """
235
+ wire_builtins = {p.wire_name for p in model.properties if p.wire_name in _BUILTIN_TYPE_NAMES}
236
+ if not wire_builtins:
237
+ return frozenset()
238
+ # Check which of these builtins actually appear in type annotations
239
+ used = set()
240
+ for prop in model.properties:
241
+ annotation = prop.type_annotation()
242
+ for name in wire_builtins:
243
+ if re.search(rf"\b{name}\b", annotation):
244
+ used.add(name)
245
+ return frozenset(used)
246
+
13
247
  def imports(self) -> FileImport:
14
248
  file_import = FileImport(self.code_model)
15
- if self.code_model.named_unions:
16
- file_import.add_submodule_import(
17
- "typing",
18
- "Union",
19
- ImportType.STDLIB,
20
- )
21
- for nu in self.code_model.named_unions:
22
- file_import.merge(
23
- nu.imports(
24
- serialize_namespace=self.serialize_namespace, serialize_namespace_type=NamespaceType.TYPES_FILE
249
+
250
+ literal_enums = self.literal_enums
251
+ if literal_enums:
252
+ file_import.add_submodule_import("typing", "Literal", ImportType.STDLIB)
253
+
254
+ td_models = self.typeddict_models
255
+ if td_models or self.discriminated_base_models:
256
+ if td_models:
257
+ file_import.add_submodule_import("typing_extensions", "TypedDict", ImportType.STDLIB)
258
+ if self.discriminated_base_models:
259
+ file_import.add_submodule_import("typing", "Union", ImportType.STDLIB)
260
+ has_required = False
261
+ needs_builtins = False
262
+ for model in td_models:
263
+ file_import.merge(
264
+ model.imports(
265
+ is_operation_file=False,
266
+ serialize_namespace=self.serialize_namespace,
267
+ serialize_namespace_type=NamespaceType.TYPES_FILE,
268
+ )
25
269
  )
26
- )
270
+ for prop in model.properties:
271
+ file_import.merge(
272
+ prop.imports(
273
+ serialize_namespace=self.serialize_namespace,
274
+ serialize_namespace_type=NamespaceType.TYPES_FILE,
275
+ called_by_property=True,
276
+ )
277
+ )
278
+ if not (prop.optional or prop.client_default_value is not None):
279
+ has_required = True
280
+ if self.get_shadowed_builtins(model):
281
+ needs_builtins = True
282
+ for parent in model.parents:
283
+ if parent.client_namespace != model.client_namespace and not parent.discriminated_subtypes:
284
+ # Import parent class from sibling namespace's types module
285
+ file_import.add_submodule_import(
286
+ self.code_model.get_relative_import_path(
287
+ self.serialize_namespace,
288
+ parent.client_namespace,
289
+ module_name="types",
290
+ ),
291
+ parent.name,
292
+ ImportType.LOCAL,
293
+ )
294
+ if has_required:
295
+ file_import.add_submodule_import("typing_extensions", "Required", ImportType.STDLIB)
296
+ if needs_builtins:
297
+ file_import.add_import("builtins", ImportType.STDLIB)
27
298
  return file_import
28
299
 
300
+ def declare_model(self, model: ModelType) -> str:
301
+ """Generate the class declaration or functional form for a TypedDict model.
302
+
303
+ Uses functional form when any property wire_name is a Python keyword
304
+ (e.g. 'and', 'class') since keywords can't be identifiers in class bodies.
305
+ """
306
+ if self.has_keyword_wire_names(model):
307
+ return "" # functional form is rendered separately
308
+ non_discriminated_parents = [p for p in model.parents if not p.discriminated_subtypes]
309
+ if non_discriminated_parents:
310
+ basename = ", ".join([m.name for m in non_discriminated_parents])
311
+ return f"class {model.name}({basename}):{model.pylint_disable()}"
312
+ return f"class {model.name}(TypedDict, total=False):{model.pylint_disable()}"
313
+
314
+ def declare_functional_model(self, model: ModelType) -> str:
315
+ """Generate a functional-form TypedDict for models with keyword wire_names.
316
+
317
+ Functional form is required when any field name is a Python keyword.
318
+ All fields (including inherited) are included since functional form
319
+ can't specify a base class.
320
+ """
321
+ shadowed = self.get_shadowed_builtins(model)
322
+ entries: list[str] = []
323
+ for prop in model.properties:
324
+ type_annotation = prop.type_annotation(
325
+ serialize_namespace=self.serialize_namespace,
326
+ serialize_namespace_type=NamespaceType.TYPES_FILE,
327
+ )
328
+ type_annotation = _qualify_shadowed_builtins(type_annotation, shadowed)
329
+ is_optional = prop.optional or prop.client_default_value is not None
330
+ if is_optional:
331
+ entries.append(f' "{prop.wire_name}": {type_annotation},')
332
+ else:
333
+ entries.append(f' "{prop.wire_name}": Required[{type_annotation}],')
334
+ fields = "\n".join(entries)
335
+ return f'{model.name} = TypedDict("{model.name}", {{\n{fields}\n}}, total=False)'
336
+
337
+ @staticmethod
338
+ def get_properties_to_declare(model: ModelType) -> list[Property]:
339
+ if TypesSerializer.has_keyword_wire_names(model):
340
+ return [] # functional form handles all properties
341
+ non_discriminated_parents = [p for p in model.parents if not p.discriminated_subtypes]
342
+ if non_discriminated_parents:
343
+ parent_properties = [p for bm in non_discriminated_parents for p in bm.properties]
344
+ return [
345
+ p
346
+ for p in model.properties
347
+ if not any(
348
+ p.client_name == pp.client_name
349
+ and p.type_annotation() == pp.type_annotation()
350
+ and not p.is_base_discriminator
351
+ for pp in parent_properties
352
+ )
353
+ ]
354
+ return list(model.properties)
355
+
356
+ def declare_property(self, prop: Property, shadowed_builtins: frozenset[str]) -> str:
357
+ type_annotation = prop.type_annotation(
358
+ serialize_namespace=self.serialize_namespace,
359
+ serialize_namespace_type=NamespaceType.TYPES_FILE,
360
+ )
361
+ type_annotation = _qualify_shadowed_builtins(type_annotation, shadowed_builtins)
362
+ is_optional = prop.optional or prop.client_default_value is not None
363
+ if is_optional:
364
+ return f"{prop.wire_name}: {type_annotation}"
365
+ return f"{prop.wire_name}: Required[{type_annotation}]"
366
+
367
+ @staticmethod
368
+ def variable_documentation_string(prop: Property) -> list[str]:
369
+ return _documentation_string(prop, "ivar", "vartype", serialize_namespace_type=NamespaceType.TYPES_FILE)
370
+
29
371
  def serialize(self) -> str:
30
- # Generate the models
31
372
  template = self.env.get_template("types.py.jinja2")
32
373
  return template.render(
33
374
  code_model=self.code_model,
34
375
  imports=FileImportSerializer(self.imports()),
35
376
  serializer=self,
377
+ literal_enums=self.literal_enums,
378
+ models=self.typeddict_models,
379
+ discriminated_bases=self.discriminated_base_models,
36
380
  )
@@ -0,0 +1,44 @@
1
+ # -------------------------------------------------------------------------
2
+ # Copyright (c) Microsoft Corporation. All rights reserved.
3
+ # Licensed under the MIT License. See License.txt in the project root for
4
+ # license information.
5
+ # --------------------------------------------------------------------------
6
+ from ..models import CodeModel
7
+ from ..models.imports import FileImport, ImportType
8
+ from ..models.utils import NamespaceType
9
+ from .import_serializer import FileImportSerializer
10
+ from .base_serializer import BaseSerializer
11
+
12
+
13
+ class UnionsSerializer(BaseSerializer):
14
+ def __init__(
15
+ self,
16
+ code_model: CodeModel,
17
+ env,
18
+ ):
19
+ super().__init__(code_model=code_model, env=env)
20
+
21
+ def imports(self) -> FileImport:
22
+ file_import = FileImport(self.code_model)
23
+ if self.code_model.named_unions:
24
+ file_import.add_submodule_import(
25
+ "typing",
26
+ "Union",
27
+ ImportType.STDLIB,
28
+ )
29
+ for nu in self.code_model.named_unions:
30
+ file_import.merge(
31
+ nu.imports(
32
+ serialize_namespace=self.serialize_namespace,
33
+ serialize_namespace_type=NamespaceType.UNIONS_FILE,
34
+ )
35
+ )
36
+ return file_import
37
+
38
+ def serialize(self) -> str:
39
+ template = self.env.get_template("unions.py.jinja2")
40
+ return template.render(
41
+ code_model=self.code_model,
42
+ imports=FileImportSerializer(self.imports()),
43
+ serializer=self,
44
+ )
@@ -9,7 +9,7 @@
9
9
 
10
10
  {{ imports }}
11
11
  {% for model in models %}
12
- {% if model.base == "dpg" %}
12
+ {% if model.base == "dpg" or model.base == "typeddict" %}
13
13
  {% include "model_dpg.py.jinja2" %}
14
14
  {% elif model.base == "msrest" %}
15
15
  {% include "model_msrest.py.jinja2" %}
@@ -4,6 +4,55 @@
4
4
  {% endif %}
5
5
 
6
6
  {{ imports }}
7
- {% for nu in code_model.named_unions %}
8
- {{nu.name}} = {{nu.type_definition()}}
7
+ {% import 'operation_tools.jinja2' as op_tools %}
8
+ {% import "macros.jinja2" as macros %}
9
+ {% for enum in literal_enums %}
10
+
11
+ {{ serializer.declare_literal_enum(enum) }}
12
+ {% if enum.yaml_data.get("description") %}
13
+ """{{ op_tools.wrap_string(enum.yaml_data["description"], "\n") }}"""
14
+ {% endif %}
15
+ {% endfor %}
16
+ {% for model in models %}
17
+ {% if serializer.has_keyword_wire_names(model) %}
18
+
19
+
20
+ {{ serializer.declare_functional_model(model) }}
21
+ {{ model.name }}.__doc__ = """{{ op_tools.wrap_string(model.description(is_operation_file=False), "\n") }}
22
+
23
+ {% if model.properties != None %}
24
+ {% for p in model.properties %}
25
+ {% for line in serializer.variable_documentation_string(p) %}
26
+ {{ macros.wrap_model_string(line, '\n') -}}
27
+ {% endfor %}
28
+ {% endfor %}
29
+ {% endif %}
30
+ """
31
+ {% else %}
32
+
33
+
34
+ {{ serializer.declare_model(model) }}
35
+ """{{ op_tools.wrap_string(model.description(is_operation_file=False), "\n ") }}
36
+
37
+ {% if model.properties != None %}
38
+ {% for p in model.properties %}
39
+ {% for line in serializer.variable_documentation_string(p) %}
40
+ {{ macros.wrap_model_string(line, '\n ') -}}
41
+ {% endfor %}
42
+ {% endfor %}
43
+ {% endif %}
44
+ """
45
+
46
+ {% set shadowed = serializer.get_shadowed_builtins(model) %}
47
+ {% for p in serializer.get_properties_to_declare(model)%}
48
+ {{ serializer.declare_property(p, shadowed) }}
49
+ {% set prop_description = p.description(is_operation_file=False).replace('"', '\\"') %}
50
+ {% if prop_description %}
51
+ """{{ macros.wrap_model_string(prop_description, '\n ', '\"\"\"') -}}
52
+ {% endif %}
53
+ {% endfor %}
54
+ {% endif %}
55
+ {% endfor %}
56
+ {% for model in discriminated_bases %}
57
+ {{ serializer.discriminated_subtypes_union(model) }}
9
58
  {% endfor %}
@@ -0,0 +1,9 @@
1
+ # coding=utf-8
2
+ {% if code_model.license_header %}
3
+ {{ code_model.license_header }}
4
+ {% endif %}
5
+
6
+ {{ imports }}
7
+ {% for nu in code_model.named_unions %}
8
+ {{nu.name}} = {{nu.type_definition()}}
9
+ {% endfor %}