@typespec/http-client-python 0.29.0 → 0.29.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.
@@ -677,7 +677,7 @@ export async function preprocess(flavor: string, generatedFolder: string): Promi
677
677
 
678
678
  /**
679
679
  * Resets the `tests/generated/{azure,unbranded}` baseline by sparse-checking-out
680
- * `eng/tools/emitter/gen` from the Azure/azure-sdk-for-python repo, then
680
+ * `eng/tools/azure-sdk-tools/emitter/generated` from the Azure/azure-sdk-for-python repo, then
681
681
  * deleting a couple of fully-generated package folders so regeneration has to
682
682
  * recreate them from scratch (smoke test of full-emit path).
683
683
  *
@@ -686,8 +686,8 @@ export async function preprocess(flavor: string, generatedFolder: string): Promi
686
686
  */
687
687
  export async function prepareBaselineOfGeneratedCode(generatedFolder: string): Promise<void> {
688
688
  const repoUrl = "https://github.com/Azure/azure-sdk-for-python.git";
689
- const branch = "main";
690
- const sourceSubdir = "eng/tools/emitter/gen";
689
+ const branch = "typespec-python-generated-tests";
690
+ const sourceSubdir = "eng/tools/azure-sdk-tools/emitter/generated";
691
691
  const testsGeneratedDir = resolve(generatedFolder, "../tests/generated");
692
692
 
693
693
  console.log(pc.cyan(`\n${"=".repeat(60)}`));
@@ -708,6 +708,7 @@ export async function prepareBaselineOfGeneratedCode(generatedFolder: string): P
708
708
  execSync(cmd, { cwd: tempDir, stdio: ["ignore", "ignore", "inherit"] });
709
709
 
710
710
  run(`git init`);
711
+ run(`git config core.longpaths true`);
711
712
  run(`git remote add origin ${repoUrl}`);
712
713
  run(`git config core.sparseCheckout true`);
713
714
  run(`git sparse-checkout init --cone`);
@@ -732,16 +733,23 @@ export async function prepareBaselineOfGeneratedCode(generatedFolder: string): P
732
733
  rmSync(tempDir, { recursive: true, force: true });
733
734
  }
734
735
 
