@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.
- package/dist/emitter/code-model.d.ts.map +1 -1
- package/dist/emitter/code-model.js.map +1 -1
- package/dist/emitter/types.d.ts.map +1 -1
- package/dist/emitter/types.js +5 -1
- package/dist/emitter/types.js.map +1 -1
- package/emitter/src/code-model.ts +1 -2
- package/emitter/src/types.ts +6 -2
- package/emitter/temp/tsconfig.tsbuildinfo +1 -1
- package/eng/scripts/ci/config/pylintrc +5 -1
- package/eng/scripts/ci/dev_requirements.txt +3 -3
- package/eng/scripts/ci/regenerate-common.ts +26 -98
- package/eng/scripts/ci/regenerate.ts +2 -2
- package/eng/scripts/setup/__pycache__/package_manager.cpython-311.pyc +0 -0
- package/eng/scripts/setup/__pycache__/venvtools.cpython-311.pyc +0 -0
- package/generator/build/lib/pygen/codegen/models/client.py +15 -0
- package/generator/build/lib/pygen/codegen/models/operation.py +10 -2
- package/generator/build/lib/pygen/codegen/models/operation_group.py +2 -0
- package/generator/build/lib/pygen/codegen/models/primitive_types.py +5 -0
- package/generator/build/lib/pygen/codegen/serializers/builder_serializer.py +53 -5
- package/generator/build/lib/pygen/codegen/serializers/general_serializer.py +21 -1
- package/generator/build/lib/pygen/codegen/serializers/model_serializer.py +28 -5
- package/generator/build/lib/pygen/codegen/serializers/parameter_serializer.py +17 -3
- package/generator/build/lib/pygen/codegen/serializers/types_serializer.py +42 -14
- package/generator/build/lib/pygen/codegen/templates/model_base.py.jinja2 +17 -1
- package/generator/build/lib/pygen/codegen/templates/serialization.py.jinja2 +5 -1
- package/generator/build/lib/pygen/codegen/templates/validation.py.jinja2 +1 -1
- package/generator/build/lib/pygen/preprocess/__init__.py +19 -0
- package/generator/build/lib/pygen/utils.py +1 -1
- package/generator/component-detection-pip-report.json +3 -3
- package/generator/dist/pygen-0.1.0-py3-none-any.whl +0 -0
- package/generator/pygen/codegen/models/client.py +15 -0
- package/generator/pygen/codegen/models/operation.py +10 -2
- package/generator/pygen/codegen/models/operation_group.py +2 -0
- package/generator/pygen/codegen/models/primitive_types.py +5 -0
- package/generator/pygen/codegen/serializers/builder_serializer.py +53 -5
- package/generator/pygen/codegen/serializers/general_serializer.py +21 -1
- package/generator/pygen/codegen/serializers/model_serializer.py +28 -5
- package/generator/pygen/codegen/serializers/parameter_serializer.py +17 -3
- package/generator/pygen/codegen/serializers/types_serializer.py +42 -14
- package/generator/pygen/codegen/templates/model_base.py.jinja2 +17 -1
- package/generator/pygen/codegen/templates/serialization.py.jinja2 +5 -1
- package/generator/pygen/codegen/templates/validation.py.jinja2 +1 -1
- package/generator/pygen/preprocess/__init__.py +19 -0
- package/generator/pygen/utils.py +1 -1
- 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
|
|
@@ -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,
|
|
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
|
|
259
|
+
await cleanGeneratedCode(GENERATED_FOLDER);
|
|
260
260
|
|
|
261
261
|
if (flavor) {
|
|
262
262
|
success = await regenerateFlavor(flavor, name, debug, jobs);
|
|
Binary file
|
|
Binary file
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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} {
|
|
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}):{
|
|
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}):{
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
271
|
+
if _annotation_references_builtin(annotation, name):
|
|
244
272
|
used.add(name)
|
|
245
273
|
return frozenset(used)
|
|
246
274
|
|