@typespec/http-client-python 0.33.0 → 0.34.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/dist/emitter/types.d.ts.map +1 -1
  2. package/dist/emitter/types.js +2 -0
  3. package/dist/emitter/types.js.map +1 -1
  4. package/emitter/src/types.ts +2 -0
  5. package/emitter/temp/tsconfig.tsbuildinfo +1 -1
  6. package/eng/scripts/ci/regenerate-common.ts +40 -15
  7. package/eng/scripts/setup/__pycache__/package_manager.cpython-311.pyc +0 -0
  8. package/eng/scripts/setup/__pycache__/venvtools.cpython-311.pyc +0 -0
  9. package/generator/build/lib/pygen/__init__.py +3 -3
  10. package/generator/build/lib/pygen/codegen/models/__init__.py +4 -2
  11. package/generator/build/lib/pygen/codegen/models/code_model.py +27 -5
  12. package/generator/build/lib/pygen/codegen/models/combined_type.py +3 -3
  13. package/generator/build/lib/pygen/codegen/models/enum_type.py +75 -25
  14. package/generator/build/lib/pygen/codegen/models/list_type.py +10 -4
  15. package/generator/build/lib/pygen/codegen/models/model_type.py +145 -4
  16. package/generator/build/lib/pygen/codegen/models/operation.py +1 -0
  17. package/generator/build/lib/pygen/codegen/models/paging_operation.py +5 -1
  18. package/generator/build/lib/pygen/codegen/models/parameter.py +2 -2
  19. package/generator/build/lib/pygen/codegen/models/primitive_types.py +30 -6
  20. package/generator/build/lib/pygen/codegen/models/property.py +21 -8
  21. package/generator/build/lib/pygen/codegen/models/response.py +16 -3
  22. package/generator/build/lib/pygen/codegen/models/utils.py +3 -6
  23. package/generator/build/lib/pygen/codegen/serializers/__init__.py +46 -11
  24. package/generator/build/lib/pygen/codegen/serializers/builder_serializer.py +48 -15
  25. package/generator/build/lib/pygen/codegen/serializers/model_init_serializer.py +1 -1
  26. package/generator/build/lib/pygen/codegen/serializers/model_serializer.py +127 -3
  27. package/generator/build/lib/pygen/codegen/serializers/types_serializer.py +263 -12
  28. package/generator/build/lib/pygen/codegen/serializers/unions_serializer.py +44 -0
  29. package/generator/build/lib/pygen/codegen/templates/model_container.py.jinja2 +1 -1
  30. package/generator/build/lib/pygen/codegen/templates/types.py.jinja2 +51 -2
  31. package/generator/build/lib/pygen/codegen/templates/unions.py.jinja2 +9 -0
  32. package/generator/build/lib/pygen/preprocess/__init__.py +137 -162
  33. package/generator/build/lib/pygen/utils.py +6 -24
  34. package/generator/dist/pygen-0.1.0-py3-none-any.whl +0 -0
  35. package/generator/pygen/__init__.py +3 -3
  36. package/generator/pygen/codegen/models/__init__.py +4 -2
  37. package/generator/pygen/codegen/models/code_model.py +27 -5
  38. package/generator/pygen/codegen/models/combined_type.py +3 -3
  39. package/generator/pygen/codegen/models/enum_type.py +75 -25
  40. package/generator/pygen/codegen/models/list_type.py +10 -4
  41. package/generator/pygen/codegen/models/model_type.py +145 -4
  42. package/generator/pygen/codegen/models/operation.py +1 -0
  43. package/generator/pygen/codegen/models/paging_operation.py +5 -1
  44. package/generator/pygen/codegen/models/parameter.py +2 -2
  45. package/generator/pygen/codegen/models/primitive_types.py +30 -6
  46. package/generator/pygen/codegen/models/property.py +21 -8
  47. package/generator/pygen/codegen/models/response.py +16 -3
  48. package/generator/pygen/codegen/models/utils.py +3 -6
  49. package/generator/pygen/codegen/serializers/__init__.py +46 -11
  50. package/generator/pygen/codegen/serializers/builder_serializer.py +48 -15
  51. package/generator/pygen/codegen/serializers/model_init_serializer.py +1 -1
  52. package/generator/pygen/codegen/serializers/model_serializer.py +127 -3
  53. package/generator/pygen/codegen/serializers/types_serializer.py +263 -12
  54. package/generator/pygen/codegen/serializers/unions_serializer.py +44 -0
  55. package/generator/pygen/codegen/templates/model_container.py.jinja2 +1 -1
  56. package/generator/pygen/codegen/templates/types.py.jinja2 +51 -2
  57. package/generator/pygen/codegen/templates/unions.py.jinja2 +9 -0
  58. package/generator/pygen/preprocess/__init__.py +137 -162
  59. package/generator/pygen/utils.py +6 -24
  60. package/generator/pygen.egg-info/SOURCES.txt +2 -0
  61. package/package.json +5 -5