735
- // Delete a couple of fully-generated package folders so regeneration has to
736
- // recreate them from scratch (smoke test of full-emit path).
737
- const targetsToDelete = [
738
- join(testsGeneratedDir, "azure", "authentication-http-custom"),
739
- join(testsGeneratedDir, "unbranded", "encode-array"),
740
- ];
741
- for (const target of targetsToDelete) {
742
- if (existsSync(target)) {
743
- console.log(pc.dim(`Deleting ${target}`));
744
- rmSync(target, { recursive: true, force: true });
736
+ // Smoke test the full-emit path: delete a couple of fully-generated package
737
+ // folders and every README.md so regeneration has to recreate them.
738
+ const deleteIfExists = (path: string) => {
739
+ if (!existsSync(path)) return;
740
+ console.log(pc.dim(`Deleting ${path}`));
741
+ rmSync(path, { recursive: true, force: true });
742
+ };
743
+
744
+ deleteIfExists(join(testsGeneratedDir, "azure", "authentication-http-custom"));
745
+ deleteIfExists(join(testsGeneratedDir, "unbranded", "encode-array"));
746
+
747
+ if (existsSync(testsGeneratedDir)) {
748
+ const entries = await readdir(testsGeneratedDir, { recursive: true, withFileTypes: true });
749
+ for (const entry of entries) {
750
+ if (entry.isFile() && entry.name === "README.md") {
751
+ deleteIfExists(join(entry.parentPath, entry.name));
752
+ }
745
753
  }
746
754
  }
747
755
  }
@@ -181,7 +181,21 @@ class PagingResponse(Response):
181
181
 
182
182
  def type_annotation(self, **kwargs: Any) -> str:
183
183
  iterable = "AsyncItemPaged" if kwargs["async_mode"] else "ItemPaged"
184
- return f"{iterable}[{self.item_type.type_annotation(**kwargs)}]"
184
+ return f"{iterable}[{self._item_type_annotation(**kwargs)}]"
185
+
186
+ def _item_type_annotation(self, **kwargs: Any) -> str:
187
+ # When the page item is a ListType, render the outer `List`/`list`
188
+ # wrapper here using the operation-file alias decision so a list page
189
+ # item rendered inside an operation file named `list` uses the `List`
190
+ # alias (avoiding the built-in `list` shadowed by `List = list`).
191
+ # Recurse into the element type without is_operation_file so nested
192
+ # generated model types keep their forward-reference quoting
193
+ # (e.g. ItemPaged[List["_models.Product"]]).
194
+ if isinstance(self.item_type, ListType):
195
+ use_list_import = self.code_model.has_operation_named_list
196
+ list_type = "List" if use_list_import else "list"
197
+ return f"{list_type}[{self.item_type.element_type.type_annotation(**kwargs)}]"
198
+ return self.item_type.type_annotation(**kwargs)
185
199
 
186
200
  def docstring_text(self, **kwargs: Any) -> str:
187
201
  base_description = "An iterator like instance of "
@@ -6,13 +6,73 @@
6
6
  from typing import Optional
7
7
  from abc import ABC, abstractmethod
8
8
 
9
- from ..models import ModelType, Property, ConstantType, EnumValue
9
+ from ..models import ModelType, Property, ConstantType, EnumValue, EnumType
10
10
  from ..models.imports import FileImport, TypingSection, MsrestImportType, ImportType
11
+ from ..models.primitive_types import (
12
+ BooleanType,
13
+ IntegerType,
14
+ FloatType,
15
+ DecimalType,
16
+ StringType,
17
+ DatetimeType,
18
+ UnixTimeType,
19
+ TimeType,
20
+ DateType,
21
+ DurationType,
22
+ ByteArraySchema,
23
+ )
11
24
  from .import_serializer import FileImportSerializer
12
25
  from .base_serializer import BaseSerializer
13
26
  from ..models.utils import NamespaceType
14
27
 
15
28
 
29
+ def _get_xml_deserializer_name(prop: Property) -> Optional[str]: # pylint: disable=too-many-return-statements
30
+ """Return the _xml_deser_* function name for a scalar XML property, or None."""
31
+ prop_type = prop.type
32
+ # Unwrap ConstantType to get the underlying value type
33
+ if isinstance(prop_type, ConstantType):
34
+ prop_type = prop_type.value_type
35
+
36
+ if isinstance(prop_type, StringType):
37
+ return "_xml_deser_str"
38
+ if isinstance(prop_type, IntegerType):
39
+ return "_xml_deser_int"
40
+ if isinstance(prop_type, FloatType):
41
+ return "_xml_deser_float"
42
+ if isinstance(prop_type, BooleanType):
43
+ return "_xml_deser_bool"
44
+ if isinstance(prop_type, DecimalType):
45
+ return "_xml_deser_decimal"
46
+ if isinstance(prop_type, DatetimeType):
47
+ if prop_type.encode == "rfc7231":
48
+ return "_xml_deser_datetime_rfc7231"
49
+ return "_xml_deser_datetime"
50
+ if isinstance(prop_type, UnixTimeType):
51
+ return "_xml_deser_datetime_unix_timestamp"
52
+ if isinstance(prop_type, TimeType):
53
+ return "_xml_deser_time"
54
+ if isinstance(prop_type, DateType):
55
+ return "_xml_deser_date"
56
+ if isinstance(prop_type, DurationType):
57
+ return "_xml_deser_duration"
58
+ if isinstance(prop_type, ByteArraySchema):
59
+ encode = getattr(prop_type, "encode", None) or getattr(prop, "encode", None)
60
+ if encode == "base64url":
61
+ return "_xml_deser_bytes_base64url"
62
+ return "_xml_deser_bytes"
63
+ if isinstance(prop_type, EnumType):
64
+ return f"enum:{prop_type.name}"
65
+ return None
66
+
67
+
68
+ def _get_xml_deserializer_enum_type(prop: Property) -> Optional[EnumType]:
69
+ """Return the EnumType for an XML enum property (unwrapping ConstantType), or None."""
70
+ prop_type = prop.type
71
+ if isinstance(prop_type, ConstantType):
72
+ prop_type = prop_type.value_type
73
+ return prop_type if isinstance(prop_type, EnumType) else None
74
+
75
+
16
76
  def _documentation_string(prop: Property, description_keyword: str, docstring_type_keyword: str) -> list[str]:
17
77
  retval: list[str] = []
18
78
  sphinx_prefix = f":{description_keyword} {prop.client_name}:"
@@ -253,6 +313,9 @@ class DpgModelSerializer(_ModelSerializer):
253
313
  TypingSection.REGULAR,
254
314
  alias="_Model",
255
315
  )
