@typespec/http-client-python 0.34.2 → 0.35.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 (45) hide show
  1. package/dist/emitter/code-model.d.ts.map +1 -1
  2. package/dist/emitter/code-model.js.map +1 -1
  3. package/dist/emitter/types.d.ts.map +1 -1
  4. package/dist/emitter/types.js +5 -1
  5. package/dist/emitter/types.js.map +1 -1
  6. package/emitter/src/code-model.ts +1 -2
  7. package/emitter/src/types.ts +6 -2
  8. package/emitter/temp/tsconfig.tsbuildinfo +1 -1
  9. package/eng/scripts/ci/config/pylintrc +5 -1
  10. package/eng/scripts/ci/dev_requirements.txt +3 -3
  11. package/eng/scripts/ci/regenerate-common.ts +26 -98
  12. package/eng/scripts/ci/regenerate.ts +2 -2
  13. package/eng/scripts/setup/__pycache__/package_manager.cpython-311.pyc +0 -0
  14. package/eng/scripts/setup/__pycache__/venvtools.cpython-311.pyc +0 -0
  15. package/generator/build/lib/pygen/codegen/models/client.py +15 -0
  16. package/generator/build/lib/pygen/codegen/models/operation.py +10 -2
  17. package/generator/build/lib/pygen/codegen/models/operation_group.py +2 -0
  18. package/generator/build/lib/pygen/codegen/models/primitive_types.py +5 -0
  19. package/generator/build/lib/pygen/codegen/serializers/builder_serializer.py +53 -5
  20. package/generator/build/lib/pygen/codegen/serializers/general_serializer.py +21 -1
  21. package/generator/build/lib/pygen/codegen/serializers/model_serializer.py +28 -5
  22. package/generator/build/lib/pygen/codegen/serializers/parameter_serializer.py +17 -3
  23. package/generator/build/lib/pygen/codegen/serializers/types_serializer.py +42 -14
  24. package/generator/build/lib/pygen/codegen/templates/model_base.py.jinja2 +17 -1
  25. package/generator/build/lib/pygen/codegen/templates/serialization.py.jinja2 +5 -1
  26. package/generator/build/lib/pygen/codegen/templates/validation.py.jinja2 +1 -1
  27. package/generator/build/lib/pygen/preprocess/__init__.py +19 -0
  28. package/generator/build/lib/pygen/utils.py +1 -1
  29. package/generator/component-detection-pip-report.json +3 -3
  30. package/generator/dist/pygen-0.1.0-py3-none-any.whl +0 -0
  31. package/generator/pygen/codegen/models/client.py +15 -0
  32. package/generator/pygen/codegen/models/operation.py +10 -2
  33. package/generator/pygen/codegen/models/operation_group.py +2 -0
  34. package/generator/pygen/codegen/models/primitive_types.py +5 -0
  35. package/generator/pygen/codegen/serializers/builder_serializer.py +53 -5
  36. package/generator/pygen/codegen/serializers/general_serializer.py +21 -1
  37. package/generator/pygen/codegen/serializers/model_serializer.py +28 -5
  38. package/generator/pygen/codegen/serializers/parameter_serializer.py +17 -3
  39. package/generator/pygen/codegen/serializers/types_serializer.py +42 -14
  40. package/generator/pygen/codegen/templates/model_base.py.jinja2 +17 -1
  41. package/generator/pygen/codegen/templates/serialization.py.jinja2 +5 -1
  42. package/generator/pygen/codegen/templates/validation.py.jinja2 +1 -1
  43. package/generator/pygen/preprocess/__init__.py +19 -0
  44. package/generator/pygen/utils.py +1 -1
  45. package/package.json +7 -4
@@ -16,8 +16,12 @@ enable=useless-suppression
16
16
  # cyclic-import: because of https://github.com/PyCQA/pylint/issues/850
17
17
  # too-many-arguments: Due to the nature of the CLI many commands have large arguments set which reflect in large arguments set in corresponding methods.
18
18
  # too-many-lines: Due to code generation many files end up with too many lines.
19
+ # no-cross-package-private-import: The guideline checker does not resolve relative
20
+ # imports (it ignores the import level), so legitimate same-package relative imports
21
+ # like `from ...operations._operations import ...` in generated async code are
22
+ # misreported as cross-package private imports.
19
23
  # Let's black deal with bad-continuation