@@ -3,34 +3,285 @@
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 typing import Optional
9
+ from ..models import ModelType, CodeModel
10
+ from ..models.enum_type import EnumType
6
11
  from ..models.imports import FileImport, ImportType
7
12
  from ..models.utils import NamespaceType
13
+ from ..models.property import Property
14
+ from .model_serializer import _documentation_string
8
15
  from .import_serializer import FileImportSerializer
9
16
  from .base_serializer import BaseSerializer
10
17
 
18
+ # Python builtin type names that can be shadowed by TypedDict field wire_names.
19
+ # When a field name matches one of these, all references to that builtin in type
20
+ # annotations within the same class are qualified as builtins.X.
21
+ _BUILTIN_TYPE_NAMES = frozenset(
22
+ {
23
+ "int",
24
+ "str",
25
+ "float",
26
+ "bool",
27
+ "list",
28
+ "dict",
29
+ "tuple",
30
+ "set",
31
+ "bytes",
32
+ "type",
33
+ "object",
34
+ "complex",
35
+ "frozenset",
36
+ "bytearray",
37
+ "memoryview",
38
+ }
39
+ )
40
+
41
+
42
+ def _qualify_shadowed_builtins(annotation: str, shadowed: frozenset[str]) -> str:
43
+ """Replace bare builtin type references with builtins.X when shadowed by a field name."""
44
+ if not shadowed:
45
+ return annotation
46
+ for name in shadowed:
47
+ annotation = re.sub(rf"\b{name}\b", f"builtins.{name}", annotation)
48
+ return annotation
49
+
11
50
 
12
51
  class TypesSerializer(BaseSerializer):