316
+ # Collect XML deserializer functions needed by models in this file
317
+ xml_deser_names: set[str] = set()
318
+ xml_deser_enums: dict[str, EnumType] = {}
256
319
  for model in self.models:
257
320
  if model.base == "json":
258
321
  continue
@@ -271,6 +334,15 @@ class DpgModelSerializer(_ModelSerializer):
271
334
  called_by_property=True,
272
335
  )
273
336
  )
337
+ # Track XML deserializer functions needed
338
+ if prop.xml_metadata:
339
+ deser_name = _get_xml_deserializer_name(prop)
340
+ if deser_name:
341
+ xml_deser_names.add(deser_name)
342
+ if deser_name and deser_name.startswith("enum:"):
343
+ enum_type = _get_xml_deserializer_enum_type(prop)
344
+ if enum_type is not None:
345
+ xml_deser_enums[enum_type.name] = enum_type
274
346
  for parent in model.parents:
275
347
  if parent.client_namespace != model.client_namespace:
276
348
  file_import.add_submodule_import(
@@ -285,6 +357,38 @@ class DpgModelSerializer(_ModelSerializer):
285
357
  file_import.add_submodule_import("typing", "overload", ImportType.STDLIB)
286
358
  file_import.add_submodule_import("typing", "Mapping", ImportType.STDLIB)
287
359
  file_import.add_submodule_import("typing", "Any", ImportType.STDLIB)
360
+ # Add imports for XML deserializer functions
361
+ has_enum_deser = False
362
+ for deser_name in sorted(xml_deser_names):
363
+ if deser_name.startswith("enum:"):
364
+ has_enum_deser = True
365
+ continue
366
+ file_import.add_submodule_import(
367
+ self.code_model.get_relative_import_path(self.serialize_namespace, module_name="_utils.model_base"),
368
+ deser_name,
369
+ ImportType.LOCAL,
370
+ )
371
+ if has_enum_deser:
372
+ file_import.add_import("functools", ImportType.STDLIB)
373
+ file_import.add_submodule_import(
374
+ self.code_model.get_relative_import_path(self.serialize_namespace, module_name="_utils.model_base"),
375
+ "_xml_deser_enum_or_str",
376
+ ImportType.LOCAL,
377
+ )
378
+ # Ensure each referenced enum class is imported at runtime (not just
379
+ # under TYPE_CHECKING), so functools.partial can bind the class at
380
+ # class-body evaluation time.
381
+ for enum_name, enum_type in xml_deser_enums.items():
382
+ file_import.add_submodule_import(
383
+ self.code_model.get_relative_import_path(
384
+ self.serialize_namespace,
385
+ self.code_model.get_imported_namespace_for_model(enum_type.client_namespace),
386
+ module_name=self.code_model.enums_filename,
387
+ ),
388
+ enum_name,
389
+ ImportType.LOCAL,
390
+ TypingSection.REGULAR,
391
+ )
288
392
  # if there is a property named `list` we have to make sure there's no conflict with the built-in `list`
289
393
  if self.code_model.has_property_named_list:
290
394
  file_import.define_mypy_type("List", "list")
@@ -337,6 +441,14 @@ class DpgModelSerializer(_ModelSerializer):
337
441
 
338
442
  if prop.xml_metadata:
339
443
  args.append(f"xml={prop.xml_metadata}")
444
+ # Add fast-path deserializer for scalar XML fields
445
+ deser_name = _get_xml_deserializer_name(prop)
446
+ if deser_name:
447
+ if deser_name.startswith("enum:"):
448
+ enum_name = deser_name[5:]
449
+ args.append(f"deserializer=functools.partial(_xml_deser_enum_or_str, {enum_name})")
450
+ else:
451
+ args.append(f"deserializer={deser_name}")
340
452
 
341
453
  if prop.original_tsp_name:
342
454
  args.append(f'original_tsp_name="{prop.original_tsp_name}"')