20
- disable=missing-docstring,locally-disabled,fixme,cyclic-import,too-many-arguments,invalid-name,duplicate-code,too-few-public-methods,consider-using-f-string,super-with-arguments,redefined-builtin,import-outside-toplevel,client-suffix-needed,unnecessary-dunder-call,unnecessary-ellipsis,disallowed-name,consider-using-max-builtin,unknown-option-value,file-needs-copyright-header,too-many-positional-arguments
24
+ disable=missing-docstring,locally-disabled,fixme,cyclic-import,too-many-arguments,invalid-name,duplicate-code,too-few-public-methods,consider-using-f-string,super-with-arguments,redefined-builtin,import-outside-toplevel,client-suffix-needed,unnecessary-dunder-call,unnecessary-ellipsis,disallowed-name,consider-using-max-builtin,unknown-option-value,file-needs-copyright-header,too-many-positional-arguments,no-cross-package-private-import
21
25
 
22
26
  [FORMAT]
23
27
  max-line-length=120
@@ -1,8 +1,8 @@
1
- pyright==1.1.407
2
- pylint==4.0.4
1
+ pyright==1.1.411
2
+ pylint==4.0.6
3
3
  tox==4.16.0
4
4
  tox-uv
5
- mypy==1.19.1
5
+ mypy==2.1.0
6
6
  colorama==0.4.6
7
7
  debugpy==1.8.2
8
8
  pytest==9.0.3
@@ -16,8 +16,7 @@
16
16
  import { compile, NodeHost } from "@typespec/compiler";
17
17
  import { execSync } from "child_process";
18
18
  import { existsSync, rmSync } from "fs";
19
- import { access, cp, mkdir, mkdtemp, readdir, writeFile } from "fs/promises";
20
- import { tmpdir } from "os";
19
+ import { access, mkdir, readdir, writeFile } from "fs/promises";
21
20
  import { dirname, join, relative, resolve } from "path";
22
21
  import pc from "picocolors";
23
22
 
@@ -184,6 +183,9 @@ export const AZURE_EMITTER_OPTIONS: Record<
184
183
  "client/overload": {
185
184
  namespace: "client.overload",
186
185
  },
186
+ "encode/boolean": {
187
+ namespace: "encode.boolean",
188
+ },
187
189
  "encode/duration": {
188
190
  namespace: "encode.duration",
189
191
  },
@@ -652,6 +654,28 @@ export async function runParallel(
652
654
  return results;
653
655
  }
654
656
 