52
+ def __init__(
53
+ self,
54
+ code_model: CodeModel,
55
+ env,
56
+ client_namespace: Optional[str] = None,
57
+ models: Optional[list[ModelType]] = None,
58
+ enums: Optional[list["EnumType"]] = None,
59
+ ):
60
+ super().__init__(code_model=code_model, env=env, client_namespace=client_namespace)
61
+ self._models = models or []
62
+ self._enums = enums or []
63
+
64
+ @property
65
+ def literal_enums(self) -> list[EnumType]:
66
+ """Enums to render as Literal type aliases in typeddict mode."""
67
+ return sorted(self._enums)
68
+
69
+ def declare_literal_enum(self, enum: EnumType) -> str:
70
+ """Generate a Literal type alias for an enum, e.g. MyColor = Literal["red", "blue"]."""
71
+ values = [enum.get_declaration(v.value) for v in enum.values]
72
+ return f"{enum.name} = Literal[{', '.join(values)}]"
73
+
74
+ @property
75
+ def typeddict_models(self) -> list[ModelType]:
76
+ """Models that should be rendered as TypedDicts (excluding discriminated bases which become unions).
77
+
78
+ When both a dpg model and its typeddict copy exist (same crossLanguageDefinitionId),
79
+ prefer the dpg model (it already renders as a TypedDict in types.py) and skip the copy.
80
+ """
81
+ candidates = [m for m in self._models if m.base != "json" and not m.discriminated_subtypes]
82
+ seen_ids: dict[str, "ModelType"] = {}
83
+ result: list["ModelType"] = []
84
+ for m in candidates:
85
+ clid = m.yaml_data.get("crossLanguageDefinitionId")
86
+ if clid and clid in seen_ids:
87
+ # Prefer the dpg model over the typeddict copy
88
+ if m.base == "dpg" and seen_ids[clid].base == "typeddict":
89
+ # 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
92
+ # Otherwise skip this duplicate
93
+ continue
94
+ if clid:
95
+ seen_ids[clid] = m
96
+ result.append(m)
97
+ return result
98
+
99
+ @property
100
+ def discriminated_base_models(self) -> list[ModelType]:
101
+ """Discriminated base models that become Union type aliases in types.py.
102
+
103
+ Topologically sorted so that nested discriminated bases (e.g. Shark)
104
+ are defined before their parents (e.g. Fish = Union[Salmon, Shark]).
105
+ """
106
+ bases = [m for m in self._models if m.base != "json" and m.discriminated_subtypes]
107
+ base_names = {m.name for m in bases}
108
+ sorted_bases: list[ModelType] = []
109
+ visited: set[str] = set()
110
+
111
+ def visit(model: ModelType) -> None:
112
+ if model.name in visited:
113
+ return
114
+ visited.add(model.name)
115
+ for subtype in model.discriminated_subtypes.values():
116
+ if subtype.name in base_names:
117
+ visit(subtype)
118
+ sorted_bases.append(model)
119
+
120
+ for m in bases:
121
+ visit(m)
122
+ return sorted_bases
123
+
124
+ def discriminated_subtypes_union(self, model: ModelType) -> str:
125
+ """Generate a Union alias for a discriminated base using TypedDict subtype names."""
126
+ subtypes = list(model.discriminated_subtypes.values())
127
+ subtype_names = [s.name for s in subtypes]
128
+ return f"{model.name} = Union[{', '.join(subtype_names)}]"
129
+
130
+ @staticmethod
131
+ def has_keyword_wire_names(model: ModelType) -> bool:
132
+ """Whether any property wire_name is a Python keyword or requires functional TypedDict form."""
133
+ return any(keyword.iskeyword(p.wire_name) or not p.wire_name.isidentifier() for p in model.properties)
134
+
135
+ @staticmethod
136
+ def get_shadowed_builtins(model: ModelType) -> frozenset[str]:
137
+ """Return the set of builtin type names shadowed by property wire_names in this model.
138
+
139
+ Only includes a builtin if it is both used as a wire_name AND referenced
140
+ in a type annotation within the same model (otherwise no shadowing occurs).
141
+ """
142
+ wire_builtins = {p.wire_name for p in model.properties if p.wire_name in _BUILTIN_TYPE_NAMES}
143
+ if not wire_builtins:
144
+ return frozenset()
145
+ # Check which of these builtins actually appear in type annotations
146
+ used = set()
147
+ for prop in model.properties:
148
+ annotation = prop.type_annotation()
149
+ for name in wire_builtins:
150
+ if re.search(rf"\b{name}\b", annotation):
151
+ used.add(name)
152
+ return frozenset(used)
153
+
13
154
  def imports(self) -> FileImport:
14
155
  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
156
+
157
+ literal_enums = self.literal_enums
158
+ if literal_enums:
159
+ file_import.add_submodule_import("typing", "Literal", ImportType.STDLIB)
160
+
161
+ td_models = self.typeddict_models
162
+ if td_models or self.discriminated_base_models:
163
+ if td_models:
164
+ file_import.add_submodule_import("typing_extensions", "TypedDict", ImportType.STDLIB)
165
+ if self.discriminated_base_models:
166
+ file_import.add_submodule_import("typing", "Union", ImportType.STDLIB)
167
+ has_required = False
168
+ needs_builtins = False
169
+ for model in td_models:
170
+ file_import.merge(
171
+ model.imports(
172
+ is_operation_file=False,
173
+ serialize_namespace=self.serialize_namespace,
174
+ serialize_namespace_type=NamespaceType.TYPES_FILE,
175
+ )
25
176
  )
26
- )
177
+ for prop in model.properties:
178
+ file_import.merge(
179
+ prop.imports(
180
+ serialize_namespace=self.serialize_namespace,
181
+ serialize_namespace_type=NamespaceType.TYPES_FILE,
182
+ called_by_property=True,
183
+ )
184
+ )
185
+ if not (prop.optional or prop.client_default_value is not None):
186
+ has_required = True
187
+ if self.get_shadowed_builtins(model):
188
+ needs_builtins = True
189
+ for parent in model.parents:
190
+ if parent.client_namespace != model.client_namespace and not parent.discriminated_subtypes:
191
+ # Import parent class from sibling namespace's types module
192
+ file_import.add_submodule_import(
193
+ self.code_model.get_relative_import_path(
194
+ self.serialize_namespace,
195
+ parent.client_namespace,
196
+ module_name="types",
197
+ ),
198
+ parent.name,
199
+ ImportType.LOCAL,
200
+ )
201
+ if has_required:
202
+ file_import.add_submodule_import("typing_extensions", "Required", ImportType.STDLIB)
203
+ if needs_builtins:
204
+ file_import.add_import("builtins", ImportType.STDLIB)
27
205
  return file_import
