@typespec/http-client-python 0.29.1 → 0.30.0-alpha.20260529.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 (40) hide show
  1. package/dist/emitter/http.d.ts.map +1 -1
  2. package/dist/emitter/http.js +3 -2
  3. package/dist/emitter/http.js.map +1 -1
  4. package/dist/emitter/types.d.ts.map +1 -1
  5. package/dist/emitter/types.js +3 -2
  6. package/dist/emitter/types.js.map +1 -1
  7. package/dist/emitter/utils.d.ts +5 -0
  8. package/dist/emitter/utils.d.ts.map +1 -1
  9. package/dist/emitter/utils.js +6 -2
  10. package/dist/emitter/utils.js.map +1 -1
  11. package/emitter/src/http.ts +3 -1
  12. package/emitter/src/types.ts +3 -1
  13. package/emitter/src/utils.ts +8 -2
  14. package/emitter/temp/tsconfig.tsbuildinfo +1 -1
  15. package/eng/scripts/Build-Packages.ps1 +17 -2
  16. package/eng/scripts/ci/dev_requirements.txt +2 -2
  17. package/eng/scripts/ci/regenerate-common.ts +21 -13
  18. package/eng/scripts/ci/run_pylint.py +9 -0
  19. package/eng/scripts/setup/__pycache__/package_manager.cpython-311.pyc +0 -0
  20. package/eng/scripts/setup/__pycache__/venvtools.cpython-311.pyc +0 -0
  21. package/eng/scripts/setup/package_manager.py +13 -3
  22. package/generator/build/lib/pygen/codegen/models/operation.py +6 -1
  23. package/generator/build/lib/pygen/codegen/models/parameter.py +1 -0
  24. package/generator/build/lib/pygen/codegen/serializers/builder_serializer.py +3 -2
  25. package/generator/build/lib/pygen/codegen/serializers/model_serializer.py +113 -1
  26. package/generator/build/lib/pygen/codegen/templates/model_base.py.jinja2 +329 -45
  27. package/generator/build/lib/pygen/codegen/templates/serialization.py.jinja2 +22 -1
  28. package/generator/build/lib/pygen/preprocess/__init__.py +17 -5
  29. package/generator/dist/pygen-0.1.0-py3-none-any.whl +0 -0
  30. package/generator/pygen/codegen/models/operation.py +6 -1
  31. package/generator/pygen/codegen/models/parameter.py +1 -0
  32. package/generator/pygen/codegen/serializers/builder_serializer.py +3 -2
  33. package/generator/pygen/codegen/serializers/model_serializer.py +113 -1
  34. package/generator/pygen/codegen/templates/model_base.py.jinja2 +329 -45
  35. package/generator/pygen/codegen/templates/serialization.py.jinja2 +22 -1
  36. package/generator/pygen/preprocess/__init__.py +17 -5
  37. package/generator/pygen.egg-info/PKG-INFO +1 -1
  38. package/generator/pygen.egg-info/requires.txt +1 -1
  39. package/generator/setup.py +1 -1
  40. package/package.json +6 -6
@@ -75,6 +75,13 @@ New-Item -ItemType Directory -Force -Path "$outputPath/packages" | Out-Null
75
75
  $emitterVersion = node -p -e "require('$packageRoot/package.json').version"
76
76
  Write-Host "Package version: $emitterVersion"
77
77
 
78
+ # Stamp prerelease version if BuildNumber is provided
79
+ if ($BuildNumber) {
80
+ $versionTag = $Prerelease ? "-alpha" : "-beta"
81
+ $emitterVersion = "$emitterVersion$versionTag.$BuildNumber"
82
+ Write-Host "Stamped prerelease version: $emitterVersion"
83
+ }
84
+
78
85
  Push-Location "$packageRoot"
79
86
  try {
80
87
  # Step 1: Build the emitter and generator
@@ -87,12 +94,20 @@ try {
87
94
  Invoke-LoggedCommand "npm run lint" -GroupOutput
88
95
  }
89
96
 
90
- # Step 3: Create npm package
97
+ # Step 3: Stamp version in package.json if prerelease
98
+ if ($BuildNumber) {
99
+ Write-Host "`n=== Updating package.json version to $emitterVersion ===" -ForegroundColor Cyan
100
+ $packageJson = Get-Content -Raw "package.json" | ConvertFrom-Json -AsHashtable
101
+ $packageJson.version = $emitterVersion
102
+ $packageJson | ConvertTo-Json -Depth 100 | Out-File -Path "package.json" -Encoding utf8 -NoNewline -Force
103
+ }
104
+
105
+ # Step 4: Create npm package
91
106
  Write-Host "`n=== Creating npm package ===" -ForegroundColor Cyan
92
107
  Invoke-LoggedCommand "npm pack"
93
108
  Copy-Item "typespec-http-client-python-$emitterVersion.tgz" -Destination "$outputPath/packages"
94
109
 
95
- # Step 4: Verify package can be installed
110
+ # Step 5: Verify package can be installed
96
111
  Write-Host "`n=== Verifying package installation ===" -ForegroundColor Cyan
97
112
  Invoke-LoggedCommand "npm install typespec-http-client-python-$emitterVersion.tgz" -GroupOutput
98
113
 
@@ -5,8 +5,8 @@ tox-uv
5
5
  mypy==1.19.1
6
6
  colorama==0.4.6
7
7
  debugpy==1.8.2
8
- pytest==8.3.2
8
+ pytest==9.0.3
9
9
  coverage==7.6.1
10
- black==26.3.1
10
+ black==24.4.0
11
11
  ptvsd==4.3.2
12
12
  types-PyYAML==6.0.12.8
@@ -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
  }