657
+ /**
658
+ * Removes every generated file under `tests/generated`, keeping only the
659
+ * hand-authored files that are tracked in git. `git clean -xdf` deletes ignored
660
+ * and untracked files but never tracked ones, so `.gitignore` stays the single
661
+ * source of truth for which fixtures survive regeneration (each exception there
662
+ * documents why it cannot be regenerated).
663
+ */
664
+ export async function cleanGeneratedCode(generatedFolder: string): Promise<void> {
665
+ const repoDir = resolve(generatedFolder, "..");
666
+ const testsGeneratedDir = resolve(repoDir, "tests/generated");
667
+ if (!existsSync(testsGeneratedDir)) return;
668
+
669
+ console.log(pc.cyan(`\n${"=".repeat(60)}`));
670
+ console.log(pc.cyan(`Cleaning generated test packages`));
671
+ console.log(pc.cyan(`${"=".repeat(60)}\n`));
672
+
673
+ execSync(`git clean -x -d -f -q -- "${testsGeneratedDir}"`, {
674
+ cwd: repoDir,
675
+ stdio: ["ignore", "inherit", "inherit"],
676
+ });
677
+ }
678
+
655
679
  /**
656
680
  * Pre-create the marker files that the test harness expects to find before
657
681
  * regeneration so it can verify they're cleared/preserved correctly.
@@ -695,99 +719,3 @@ export async function preprocess(flavor: string, generatedFolder: string): Promi
695
719
  }),
696
720
  );
697
721
  }
698
-
699
- /**
700
- * Resets the `tests/generated/{azure,unbranded}` baseline by sparse-checking-out
701
- * `eng/tools/azure-sdk-tools/emitter/generated` from the Azure/azure-sdk-for-python repo, then
702
- * deleting a couple of fully-generated package folders so regeneration has to
703
- * recreate them from scratch (smoke test of full-emit path).
704
- *
705
- * `generatedFolder` is the per-repo `generator/` directory; baseline lands at
706
- * `<generatedFolder>/../tests/generated`.
707
- */
708
- export async function prepareBaselineOfGeneratedCode(generatedFolder: string): Promise<void> {
709
- const repoUrl = "https://github.com/Azure/azure-sdk-for-python.git";
710
- const branch = "typespec-python-generated-tests";
711
- const sourceSubdir = "eng/tools/azure-sdk-tools/emitter/generated";
712
- const testsGeneratedDir = resolve(generatedFolder, "../tests/generated");
713
-
714
- console.log(pc.cyan(`\n${"=".repeat(60)}`));
715
- console.log(pc.cyan(`Resetting baseline from ${repoUrl} (${branch}/${sourceSubdir})`));
716
- console.log(pc.cyan(`${"=".repeat(60)}\n`));
717
-
718
- // Wipe tests/generated
719
- if (existsSync(testsGeneratedDir)) {
720
- console.log(pc.dim(`Removing ${testsGeneratedDir}`));
721
- rmSync(testsGeneratedDir, { recursive: true, force: true });
722
- }
723
-
724
- // Sparse-checkout the baseline folder into a temp directory
725
- const tempDir = await mkdtemp(join(tmpdir(), "azsdk-baseline-"));
726
- try {
727
- console.log(pc.dim(`Cloning into ${tempDir}`));
728
- const run = (cmd: string) =>
729
- execSync(cmd, { cwd: tempDir, stdio: ["ignore", "ignore", "inherit"] });
730
-
731
- run(`git init`);
732
- run(`git config core.longpaths true`);
733
- run(`git remote add origin ${repoUrl}`);
734
- run(`git config core.sparseCheckout true`);
735
- run(`git sparse-checkout init --cone`);
736
- run(`git sparse-checkout set ${sourceSubdir}`);
737
- run(`git fetch --depth 1 origin ${branch}`);
738
- run(`git checkout FETCH_HEAD`);
739
-
740
- // we don't copy whole generated folder, just the specific subfolders needed for tests
741
- // to verify correct preservation/deletion of files and folders during regeneration,
742
- // to avoid accidentally including any manually edited code that might be in the repo
743
- // and cause confusion when it doesn't get updated during regeneration
744
- const legacyCodePathNeededForTests = [
745
- "azure/authentication-api-key",
746
- "unbranded/authentication-api-key",
747
- "azure/authentication-union",
748
- "azure/generation-subdir",
749
- "azure/generation-subdir2",
750
- "unbranded/generation-subdir",
751
- "unbranded/generation-subdir2",
752
- "azure/azure-client-generator-core-alternate-type",
753
- ];
754
-
755
- const sourceRoot = join(tempDir, ...sourceSubdir.split("/"));
756
- for (const subPath of legacyCodePathNeededForTests) {
757
- const segments = subPath.split("/");
758
- const src = join(sourceRoot, ...segments);
759
- const dest = join(testsGeneratedDir, ...segments);
760
- if (!existsSync(src)) {
761
- console.warn(pc.yellow(`Baseline folder not found: ${src}`));
762
- continue;
763
- }
764
- console.log(pc.dim(`Copying ${subPath} -> ${dest}`));
765
- await mkdir(dirname(dest), { recursive: true });
766
- await cp(src, dest, { recursive: true });
767
- }
768
-
769
- console.log(pc.green(`Baseline reset complete.\n`));
770
- } finally {
771
- rmSync(tempDir, { recursive: true, force: true });
772
- }
773
-
774
- // Smoke test the full-emit path: delete a couple of fully-generated package
775
- // folders and every README.md so regeneration has to recreate them.
776
- const deleteIfExists = (path: string) => {
777
- if (!existsSync(path)) return;
778
- console.log(pc.dim(`Deleting ${path}`));
779
- rmSync(path, { recursive: true, force: true });
780
- };
781
-
782
- deleteIfExists(join(testsGeneratedDir, "azure", "authentication-http-custom"));
783
- deleteIfExists(join(testsGeneratedDir, "unbranded", "encode-array"));
784
-
785
- if (existsSync(testsGeneratedDir)) {
786
- const entries = await readdir(testsGeneratedDir, { recursive: true, withFileTypes: true });
787
- for (const entry of entries) {
788
- if (entry.isFile() && entry.name === "README.md") {
789
- deleteIfExists(join(entry.parentPath, entry.name));
790
- }
791
- }
792
- }
793
- }
@@ -22,8 +22,8 @@ import { parseArgs } from "util";
22
22
 