28
206
 
207
+ def declare_model(self, model: ModelType) -> str:
208
+ """Generate the class declaration or functional form for a TypedDict model.
209
+
210
+ Uses functional form when any property wire_name is a Python keyword
211
+ (e.g. 'and', 'class') since keywords can't be identifiers in class bodies.
212
+ """
213
+ if self.has_keyword_wire_names(model):
214
+ return "" # functional form is rendered separately
215
+ non_discriminated_parents = [p for p in model.parents if not p.discriminated_subtypes]
216
+ if non_discriminated_parents:
217
+ basename = ", ".join([m.name for m in non_discriminated_parents])
218
+ return f"class {model.name}({basename}):{model.pylint_disable()}"
219
+ return f"class {model.name}(TypedDict, total=False):{model.pylint_disable()}"
220
+
221
+ def declare_functional_model(self, model: ModelType) -> str:
222
+ """Generate a functional-form TypedDict for models with keyword wire_names.
223
+
224
+ Functional form is required when any field name is a Python keyword.
225
+ All fields (including inherited) are included since functional form
226
+ can't specify a base class.
227
+ """
228
+ shadowed = self.get_shadowed_builtins(model)
229
+ entries: list[str] = []
230
+ for prop in model.properties:
231
+ type_annotation = prop.type_annotation(
232
+ serialize_namespace=self.serialize_namespace,
233
+ serialize_namespace_type=NamespaceType.TYPES_FILE,
234
+ )
235
+ type_annotation = _qualify_shadowed_builtins(type_annotation, shadowed)
236
+ is_optional = prop.optional or prop.client_default_value is not None
237
+ if is_optional:
238
+ entries.append(f' "{prop.wire_name}": {type_annotation},')
239
+ else:
240
+ entries.append(f' "{prop.wire_name}": Required[{type_annotation}],')
241
+ fields = "\n".join(entries)
242
+ return f'{model.name} = TypedDict("{model.name}", {{\n{fields}\n}}, total=False)'
243
+
244
+ @staticmethod
245
+ def get_properties_to_declare(model: ModelType) -> list[Property]:
246
+ if TypesSerializer.has_keyword_wire_names(model):
247
+ return [] # functional form handles all properties
248
+ non_discriminated_parents = [p for p in model.parents if not p.discriminated_subtypes]
249
+ if non_discriminated_parents:
250
+ parent_properties = [p for bm in non_discriminated_parents for p in bm.properties]
251
+ return [
252
+ p
253
+ for p in model.properties
254
+ if not any(
255
+ p.client_name == pp.client_name
256
+ and p.type_annotation() == pp.type_annotation()
257
+ and not p.is_base_discriminator
258
+ for pp in parent_properties
259
+ )
260
+ ]
261
+ return list(model.properties)
262
+
263
+ def declare_property(self, prop: Property, shadowed_builtins: frozenset[str]) -> str:
264
+ type_annotation = prop.type_annotation(
265
+ serialize_namespace=self.serialize_namespace,
266
+ serialize_namespace_type=NamespaceType.TYPES_FILE,
267
+ )
268
+ type_annotation = _qualify_shadowed_builtins(type_annotation, shadowed_builtins)
269
+ is_optional = prop.optional or prop.client_default_value is not None
270
+ if is_optional:
271
+ return f"{prop.wire_name}: {type_annotation}"
272
+ return f"{prop.wire_name}: Required[{type_annotation}]"
273
+
274
+ @staticmethod
275
+ def variable_documentation_string(prop: Property) -> list[str]:
276
+ return _documentation_string(prop, "ivar", "vartype", serialize_namespace_type=NamespaceType.TYPES_FILE)
277
+
29
278
  def serialize(self) -> str:
30
- # Generate the models
31
279
  template = self.env.get_template("types.py.jinja2")
32
280
  return template.render(
33
281
  code_model=self.code_model,
34
282
  imports=FileImportSerializer(self.imports()),
35
283
  serializer=self,
284
+ literal_enums=self.literal_enums,
285
+ models=self.typeddict_models,
286
+ discriminated_bases=self.discriminated_base_models,
36
287
  )
@@ -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 %}