@@ -47,6 +47,15 @@ def _single_dir_pylint(mod):
47
47
  ]
48
48
  if is_azure:
49
49
  pylint_args.append("--load-plugins=pylint_guidelines_checker")
50
+ # Per-package suppressions for generated code where pylint's ignore-paths
51
+ # is not honored (pylint 4.x + recursive + plugin). Keep the scope as
52
+ # narrow as possible: only this package, only the offending check.
53
+ per_package_disables = {
54
+ "azure-client-generator-core-exact-name": ["client-incorrect-naming-convention"],
55
+ }
56
+ extra_disables = per_package_disables.get(mod.name)
57
+ if extra_disables:
58
+ pylint_args.append("--disable={}".format(",".join(extra_disables)))
50
59
  pylint_args.append(str(inner_class.absolute()))
51
60
  try:
52
61
  check_call(pylint_args)
@@ -100,10 +100,20 @@ def install_packages(packages: list, venv_context=None, package_manager: str = N
100
100
 
101
101
  try:
102
102
  if cwd:
103
- subprocess.check_call(install_cmd + packages, cwd=cwd)
103
+ result = subprocess.run(install_cmd + packages, cwd=cwd, capture_output=True, text=True)
104
104
  else:
105
- subprocess.check_call(install_cmd + packages)
106
- except subprocess.CalledProcessError as e:
105
+ result = subprocess.run(install_cmd + packages, capture_output=True, text=True)
106
+ if result.returncode != 0:
107
+ error_output = (result.stderr or "") + (result.stdout or "")
108
+ error_msg = f"Failed to install packages with {package_manager}: command exited with code {result.returncode}\n{error_output}"
109
+ if "401" in error_output or "Unauthorized" in error_output:
110
+ error_msg += (
111
+ "\n\nReceived a 401 Unauthorized error from the Azure feed. "
112
+ "A pip dependency may not yet be cached. To fix this, follow the authentication steps at: "
113
+ "https://github.com/Azure/azure-sdk-for-python/blob/main/CONTRIBUTING.md#authentication-for-upstream-pull-through"
114
+ )
115
+ raise RuntimeError(error_msg)
116
+ except OSError as e:
107
117
  raise RuntimeError(f"Failed to install packages with {package_manager}: {e}")
108
118
 
109
119
 
@@ -91,11 +91,16 @@ class OperationBase( # pylint: disable=too-many-public-methods,too-many-instanc
91
91
  self.has_etag: bool = self.yaml_data.get("hasEtag", False)
92
92
  self.cross_language_definition_id: Optional[str] = self.yaml_data.get("crossLanguageDefinitionId")
93
93
 
94
+ @property
95
+ def exact_name_params(self) -> set[str]:
96
+ """Return the set of client names for parameters with isExactName."""
97
+ return {p.client_name for p in self.parameters.method if p.is_exact_name}
98
+
94
99
  @property
95
100
  def stream_value(self) -> Union[str, bool]:
96
101
  return (
97
102
  f'kwargs.pop("stream", {self.has_stream_response})'
98
- if self.expose_stream_keyword and self.has_response_body
103
+ if self.expose_stream_keyword and self.has_response_body and "stream" not in self.exact_name_params
99
104
  else self.has_stream_response
100
105
  )
101
106
 
@@ -87,6 +87,7 @@ class _ParameterBase(BaseModel, abc.ABC): # pylint: disable=too-many-instance-a
87
87
  self.default_to_unset_sentinel: bool = self.yaml_data.get("defaultToUnsetSentinel", False)
88
88
  self.hide_in_method: bool = self.yaml_data.get("hideInMethod", False)
89
89
  self.is_continuation_token: bool = bool(self.yaml_data.get("isContinuationToken"))
90
+ self.is_exact_name: bool = self.yaml_data.get("isExactName", False)
90
91
 
91
92
  def get_declaration(self, value: Any = None) -> Any:
92
93
  return self.type.get_declaration(value)
@@ -627,7 +627,8 @@ class _OperationSerializer(_BuilderBaseSerializer[OperationType]):
627
627
  return ""
628
628
 
629
629
  def pop_kwargs_from_signature(self, builder: OperationType) -> list[str]:
630
- kwargs_to_pop = builder.parameters.kwargs_to_pop
630
+ exact_names = builder.exact_name_params
631
+ kwargs_to_pop = [k for k in builder.parameters.kwargs_to_pop if k.client_name not in exact_names]
631
632
  kwargs = self.parameter_serializer.pop_kwargs_from_signature(
632
633
  kwargs_to_pop,
633
634
  check_kwarg_dict=True,
@@ -645,7 +646,7 @@ class _OperationSerializer(_BuilderBaseSerializer[OperationType]):
645
646
  body_parameter=builder.parameters.body_parameter if builder.parameters.has_body else None,
646
647
  )
647
648
  for p in builder.parameters.parameters:
648
- if p.hide_in_operation_signature and not p.is_continuation_token:
649
+ if p.hide_in_operation_signature and not p.is_continuation_token and p.client_name not in exact_names:
649
650
  kwargs.append(f'{p.client_name} = kwargs.pop("{p.client_name}", None)')
650
651
  cls_annotation = builder.cls_type_annotation(
651
652
  async_mode=self.async_mode, serialize_namespace=self.serialize_namespace
@@ -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}"')