23
23
  import {
24
24
  buildTaskGroups,
25
+ cleanGeneratedCode,
25
26
  getSubdirectories,
26
- prepareBaselineOfGeneratedCode,
27
27
  preprocess,
28
28
  RegenerateContext,
29
29
  RegenerateFlags,
@@ -256,7 +256,7 @@ async function main() {
256
256
  const startTime = performance.now();
257
257
  let success: boolean;
258
258
 
259
- await prepareBaselineOfGeneratedCode(GENERATED_FOLDER);
259
+ await cleanGeneratedCode(GENERATED_FOLDER);
260
260
 
261
261
  if (flavor) {
262
262
  success = await regenerateFlavor(flavor, name, debug, jobs);
@@ -53,6 +53,17 @@ class _ClientConfigBase(Generic[ParameterListType], BaseModel):
53
53
  def name(self) -> str:
54
54
  return self.yaml_data["name"]
55
55
 
56
+ @property
57
+ def has_docstring_only_keyword(self) -> bool:
58
+ """Whether the constructor docstring documents a ``:keyword:`` that is not a real
59
+ keyword-only argument (it is popped from ``kwargs`` instead).
60
+
61
+ This happens for params routed to ``**kwargs`` (e.g. ``api_version``): they are
62
+ still rendered as ``:keyword:`` in the docstring, so we must silence the
63
+ ``docstring-keyword-should-match-keyword-only`` guideline check.
64
+ """
65
+ return any(p for p in self.parameters.method if p.method_location == ParameterMethodLocation.KWARG)
66
+
56
67
 
57
68
  class Client(_ClientConfigBase[ClientGlobalParameterList]):
58
69
  """Model representing our service client"""
@@ -187,6 +198,8 @@ class Client(_ClientConfigBase[ClientGlobalParameterList]):
187
198
  retval = add_to_pylint_disable(retval, "too-many-instance-attributes")
188
199
  if len(self.name) > NAME_LENGTH_LIMIT:
189
200
  retval = add_to_pylint_disable(retval, "name-too-long")
201
+ if self.has_docstring_only_keyword or self.has_public_lro_operations:
202
+ retval = add_to_pylint_disable(retval, "docstring-keyword-should-match-keyword-only")
190
203
  return retval
191
204
 
192
205
  @property
@@ -375,6 +388,8 @@ class Config(_ClientConfigBase[ConfigGlobalParameterList]):
375
388
  retval = add_to_pylint_disable("", "too-many-instance-attributes") if self.code_model.is_azure_flavor else ""
376
389
  if len(self.name) > NAME_LENGTH_LIMIT:
377
390
  retval = add_to_pylint_disable(retval, "name-too-long")
391
+ if self.has_docstring_only_keyword:
392
+ retval = add_to_pylint_disable(retval, "docstring-keyword-should-match-keyword-only")
378
393
  return retval
379
394
 
380
395
  @property
@@ -33,7 +33,7 @@ from .parameter import (
33
33
  )
34
34
  from .parameter_list import ParameterList
35
35
  from .model_type import ModelType
36
- from .primitive_types import BinaryIteratorType, BinaryType
36
+ from .primitive_types import BinaryIteratorType, BinaryType, ByteArraySchema
37
37
  from .base import BaseType
38
38
  from .combined_type import CombinedType
39
39
  from .request_builder import OverloadedRequestBuilder, RequestBuilder
@@ -224,7 +224,15 @@ class OperationBase( # pylint: disable=too-many-public-methods,too-many-instanc
224
224
 
225
225
  @property
226
226
  def need_import_iobase(self) -> bool:
227
- return self.parameters.has_body and isinstance(self.parameters.body_parameter.type, CombinedType)
227
+ if not (self.parameters.has_body and isinstance(self.parameters.body_parameter.type, CombinedType)):
228
+ return False
229
+ # For a binary `bytes` body the combined type is `bytes` + `IO[bytes]`. Both overloads
230
+ # serialize to raw content, so the body serialization collapses to a single assignment
231
+ # without an `isinstance` branch. In that case `IOBase` is never referenced, so avoid
232
+ # importing it to prevent an unused-import lint error in the generated code.
233
+ if any(isinstance(t, ByteArraySchema) for t in self.parameters.body_parameter.type.types):
234
+ return False
235
+ return True
228
236
 
229
237
  @staticmethod
230
238
  def has_kwargs_to_pop_with_default(
@@ -82,6 +82,8 @@ class OperationGroup(BaseModel):
82
82
  retval: str = ""
83
83
  if self.has_abstract_operations:
84
84
  retval = add_to_pylint_disable(retval, "abstract-class-instantiated")
85
+ if not self.is_mixin:
86
+ retval = add_to_pylint_disable(retval, "docstring-missing-param")
85
87
  if len(self.operations) > 20:
86
88
  retval = add_to_pylint_disable(retval, "too-many-public-methods")
87
89
  if len(self.class_name) > NAME_LENGTH_LIMIT and self.class_name[0] != "_":
@@ -50,6 +50,11 @@ class PrimitiveType(BaseType):
50
50
 
51
51
 
52
52
  class BooleanType(PrimitiveType):
53
+ def __init__(self, yaml_data: dict[str, Any], code_model: "CodeModel") -> None:
54
+ super().__init__(yaml_data=yaml_data, code_model=code_model)
55
+ if yaml_data.get("encode") == "string":
56
+ self.encode = "str"
57
+
53
58
  def serialization_type(self, **kwargs: Any) -> str:
54
59
  return "bool"
55
60
 
@@ -462,6 +462,7 @@ class RequestBuilderSerializer(_BuilderBaseSerializer[RequestBuilderType]):
462
462
  check_kwarg_dict=True,
463
463
  pop_headers_kwarg=(PopKwargType.CASE_INSENSITIVE if bool(builder.parameters.headers) else PopKwargType.NO),
464
464
  pop_params_kwarg=(PopKwargType.CASE_INSENSITIVE if bool(builder.parameters.query) else PopKwargType.NO),
465
+ is_body_optional=builder.parameters.has_body and builder.parameters.body_parameter.optional,
465
466
  )
466
467
 
467
468
  @staticmethod
@@ -491,6 +492,7 @@ class RequestBuilderSerializer(_BuilderBaseSerializer[RequestBuilderType]):
491
492
  for h in builder.parameters.headers
492
493
  if not builder.has_form_data_body or h.wire_name.lower() != "content-type"
493
494
  ]
495
+ is_body_optional = builder.parameters.has_body and builder.parameters.body_parameter.optional
494
496
  retval = ["# Construct headers"] if headers else []
495
497
  for header in headers:
496
498
  retval.extend(
@@ -499,6 +501,7 @@ class RequestBuilderSerializer(_BuilderBaseSerializer[RequestBuilderType]):
499
501
  "headers",
500
502
  self.serializer_name,
501
503
  self.code_model.is_legacy,
504
+ is_body_optional=is_body_optional,
502
505
  )
503
506
  )
504
507
  return retval
@@ -793,6 +796,34 @@ class _OperationSerializer(_BuilderBaseSerializer[OperationType]):
793
796
  retval.extend(self._serialize_body_parameter(builder))
794
797
  return retval
795
798
 
799
+ @staticmethod
800
+ def _collapsed_binary_bytes_overload(builder: OperationType) -> Optional[OperationType]:
801
+ """Return the binary overload of a binary ``bytes`` body, or ``None``.
802
+
803
+ A binary ``bytes`` body pairs a ``bytes`` overload (binary content type) with the added
804
+ ``IO[bytes]`` overload. Both serialize to raw content on the same content kwarg, so body
805
+ serialization collapses to a single unconditional assignment. This returns the binary
806
+ overload to serialize in that case, or ``None`` when the isinstance branch is needed.
807
+ """
808
+ if not builder.overloads:
809
+ return None
810
+ binary_ov = next(
811
+ (o for o in builder.overloads if isinstance(o.parameters.body_parameter.type, BinaryType)), None
812
+ )
813
+ other_ov = next(
814
+ (o for o in builder.overloads if not isinstance(o.parameters.body_parameter.type, BinaryType)), None
815
+ )
816
+ if (
817
+ binary_ov is not None
818
+ and other_ov is not None
819
+ and isinstance(other_ov.parameters.body_parameter.type, ByteArraySchema)
820
+ and other_ov.parameters.body_parameter.default_content_type != "application/json"
821
+ and binary_ov.request_builder.parameters.body_parameter.client_name
822
+ == other_ov.request_builder.parameters.body_parameter.client_name
823
+ ):
824
+ return cast(OperationType, binary_ov)
825
+ return None
826
+
796
827
  def _initialize_overloads(self, builder: OperationType, is_paging: bool = False) -> list[str]:
797
828
  retval: list[str] = []
798
829
  # For paging, we put body parameter in local place outside `prepare_request`
@@ -817,9 +848,26 @@ class _OperationSerializer(_BuilderBaseSerializer[OperationType]):
817
848
  all_dpg_model_overloads = all(
818
849
  _is_dpg_or_typeddict_body(o.parameters.body_parameter) for o in builder.overloads
819
850
  )
820
- if not all_dpg_model_overloads:
851
+ # A binary `bytes` body pairs a `bytes` overload (binary content type) with the added
852
+ # `IO[bytes]` overload. Both serialize to raw content on the same content kwarg, so we
853
+ # emit a single unconditional assignment instead of an `isinstance` branch (which would be
854
+ # redundant and confuse mypy's type narrowing). Since the assignment is unconditional, the
855
+ # `_<body> = None` pre-init below is skipped for this case as well.
856
+ collapsed_binary_bytes_overload = self._collapsed_binary_bytes_overload(builder)
857
+
858
+ if not all_dpg_model_overloads and collapsed_binary_bytes_overload is None:
821
859
  for v in sorted(set(client_names), key=client_names.index):
822
860
  retval.append(f"_{v} = None")
861
+
862
+ if collapsed_binary_bytes_overload is not None:
863
+ collapsed_body_param = collapsed_binary_bytes_overload.parameters.body_parameter
864
+ if collapsed_body_param.default_content_type and not same_content_type:
865
+ retval.append(
866
+ f'content_type = content_type or "{collapsed_body_param.default_content_type}"{check_body_suffix}'
867
+ )
868
+ retval.extend(self._create_body_parameter(collapsed_binary_bytes_overload))
869
+ return retval
870
+
823
871
  try:
824
872
  # if there is a binary overload, we do a binary check first.
825
873
  binary_overload = cast(
@@ -827,6 +875,10 @@ class _OperationSerializer(_BuilderBaseSerializer[OperationType]):
827
875
  next((o for o in builder.overloads if isinstance(o.parameters.body_parameter.type, BinaryType))),
828
876
  )
829
877
  binary_body_param = binary_overload.parameters.body_parameter
878
+ other_overload = cast(
879
+ OperationType,
880
+ next((o for o in builder.overloads if not isinstance(o.parameters.body_parameter.type, BinaryType))),
881
+ )
830
882
  retval.append(f"if {binary_body_param.type.instance_check_template.format(binary_body_param.client_name)}:")
831
883
  if binary_body_param.default_content_type and not same_content_type:
832
884
  retval.append(
@@ -834,10 +886,6 @@ class _OperationSerializer(_BuilderBaseSerializer[OperationType]):
834
886
  )
835
887
  retval.extend(f" {l}" for l in self._create_body_parameter(binary_overload))
836
888
  retval.append("else:")
837
- other_overload = cast(
838
- OperationType,
839
- next((o for o in builder.overloads if not isinstance(o.parameters.body_parameter.type, BinaryType))),
840
- )
841
889
  retval.extend(f" {l}" for l in self._create_body_parameter(other_overload))
842
890
  if other_overload.parameters.body_parameter.default_content_type and not same_content_type:
843
891
  retval.append(
@@ -320,7 +320,27 @@ class GeneralSerializer(BaseSerializer):
320
320
 
321
321
  def serialize_validation_file(self) -> str:
322
322
  template = self.env.get_template("validation.py.jinja2")
323
- return template.render(code_model=self.code_model)
323
+ return template.render(
324
+ code_model=self.code_model,
325
+ client_api_version_name=self._api_version_config_attr_name(),
326
+ )
327
+
328
+ def _api_version_config_attr_name(self) -> str:
329
+ """Name of the config attribute that stores the API version.
330
+
331
+ The attribute name is derived from the API-version parameter's ``client_name``
332
+ (e.g. ``apiVersion`` -> ``api_version``, or Storage's ``version`` -> ``version``).
333
+ ``_validation.py`` holds a single shared decorator for the whole package, so when
334
+ every client agrees on the name we bake it in directly; otherwise we fall back to
335
+ the conventional ``api_version``.
336
+ """
337
+ names = {
338
+ parameter.client_name
339
+ for client in self.code_model.clients
340
+ for parameter in client.config.parameters.parameters
341
+ if parameter.is_api_version
342
+ }
343
+ return next(iter(names)) if len(names) == 1 else "api_version"
324
344
 
325
345
  def serialize_cross_language_definition_file(self) -> str:
326
346
  cross_langauge_def_dict = {
@@ -23,7 +23,7 @@ from ..models.primitive_types import (
23
23
  )
24
24
  from .import_serializer import FileImportSerializer
25
25
  from .base_serializer import BaseSerializer
26
- from ..models.utils import NamespaceType
26
+ from ..models.utils import NamespaceType, add_to_pylint_disable
27
27
 
28
28
 
29
29
  def _get_xml_deserializer_name(prop: Property) -> Optional[str]: # pylint: disable=too-many-return-statements
@@ -77,7 +77,10 @@ def _documentation_string(
77
77
  prop: Property, description_keyword: str, docstring_type_keyword: str, **kwargs: Any
78
78
  ) -> list[str]:
79
79
  retval: list[str] = []
80
- sphinx_prefix = f":{description_keyword} {prop.client_name}:"
80
+ doc_name = (
81
+ prop.wire_name if kwargs.get("serialize_namespace_type") == NamespaceType.TYPES_FILE else prop.client_name
82
+ )
83
+ sphinx_prefix = f":{description_keyword} {doc_name}:"
81
84
  description = prop.description(is_operation_file=False).replace("\\", "\\\\")
82
85
  retval.append(f"{sphinx_prefix} {description}" if description else sphinx_prefix)
83
86
  # In the types file, use type_annotation (the serialized form) for docstrings
@@ -85,7 +88,7 @@ def _documentation_string(
85
88
  doc_type = prop.type.type_annotation(**kwargs)
86
89
  else:
87
90
  doc_type = prop.type.docstring_type(**kwargs)
88
- retval.append(f":{docstring_type_keyword} {prop.client_name}: {doc_type}")
91
+ retval.append(f":{docstring_type_keyword} {doc_name}: {doc_type}")
89
92
  return retval
90
93
 
91
94
 
@@ -199,6 +202,16 @@ class _ModelSerializer(BaseSerializer, ABC):
199
202
  def pylint_disable(self, model: ModelType) -> str:
200
203
  return " # pylint: disable=" + ", ".join(self.pylint_disable_items(model))
201
204
 
205
+ def class_pylint_disable(self, model: ModelType) -> str:
206
+ """Class-level pylint disables for the model declaration line."""
207
+ retval = model.pylint_disable()
208
+ # When the model's only constructor is ``def __init__(self, *args, **kwargs)`` (i.e. no
209
+ # typed overload is generated), the guideline checker reports the ``*args`` vararg as an
210
+ # undocumented param. There is no meaningful param to document, so silence the check.
211
+ if not self.need_init(model) and self.initialize_properties(model):
212
+ retval = add_to_pylint_disable(retval, "docstring-missing-param")
213
+ return retval
214
+
202
215
  def global_pylint_disables(self) -> str:
203
216
  return ""
204
217
 
@@ -244,7 +257,7 @@ class MsrestModelSerializer(_ModelSerializer):
244
257
  )
245
258
  if model.parents:
246
259
  basename = ", ".join([m.name for m in model.parents])
247
- return f"class {model.name}({basename}):{model.pylint_disable()}"
260
+ return f"class {model.name}({basename}):{self.class_pylint_disable(model)}"
248
261
 
249
262
  @staticmethod
250
263
  def get_properties_to_initialize(model: ModelType) -> list[Property]:
@@ -407,7 +420,17 @@ class DpgModelSerializer(_ModelSerializer):
407
420
  basename = ", ".join([m.name for m in model.parents])
408
421
  if model.discriminator_value:
409
422
  basename += f", discriminator='{model.discriminator_value}'"
410
- return f"class {model.name}({basename}):{model.pylint_disable()}"
423
+ return f"class {model.name}({basename}):{self.class_pylint_disable(model)}"
424
+
425
+ def class_pylint_disable(self, model: ModelType) -> str:
426
+ retval = super().class_pylint_disable(model)
427
+ # DPG models render an empty ``@overload def __init__(self, *, ...)`` body, so the
428
+ # guideline checker validates the keyword-only arguments against the *class* docstring.
429
+ # We only document those arguments as instance variables (``:ivar:``) to avoid a redundant
430
+ # ``:keyword:`` entry per property, so silence the keyword/keyword-only correlation check.
431
+ if self._init_line_parameters(model):
432
+ retval = add_to_pylint_disable(retval, "docstring-keyword-should-match-keyword-only")
433
+ return retval
411
434
 
412
435
  @staticmethod
413
436
  def get_properties_to_declare(model: ModelType) -> list[Property]:
@@ -139,6 +139,8 @@ class ParameterSerializer:
139
139
  kwarg_name: str,
140
140
  serializer_name: str,
141
141
  is_legacy: bool,
142
+ *,
143
+ is_body_optional: bool = False,
142
144
  ) -> list[str]:
143
145
  if (
144
146
  not is_legacy
@@ -166,7 +168,11 @@ class ParameterSerializer:
166
168
  param.wire_name,
167
169
  self.serialize_parameter(param, serializer_name),
168
170
  )
169
- if not param.optional and (param.in_method_signature or param.constant):
171
+ # When the body is optional, the content-type may legitimately be ``None`` (no body
172
+ # was sent). In that case the header must be omitted instead of serialized, otherwise
173
+ # ``_SERIALIZER.header`` raises ``ValueError: No value for given attribute``.
174
+ is_content_type_optional = getattr(param, "is_content_type", False) and is_body_optional
175
+ if not param.optional and not is_content_type_optional and (param.in_method_signature or param.constant):
170
176
  retval = [set_parameter]
171
177
  else:
172
178
  retval = [
@@ -184,6 +190,7 @@ class ParameterSerializer:
184
190
  check_client_input: bool = False,
185
191
  *,
186
192
  body_parameter: Optional[BodyParameterType] = None,
193
+ is_body_optional: Optional[bool] = None,
187
194
  ) -> list[str]:
188
195
  retval = []
189
196
 
@@ -195,7 +202,8 @@ class ParameterSerializer:
195
202
 
196
203
  append_pop_kwarg("headers", pop_headers_kwarg)
197
204
  append_pop_kwarg("params", pop_params_kwarg)
198
- is_body_optional = check_body_optional(body_parameter)
205
+ if is_body_optional is None:
206
+ is_body_optional = check_body_optional(body_parameter)
199
207
  if pop_headers_kwarg != PopKwargType.NO or pop_params_kwarg != PopKwargType.NO:
200
208
  retval.append("")
201
209
  for kwarg in parameters:
@@ -206,7 +214,13 @@ class ParameterSerializer:
206
214
  if is_content_type_optional and not type_annotation.startswith("Optional[")
207
215
  else type_annotation
208
216
  )
209
- if kwarg.client_default_value is not None or kwarg.optional or kwarg.constant or kwarg.is_api_version:
217
+ if (
218
+ kwarg.client_default_value is not None
219
+ or kwarg.optional
220
+ or kwarg.constant
221
+ or kwarg.is_api_version
222
+ or is_content_type_optional
223
+ ):
210
224
  if check_client_input and kwarg.check_client_input:
211
225
  default_value = f"self._config.{kwarg.client_name}"
212
226
  elif kwarg.constant:
@@ -40,13 +40,38 @@ _BUILTIN_TYPE_NAMES = frozenset(
40
40
  )
41
41
 
42
42
 
43
+ # Matches a single- or double-quoted string literal (escape-aware). Used to split an
44
+ # annotation so that bare-identifier rewriting/detection skips text inside string literals
45
+ # such as ``Literal["type"]`` values or quoted forward references like ``"SomeModel"``.
46
+ _STRING_LITERAL_RE = re.compile(r"""('(?:[^'\\]|\\.)*'|"(?:[^"\\]|\\.)*")""")
47
+
48
+
49
+ def _annotation_references_builtin(annotation: str, name: str) -> bool:
50
+ """Whether ``name`` appears as a bare (non-dotted) identifier outside any string literal."""
51
+ pattern = re.compile(rf"(?<!\.)\b{name}\b")
52
+ # re.split with a single capturing group yields code/string/code/string/... segments.
53
+ for index, segment in enumerate(_STRING_LITERAL_RE.split(annotation)):
54
+ if index % 2 == 0 and pattern.search(segment):
55
+ return True
56
+ return False
57
+
58
+
43
59
  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."""
60
+ """Replace bare builtin type references with builtins.X when shadowed by a field name.
61
+
62
+ Only bare identifiers in real type-expression positions are rewritten; identifiers
63
+ inside string literals (e.g. ``Literal["type"]`` values, quoted forward references)
64
+ and already-dotted names are left untouched.
65
+ """
45
66
  if not shadowed:
46
67
  return annotation
47
- for name in shadowed:
48
- annotation = re.sub(rf"\b{name}\b", f"builtins.{name}", annotation)
49
- return annotation
68
+ segments = _STRING_LITERAL_RE.split(annotation)
69
+ for index in range(0, len(segments), 2): # even indices are code, odd are string literals
70
+ segment = segments[index]
71
+ for name in shadowed:
72
+ segment = re.sub(rf"(?<!\.)\b{name}\b", f"builtins.{name}", segment)
73
+ segments[index] = segment
74
+ return "".join(segments)
50
75
 
51
76
 
52
77
  class TypesSerializer(BaseSerializer):
@@ -164,9 +189,7 @@ class TypesSerializer(BaseSerializer):
164
189
  property — are kept, ensuring no forward reference is left undefined.
165
190
  """
166
191
  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
- ]
192
+ candidates = [m for m in self._models if m.base != "json" and not m.discriminated_subtypes and m.name in needed]
170
193
  seen_names: dict[str, "ModelType"] = {}
171
194
  result: list["ModelType"] = []
172
195
  for m in candidates:
@@ -225,22 +248,27 @@ class TypesSerializer(BaseSerializer):
225
248
  """Whether any property wire_name is a Python keyword or requires functional TypedDict form."""
226
249
  return any(keyword.iskeyword(p.wire_name) or not p.wire_name.isidentifier() for p in model.properties)
227
250
 
228
- @staticmethod
229
- def get_shadowed_builtins(model: ModelType) -> frozenset[str]:
251
+ def get_shadowed_builtins(self, model: ModelType) -> frozenset[str]:
230
252
  """Return the set of builtin type names shadowed by property wire_names in this model.
231
253
 
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).
254
+ Only includes a builtin if it is both used as a wire_name AND referenced as a bare
255
+ type in an annotation within the same model (otherwise no shadowing occurs). Detection
256
+ uses the exact ``TYPES_FILE`` annotation emitted for each property so that types which
257
+ change under the types-file serialization (e.g. ``bytes``/``datetime`` -> ``str``,
258
+ ``decimal`` -> ``float``) are evaluated as they actually appear in the output.
234
259
  """
235
260
  wire_builtins = {p.wire_name for p in model.properties if p.wire_name in _BUILTIN_TYPE_NAMES}
236
261
  if not wire_builtins:
237
262
  return frozenset()
238
- # Check which of these builtins actually appear in type annotations
263
+ # Check which of these builtins actually appear in emitted type annotations
239
264
  used = set()
240
265
  for prop in model.properties:
241
- annotation = prop.type_annotation()
266
+ annotation = prop.type_annotation(
267
+ serialize_namespace=self.serialize_namespace,
268
+ serialize_namespace_type=NamespaceType.TYPES_FILE,
269
+ )
242
270
  for name in wire_builtins:
243
- if re.search(rf"\b{name}\b", annotation):
271
+ if _annotation_references_builtin(annotation, name):
244
272
  used.add(name)
245
273
  return frozenset(used)
246
274