diff --git a/packages/http-client-python/CHANGELOG.md b/packages/http-client-python/CHANGELOG.md index c72de8a4ad..8e8ad21123 100644 --- a/packages/http-client-python/CHANGELOG.md +++ b/packages/http-client-python/CHANGELOG.md @@ -1,5 +1,11 @@ # Change Log - @typespec/http-client-python +## 0.6.0 + +### Features + +- Add support for typespec namespace + ## 0.5.1 ### Bug Fixes diff --git a/packages/http-client-python/emitter/src/code-model.ts b/packages/http-client-python/emitter/src/code-model.ts index 87518b2546..398fdc8f4e 100644 --- a/packages/http-client-python/emitter/src/code-model.ts +++ b/packages/http-client-python/emitter/src/code-model.ts @@ -32,7 +32,12 @@ import { simpleTypesMap, typesMap, } from "./types.js"; -import { emitParamBase, getImplementation, removeUnderscoresFromNamespace } from "./utils.js"; +import { + emitParamBase, + getClientNamespace, + getImplementation, + removeUnderscoresFromNamespace, +} from "./utils.js"; function emitBasicMethod( context: PythonSdkContext, @@ -193,6 +198,7 @@ function emitOperationGroups( propertyName: operationGroup.name, operations: operations, operationGroups: emitOperationGroups(context, operationGroup, rootClient, name), + clientNamespace: getClientNamespace(context, operationGroup.clientNamespace), }); } } @@ -210,10 +216,18 @@ function emitOperationGroups( className: "", propertyName: "", operations: operations, + clientNamespace: getClientNamespace(context, client.clientNamespace), }); } } + // operation has same clientNamespace as the operation group + for (const og of operationGroups) { + for (const op of og.operations) { + op.clientNamespace = getClientNamespace(context, og.clientNamespace); + } + } + return operationGroups.length > 0 ? operationGroups : undefined; } @@ -247,6 +261,7 @@ function emitClient( url, apiVersions: client.apiVersions, arm: context.arm, + clientNamespace: getClientNamespace(context, client.clientNamespace), }; } @@ -268,14 +283,9 @@ export function emitCodeModel( const codeModel: Record = { namespace: removeUnderscoresFromNamespace(sdkPackage.rootNamespace).toLowerCase(), clients: [], - subnamespaceToClients: {}, }; for (const client of sdkPackage.clients) { codeModel["clients"].push(emitClient(sdkContext, client)); - if (client.nameSpace === sdkPackage.rootNamespace) { - } else { - codeModel["subnamespaceToClients"][client.nameSpace] = emitClient(sdkContext, client); - } } // loop through models and enums since there may be some orphaned models needs to be generated for (const model of sdkPackage.models) { diff --git a/packages/http-client-python/emitter/src/emitter.ts b/packages/http-client-python/emitter/src/emitter.ts index d4fe079e51..6a36b3eba2 100644 --- a/packages/http-client-python/emitter/src/emitter.ts +++ b/packages/http-client-python/emitter/src/emitter.ts @@ -57,6 +57,9 @@ function addDefaultOptions(sdkContext: SdkContext) { if (!options.flavor && sdkContext.emitContext.emitterOutputDir.includes("azure")) { options.flavor = "azure"; } + if (options["enable-typespec-namespace"] === undefined) { + options["enable-typespec-namespace"] = options.flavor !== "azure"; + } } async function createPythonSdkContext( @@ -79,6 +82,7 @@ export async function $onEmit(context: EmitContext) { const sdkContext = await createPythonSdkContext(context); const root = path.join(dirname(fileURLToPath(import.meta.url)), "..", ".."); const outputDir = context.emitterOutputDir; + addDefaultOptions(sdkContext); const yamlMap = emitCodeModel(sdkContext); if (yamlMap.clients.length === 0) { reportDiagnostic(program, { @@ -88,7 +92,6 @@ export async function $onEmit(context: EmitContext) { return; } const yamlPath = await saveCodeModelAsYaml("python-yaml-path", yamlMap); - addDefaultOptions(sdkContext); const resolvedOptions = sdkContext.emitContext.options; const commandArgs: Record = {}; if (resolvedOptions["packaging-files-config"]) { diff --git a/packages/http-client-python/emitter/src/lib.ts b/packages/http-client-python/emitter/src/lib.ts index b028aaeadc..c106d9b14f 100644 --- a/packages/http-client-python/emitter/src/lib.ts +++ b/packages/http-client-python/emitter/src/lib.ts @@ -17,6 +17,9 @@ export interface PythonEmitterOptions { debug?: boolean; flavor?: "azure"; "examples-dir"?: string; + // If true, package namespace will respect the typespec namespace. Otherwise, + // package namespace is always aligned with package name. + "enable-typespec-namespace"?: boolean; "use-pyodide"?: boolean; } @@ -44,6 +47,7 @@ const EmitterOptionsSchema: JSONSchemaType = { debug: { type: "boolean", nullable: true }, flavor: { type: "string", nullable: true }, "examples-dir": { type: "string", nullable: true, format: "absolute-path" }, + "enable-typespec-namespace": { type: "boolean", nullable: true }, "use-pyodide": { type: "boolean", nullable: true }, }, required: [], diff --git a/packages/http-client-python/emitter/src/types.ts b/packages/http-client-python/emitter/src/types.ts index 356ce0c54a..4f13a5b96f 100644 --- a/packages/http-client-python/emitter/src/types.ts +++ b/packages/http-client-python/emitter/src/types.ts @@ -21,7 +21,13 @@ import { Type } from "@typespec/compiler"; import { HttpAuth, Visibility } from "@typespec/http"; import { dump } from "js-yaml"; import { PythonSdkContext } from "./lib.js"; -import { camelToSnakeCase, emitParamBase, getAddedOn, getImplementation } from "./utils.js"; +import { + camelToSnakeCase, + emitParamBase, + getAddedOn, + getClientNamespace, + getImplementation, +} from "./utils.js"; export const typesMap = new Map>(); export const simpleTypesMap = new Map>(); @@ -75,7 +81,7 @@ export function getType( case "union": return emitUnion(context, type); case "enum": - return emitEnum(type); + return emitEnum(context, type); case "constant": return emitConstant(type)!; case "array": @@ -86,7 +92,7 @@ export function getType( case "duration": return emitDurationOrDateType(type); case "enumvalue": - return emitEnumMember(type, emitEnum(type.enumType)); + return emitEnumMember(type, emitEnum(context, type.enumType)); case "credential": return emitCredential(type); case "bytes": @@ -289,6 +295,7 @@ function emitModel( usage: type.usage, isXml: type.usage & UsageFlags.Xml ? true : false, xmlMetadata: type.usage & UsageFlags.Xml ? getXmlMetadata(type) : undefined, + clientNamespace: getClientNamespace(context, type.clientNamespace), }; typesMap.set(type, newValue); @@ -314,7 +321,10 @@ function emitModel( return newValue; } -function emitEnum(type: SdkEnumType): Record { +function emitEnum( + context: PythonSdkContext, + type: SdkEnumType, +): Record { if (typesMap.has(type)) { return typesMap.get(type)!; } @@ -352,6 +362,7 @@ function emitEnum(type: SdkEnumType): Record { values, xmlMetadata: {}, crossLanguageDefinitionId: type.crossLanguageDefinitionId, + clientNamespace: getClientNamespace(context, type.clientNamespace), }; for (const value of type.values) { newValue.values.push(emitEnumMember(value, newValue)); @@ -469,6 +480,7 @@ function emitUnion( type: "combined", types: type.variantTypes.map((x) => getType(context, x)), xmlMetadata: {}, + clientNamespace: getClientNamespace(context, type.clientNamespace), }); } diff --git a/packages/http-client-python/emitter/src/utils.ts b/packages/http-client-python/emitter/src/utils.ts index 6bdde071c1..4028a421fe 100644 --- a/packages/http-client-python/emitter/src/utils.ts +++ b/packages/http-client-python/emitter/src/utils.ts @@ -199,3 +199,30 @@ export function isAzureCoreErrorResponse(t: SdkType | undefined): boolean { export function capitalize(name: string): string { return name[0].toUpperCase() + name.slice(1); } + +export function getClientNamespace( + context: PythonSdkContext, + clientNamespace: string, +) { + const rootNamespace = removeUnderscoresFromNamespace( + context.sdkPackage.rootNamespace, + ).toLowerCase(); + if (!context.emitContext.options["enable-typespec-namespace"]) { + return rootNamespace; + } + if ( + [ + "azure.core", + "azure.resourcemanager", + "azure.clientgenerator.core", + "typespec.rest", + "typespec.http", + "typespec.versioning", + ].some((item) => clientNamespace.toLowerCase().startsWith(item)) + ) { + return rootNamespace; + } + return clientNamespace === "" + ? rootNamespace + : removeUnderscoresFromNamespace(clientNamespace).toLowerCase(); +} diff --git a/packages/http-client-python/eng/scripts/ci/regenerate.ts b/packages/http-client-python/eng/scripts/ci/regenerate.ts index c762d8ad10..ab3b786610 100644 --- a/packages/http-client-python/eng/scripts/ci/regenerate.ts +++ b/packages/http-client-python/eng/scripts/ci/regenerate.ts @@ -123,6 +123,9 @@ const EMITTER_OPTIONS: Record | Record> = { @@ -267,6 +271,9 @@ function addOptions( options["company-name"] = "Unbranded"; } options["examples-dir"] = toPosix(join(dirname(spec), "examples")); + if (options["enable-typespec-namespace"] === undefined) { + options["enable-typespec-namespace"] = "false"; + } const configs = Object.entries(options).flatMap(([k, v]) => { return `--option ${argv.values.emitterName || "@typespec/http-client-python"}.${k}=${v}`; }); diff --git a/packages/http-client-python/generator/pygen/black.py b/packages/http-client-python/generator/pygen/black.py index 39dee18947..d20931a214 100644 --- a/packages/http-client-python/generator/pygen/black.py +++ b/packages/http-client-python/generator/pygen/black.py @@ -43,8 +43,8 @@ def process(self) -> bool: "venv", "env", ) - and not Path(f).parts[0].startswith(".") - and Path(f).suffix == ".py" + # we shall also format generated files like "../../../generated_tests/test_xxx.py" + and (not Path(f).parts[0].startswith(".") or Path(f).parts[0] == "..") and Path(f).suffix == ".py" ], ) ) diff --git a/packages/http-client-python/generator/pygen/codegen/__init__.py b/packages/http-client-python/generator/pygen/codegen/__init__.py index 07bd5b46f1..f9d5446e15 100644 --- a/packages/http-client-python/generator/pygen/codegen/__init__.py +++ b/packages/http-client-python/generator/pygen/codegen/__init__.py @@ -40,6 +40,7 @@ class OptionsRetriever: "generate-test": False, "from-typespec": False, "emit-cross-language-definition-file": False, + "enable-typespec-namespace": False, } @property @@ -316,6 +317,7 @@ def _build_code_model_options(self) -> Dict[str, Any]: "flavor", "company_name", "emit_cross_language_definition_file", + "enable_typespec_namespace", ] return {f: getattr(self.options_retriever, f) for f in flags} diff --git a/packages/http-client-python/generator/pygen/codegen/_utils.py b/packages/http-client-python/generator/pygen/codegen/_utils.py index 726ccc3f2f..6ea066031f 100644 --- a/packages/http-client-python/generator/pygen/codegen/_utils.py +++ b/packages/http-client-python/generator/pygen/codegen/_utils.py @@ -15,3 +15,7 @@ TYPESPEC_PACKAGE_MODE = ["azure-mgmt", "azure-dataplane", "generic"] VALID_PACKAGE_MODE = SWAGGER_PACKAGE_MODE + TYPESPEC_PACKAGE_MODE NAME_LENGTH_LIMIT = 40 + + +def get_parent_namespace(namespace: str) -> str: + return namespace.rsplit(".", 1)[0] diff --git a/packages/http-client-python/generator/pygen/codegen/models/base.py b/packages/http-client-python/generator/pygen/codegen/models/base.py index 516cade3bc..c6bb12e85e 100644 --- a/packages/http-client-python/generator/pygen/codegen/models/base.py +++ b/packages/http-client-python/generator/pygen/codegen/models/base.py @@ -87,8 +87,7 @@ def xml_serialization_ctxt(self) -> Optional[str]: attrs_list.append("'text': True") return ", ".join(attrs_list) - @property - def serialization_type(self) -> str: + def serialization_type(self, **kwargs: Any) -> str: """The tag recognized by 'msrest' as a serialization/deserialization. 'str', 'int', 'float', 'bool' or @@ -103,7 +102,7 @@ def serialization_type(self) -> str: @property def msrest_deserialization_key(self) -> str: - return self.serialization_type + return self.serialization_type() @property def client_default_value(self) -> Any: diff --git a/packages/http-client-python/generator/pygen/codegen/models/base_builder.py b/packages/http-client-python/generator/pygen/codegen/models/base_builder.py index 905195ed7c..28e883b07d 100644 --- a/packages/http-client-python/generator/pygen/codegen/models/base_builder.py +++ b/packages/http-client-python/generator/pygen/codegen/models/base_builder.py @@ -73,6 +73,7 @@ def __init__( self.api_versions: List[str] = yaml_data["apiVersions"] self.added_on: Optional[str] = yaml_data.get("addedOn") self.external_docs: Optional[Dict[str, Any]] = yaml_data.get("externalDocs") + self.client_namespace: str = yaml_data.get("clientNamespace", code_model.namespace) if code_model.options["version_tolerant"] and yaml_data.get("abstract"): _LOGGER.warning( @@ -113,7 +114,7 @@ def description(self) -> str: ) return self._description or self.name - def method_signature(self, async_mode: bool) -> List[str]: + def method_signature(self, async_mode: bool, **kwargs: Any) -> List[str]: if self.abstract: return ["*args,", "**kwargs"] - return self.parameters.method_signature(async_mode) + return self.parameters.method_signature(async_mode, **kwargs) diff --git a/packages/http-client-python/generator/pygen/codegen/models/client.py b/packages/http-client-python/generator/pygen/codegen/models/client.py index b2fe0fda10..4623326f6c 100644 --- a/packages/http-client-python/generator/pygen/codegen/models/client.py +++ b/packages/http-client-python/generator/pygen/codegen/models/client.py @@ -43,6 +43,7 @@ def __init__( self.parameters = parameters self.url: str = self.yaml_data["url"] # the base endpoint of the client. Can be parameterized or not self.legacy_filename: str = self.yaml_data.get("legacyFilename", "client") + self.client_namespace: str = self.yaml_data.get("clientNamespace", code_model.namespace) @property def description(self) -> str: @@ -188,7 +189,7 @@ def lookup_operation(self, operation_id: int) -> "OperationType": except StopIteration as exc: raise KeyError(f"No operation with id {operation_id} found.") from exc - def _imports_shared(self, async_mode: bool) -> FileImport: + def _imports_shared(self, async_mode: bool, **kwargs) -> FileImport: file_import = FileImport(self.code_model) file_import.add_submodule_import("typing", "Any", ImportType.STDLIB, TypingSection.CONDITIONAL) if self.code_model.options["azure_arm"]: @@ -206,8 +207,8 @@ def _imports_shared(self, async_mode: bool) -> FileImport: file_import.merge( gp.imports( async_mode, - relative_path=".." if async_mode else ".", - operation=True, + is_operation_file=True, + **kwargs, ) ) file_import.add_submodule_import( @@ -215,8 +216,9 @@ def _imports_shared(self, async_mode: bool) -> FileImport: f"{self.name}Configuration", ImportType.LOCAL, ) + serialize_namespace = kwargs.get("serialize_namespace", self.code_model.namespace) file_import.add_msrest_import( - relative_path=".." if async_mode else ".", + serialize_namespace=serialize_namespace, msrest_import_type=MsrestImportType.SerializerDeserializer, typing_section=TypingSection.REGULAR, ) @@ -277,8 +279,8 @@ def has_non_abstract_operations(self) -> bool: """Whether there is non-abstract operation in any operation group.""" return any(og.has_non_abstract_operations for og in self.operation_groups) - def imports(self, async_mode: bool) -> FileImport: - file_import = self._imports_shared(async_mode) + def imports(self, async_mode: bool, **kwargs) -> FileImport: + file_import = self._imports_shared(async_mode, **kwargs) if async_mode: file_import.add_submodule_import("typing", "Awaitable", ImportType.STDLIB) file_import.add_submodule_import( @@ -300,9 +302,13 @@ def imports(self, async_mode: bool) -> FileImport: ImportType.SDKCORE, TypingSection.CONDITIONAL, ) + serialize_namespace = kwargs.get("serialize_namespace", self.code_model.namespace) for og in self.operation_groups: file_import.add_submodule_import( - f".{self.code_model.operations_folder_name}", + self.code_model.get_relative_import_path( + serialize_namespace, + self.code_model.get_imported_namespace_for_operation(og.client_namespace, async_mode), + ), og.class_name, ImportType.LOCAL, ) @@ -317,8 +323,8 @@ def imports(self, async_mode: bool) -> FileImport: file_import.add_submodule_import("copy", "deepcopy", ImportType.STDLIB) return file_import - def imports_for_multiapi(self, async_mode: bool) -> FileImport: - file_import = self._imports_shared(async_mode) + def imports_for_multiapi(self, async_mode: bool, **kwargs) -> FileImport: + file_import = self._imports_shared(async_mode, **kwargs) file_import.add_submodule_import("typing", "Optional", ImportType.STDLIB, TypingSection.CONDITIONAL) try: mixin_operation = next(og for og in self.operation_groups if og.is_mixin) @@ -377,7 +383,7 @@ def sdk_moniker(self) -> str: def name(self) -> str: return f"{super().name}Configuration" - def _imports_shared(self, async_mode: bool) -> FileImport: + def _imports_shared(self, async_mode: bool, **kwargs: Any) -> FileImport: file_import = FileImport(self.code_model) file_import.add_submodule_import( "pipeline" if self.code_model.is_azure_flavor else "runtime", @@ -386,7 +392,12 @@ def _imports_shared(self, async_mode: bool) -> FileImport: ) file_import.add_submodule_import("typing", "Any", ImportType.STDLIB, TypingSection.CONDITIONAL) if self.code_model.options["package_version"]: - file_import.add_submodule_import(".._version" if async_mode else "._version", "VERSION", ImportType.LOCAL) + serialize_namespace = kwargs.get("serialize_namespace", self.code_model.namespace) + file_import.add_submodule_import( + self.code_model.get_relative_import_path(serialize_namespace, module_name="_version"), + "VERSION", + ImportType.LOCAL, + ) if self.code_model.options["azure_arm"]: policy = "AsyncARMChallengeAuthenticationPolicy" if async_mode else "ARMChallengeAuthenticationPolicy" file_import.add_submodule_import("azure.mgmt.core.policies", "ARMHttpLoggingPolicy", ImportType.SDKCORE) @@ -394,22 +405,21 @@ def _imports_shared(self, async_mode: bool) -> FileImport: return file_import - def imports(self, async_mode: bool) -> FileImport: - file_import = self._imports_shared(async_mode) + def imports(self, async_mode: bool, **kwargs) -> FileImport: + file_import = self._imports_shared(async_mode, **kwargs) for gp in self.parameters: if gp.method_location == ParameterMethodLocation.KWARG and gp not in self.parameters.kwargs_to_pop: continue file_import.merge( gp.imports( async_mode=async_mode, - relative_path=".." if async_mode else ".", - operation=True, + **kwargs, ) ) return file_import - def imports_for_multiapi(self, async_mode: bool) -> FileImport: - file_import = self._imports_shared(async_mode) + def imports_for_multiapi(self, async_mode: bool, **kwargs: Any) -> FileImport: + file_import = self._imports_shared(async_mode, **kwargs) for gp in self.parameters: if ( gp.method_location == ParameterMethodLocation.KWARG @@ -420,8 +430,7 @@ def imports_for_multiapi(self, async_mode: bool) -> FileImport: file_import.merge( gp.imports_for_multiapi( async_mode=async_mode, - relative_path=".." if async_mode else ".", - operation=True, + **kwargs, ) ) return file_import diff --git a/packages/http-client-python/generator/pygen/codegen/models/code_model.py b/packages/http-client-python/generator/pygen/codegen/models/code_model.py index 7ddb90fe37..5d5cf2b16a 100644 --- a/packages/http-client-python/generator/pygen/codegen/models/code_model.py +++ b/packages/http-client-python/generator/pygen/codegen/models/code_model.py @@ -3,7 +3,7 @@ # Licensed under the MIT License. See License.txt in the project root for # license information. # -------------------------------------------------------------------------- -from typing import List, Dict, Any, Set, Union, Literal +from typing import List, Dict, Any, Set, Union, Literal, Optional, cast from .base import BaseType from .enum_type import EnumType @@ -11,12 +11,40 @@ from .combined_type import CombinedType from .client import Client from .request_builder import RequestBuilder, OverloadedRequestBuilder +from .operation_group import OperationGroup +from .utils import NamespaceType def _is_legacy(options) -> bool: return not (options.get("version_tolerant") or options.get("low_level_client")) +def get_all_operation_groups_recursively(clients: List[Client]) -> List[OperationGroup]: + operation_groups = [] + queue = [] + for client in clients: + queue.extend(client.operation_groups) + while queue: + operation_groups.append(queue.pop(0)) + if operation_groups[-1].operation_groups: + queue.extend(operation_groups[-1].operation_groups) + return operation_groups + + +class ClientNamespaceType: + def __init__( + self, + clients: Optional[List[Client]] = None, + models: Optional[List[ModelType]] = None, + enums: Optional[List[EnumType]] = None, + operation_groups: Optional[List[OperationGroup]] = None, + ): + self.clients = clients or [] + self.models = models or [] + self.enums = enums or [] + self.operation_groups = operation_groups or [] + + class CodeModel: # pylint: disable=too-many-public-methods, disable=too-many-instance-attributes """Top level code model @@ -44,8 +72,6 @@ def __init__( self, yaml_data: Dict[str, Any], options: Dict[str, Any], - *, - is_subnamespace: bool = False, ) -> None: self.yaml_data = yaml_data self.options = options @@ -59,18 +85,113 @@ def __init__( self.clients: List[Client] = [ Client.from_yaml(client_yaml_data, self) for client_yaml_data in yaml_data["clients"] ] - self.subnamespace_to_clients: Dict[str, List[Client]] = { - subnamespace: [Client.from_yaml(client_yaml, self, is_subclient=True) for client_yaml in client_yamls] - for subnamespace, client_yamls in yaml_data.get("subnamespaceToClients", {}).items() - } if self.options["models_mode"] and self.model_types: self.sort_model_types() - self.is_subnamespace = is_subnamespace self.named_unions: List[CombinedType] = [ t for t in self.types_map.values() if isinstance(t, CombinedType) and t.name ] self.cross_language_package_id = self.yaml_data.get("crossLanguagePackageId") self.for_test: bool = False + # key is typespec namespace, value is models/clients/opeartion_groups/enums cache in the namespace + self._client_namespace_types: Dict[str, ClientNamespaceType] = {} + self.has_subnamespace = False + self._operations_folder_name: Dict[str, str] = {} + self._relative_import_path: Dict[str, str] = {} + + @staticmethod + def get_imported_namespace_for_client(imported_namespace: str, async_mode: bool = False) -> str: + return imported_namespace + (".aio" if async_mode else "") + + @staticmethod + def get_imported_namespace_for_model(imported_namespace: str) -> str: + return imported_namespace + ".models" + + def get_imported_namespace_for_operation(self, imported_namespace: str, async_mode: bool = False) -> str: + module_namespace = f".{self.operations_folder_name(imported_namespace)}" + return self.get_imported_namespace_for_client(imported_namespace, async_mode) + module_namespace + + # | serialize_namespace | imported_namespace | relative_import_path | + # |----------------------|----------------------|----------------------| + # |azure.test.operations | azure.test.operations| . | + # |azure.test.operations | azure.test | .. | + # |azure.test.operations | azure.test.subtest | ..subtest | + # |azure.test.operations | azure | ... | + # |azure.test.aio.operations | azure.test | ... | + # |azure.test.subtest.aio.operations|azure.test | .... | + # |azure.test |azure.test.subtest | .subtest | + def get_relative_import_path( + self, + serialize_namespace: str, + imported_namespace: Optional[str] = None, + module_name: Optional[str] = None, + ) -> str: + if imported_namespace is None: + imported_namespace = self.namespace + + key = f"{serialize_namespace}-{imported_namespace}" + if key not in self._relative_import_path: + idx = 0 + serialize_namespace_split = serialize_namespace.split(".") + imported_namespace_split = cast(str, imported_namespace).split(".") + while idx < min(len(serialize_namespace_split), len(imported_namespace_split)): + if serialize_namespace_split[idx] != imported_namespace_split[idx]: + break + idx += 1 + self._relative_import_path[key] = "." * (len(serialize_namespace_split[idx:]) + 1) + ".".join( + imported_namespace_split[idx:] + ) + result = self._relative_import_path[key] + if module_name is None: + return result + return f"{result}{module_name}" if result.endswith(".") else f"{result}.{module_name}" + + @property + def need_unique_model_alias(self) -> bool: + return self.has_subnamespace and self.options["enable_typespec_namespace"] + + def get_unique_models_alias(self, serialize_namespace: str, imported_namespace: str) -> str: + if not self.need_unique_model_alias: + return "_models" + relative_path = self.get_relative_import_path( + serialize_namespace, self.get_imported_namespace_for_model(imported_namespace) + ) + dot_num = max(relative_path.count(".") - 1, 0) + parts = [""] + ([p for p in relative_path.split(".") if p] or ["models"]) + return "_".join(parts) + (str(dot_num) if dot_num > 0 else "") + + @property + def client_namespace_types(self) -> Dict[str, ClientNamespaceType]: + if not self._client_namespace_types: + # calculate client namespace types for each kind of client namespace + for client in self.clients: + if client.client_namespace not in self._client_namespace_types: + self._client_namespace_types[client.client_namespace] = ClientNamespaceType() + self._client_namespace_types[client.client_namespace].clients.append(client) + for model in self.model_types: + if model.client_namespace not in self._client_namespace_types: + self._client_namespace_types[model.client_namespace] = ClientNamespaceType() + self._client_namespace_types[model.client_namespace].models.append(model) + for enum in self.enums: + if enum.client_namespace not in self._client_namespace_types: + self._client_namespace_types[enum.client_namespace] = ClientNamespaceType() + self._client_namespace_types[enum.client_namespace].enums.append(enum) + for operation_group in get_all_operation_groups_recursively(self.clients): + if operation_group.client_namespace not in self._client_namespace_types: + self._client_namespace_types[operation_group.client_namespace] = ClientNamespaceType() + self._client_namespace_types[operation_group.client_namespace].operation_groups.append(operation_group) + + # here we can check and record whether there are multi kinds of client namespace + if len(self._client_namespace_types.keys()) > 1: + self.has_subnamespace = True + + # insert namespace to make sure it is continuous(e.g. ("", "azure", "azure.mgmt", "azure.mgmt.service")) + longest_namespace = sorted(self._client_namespace_types.keys())[-1] + namespace_parts = longest_namespace.split(".") + for idx in range(len(namespace_parts) + 1): + namespace = ".".join(namespace_parts[:idx]) + if namespace not in self._client_namespace_types: + self._client_namespace_types[namespace] = ClientNamespaceType() + return self._client_namespace_types @property def has_form_data(self) -> bool: @@ -80,17 +201,17 @@ def has_form_data(self) -> bool: def has_etag(self) -> bool: return any(client.has_etag for client in self.clients) + @staticmethod + def clients_has_operations(clients: List[Client]) -> bool: + return any(c for c in clients if c.has_operations) + @property def has_operations(self) -> bool: - if any(c for c in self.clients if c.has_operations): - return True - return any(c for clients in self.subnamespace_to_clients.values() for c in clients if c.has_operations) + return self.clients_has_operations(self.clients) @property def has_non_abstract_operations(self) -> bool: - return any(c for c in self.clients if c.has_non_abstract_operations) or any( - c for cs in self.subnamespace_to_clients.values() for c in cs if c.has_non_abstract_operations - ) + return any(c for c in self.clients if c.has_non_abstract_operations) def lookup_request_builder(self, request_builder_id: int) -> Union[RequestBuilder, OverloadedRequestBuilder]: """Find the request builder based off of id""" @@ -114,31 +235,73 @@ def rest_layer_name(self) -> str: def client_filename(self) -> str: return self.clients[0].filename - def need_vendored_code(self, async_mode: bool) -> bool: - """Whether we need to vendor code in the _vendor.py file for this SDK""" - if self.has_abstract_operations: - return True - if async_mode: - return self.need_mixin_abc - return self.need_mixin_abc or self.has_etag or self.has_form_data + def get_clients(self, client_namespace: str) -> List[Client]: + """Get all clients in specific namespace""" + return self.client_namespace_types.get(client_namespace, ClientNamespaceType()).clients - @property - def need_mixin_abc(self) -> bool: - return any(c for c in self.clients if c.has_mixin) + def is_top_namespace(self, client_namespace: str) -> bool: + """Whether the namespace is the top namespace. For example, a package named 'azure-mgmt-service', + 'azure.mgmt.service' is the top namespace. + """ + return client_namespace == self.namespace + + def need_vendored_code(self, async_mode: bool, client_namespace: str) -> bool: + """Whether we need to vendor code in the _vendor.py in specific namespace""" + return ( + self.need_vendored_form_data(async_mode, client_namespace) + or self.need_vendored_etag(client_namespace) + or self.need_vendored_abstract(client_namespace) + or self.need_vendored_mixin(client_namespace) + ) + + def need_vendored_form_data(self, async_mode: bool, client_namespace: str) -> bool: + return ( + (not async_mode) + and self.is_top_namespace(client_namespace) + and self.has_form_data + and self.options["models_mode"] == "dpg" + ) + + def need_vendored_etag(self, client_namespace: str) -> bool: + return self.is_top_namespace(client_namespace) and self.has_etag + + def need_vendored_abstract(self, client_namespace: str) -> bool: + return self.is_top_namespace(client_namespace) and self.has_abstract_operations + + def need_vendored_mixin(self, client_namespace: str) -> bool: + return self.has_mixin(client_namespace) + + def has_mixin(self, client_namespace: str) -> bool: + return any(c for c in self.get_clients(client_namespace) if c.has_mixin) @property def has_abstract_operations(self) -> bool: return any(c for c in self.clients if c.has_abstract_operations) - @property - def operations_folder_name(self) -> str: + def operations_folder_name(self, client_namespace: str) -> str: """Get the name of the operations folder that holds operations.""" - name = "operations" - if self.options["version_tolerant"] and not any( - og for client in self.clients for og in client.operation_groups if not og.is_mixin - ): - name = f"_{name}" - return name + if client_namespace not in self._operations_folder_name: + name = "operations" + operation_groups = self.client_namespace_types.get(client_namespace, ClientNamespaceType()).operation_groups + if self.options["version_tolerant"] and all(og.is_mixin for og in operation_groups): + name = f"_{name}" + self._operations_folder_name[client_namespace] = name + return self._operations_folder_name[client_namespace] + + def get_serialize_namespace( + self, + client_namespace: str, + async_mode: bool = False, + client_namespace_type: NamespaceType = NamespaceType.CLIENT, + ) -> str: + """calculate the namespace for serialization from client namespace""" + if client_namespace_type == NamespaceType.CLIENT: + return client_namespace + (".aio" if async_mode else "") + if client_namespace_type == NamespaceType.MODEL: + return client_namespace + ".models" + + operations_folder_name = self.operations_folder_name(client_namespace) + return client_namespace + (".aio." if async_mode else ".") + operations_folder_name @property def description(self) -> str: @@ -170,9 +333,13 @@ def model_types(self) -> List[ModelType]: def model_types(self, val: List[ModelType]) -> None: self._model_types = val + @staticmethod + def get_public_model_types(models: List[ModelType]) -> List[ModelType]: + return [m for m in models if not m.internal and not m.base == "json"] + @property def public_model_types(self) -> List[ModelType]: - return [m for m in self.model_types if not m.internal and not m.base == "json"] + return self.get_public_model_types(self.model_types) @property def enums(self) -> List[EnumType]: diff --git a/packages/http-client-python/generator/pygen/codegen/models/combined_type.py b/packages/http-client-python/generator/pygen/codegen/models/combined_type.py index 6afbe08ac9..8170758f23 100644 --- a/packages/http-client-python/generator/pygen/codegen/models/combined_type.py +++ b/packages/http-client-python/generator/pygen/codegen/models/combined_type.py @@ -8,6 +8,7 @@ from .imports import FileImport, ImportType, TypingSection from .base import BaseType from .model_type import ModelType +from .utils import NamespaceType if TYPE_CHECKING: from .code_model import CodeModel @@ -30,9 +31,9 @@ def __init__( self.types = types # the types that this type is combining self.name = yaml_data.get("name") self._is_union_of_literals = all(i.type == "constant" for i in self.types) + self.client_namespace: str = self.yaml_data.get("clientNamespace", code_model.namespace) - @property - def serialization_type(self) -> str: + def serialization_type(self, **kwargs: Any) -> str: """The tag recognized by 'msrest' as a serialization/deserialization. 'str', 'int', 'float', 'bool' or @@ -45,7 +46,7 @@ def serialization_type(self) -> str: """ if not all(t for t in self.types if t.type == "constant"): raise ValueError("Shouldn't get serialization type of a combinedtype") - return self.types[0].serialization_type + return self.types[0].serialization_type(**kwargs) @property def client_default_value(self) -> Any: @@ -112,9 +113,11 @@ def instance_check_template(self) -> str: def imports(self, **kwargs: Any) -> FileImport: file_import = FileImport(self.code_model) - if self.name and not kwargs.get("is_types_file"): + serialize_namespace = kwargs.get("serialize_namespace", self.code_model.namespace) + serialize_namespace_type = kwargs.get("serialize_namespace_type") + if self.name and serialize_namespace_type != NamespaceType.TYPES_FILE: file_import.add_submodule_import( - kwargs.pop("relative_path"), + self.code_model.get_relative_import_path(serialize_namespace), "_types", ImportType.LOCAL, TypingSection.TYPING, diff --git a/packages/http-client-python/generator/pygen/codegen/models/constant_type.py b/packages/http-client-python/generator/pygen/codegen/models/constant_type.py index 02e1bfaca2..b99b469a16 100644 --- a/packages/http-client-python/generator/pygen/codegen/models/constant_type.py +++ b/packages/http-client-python/generator/pygen/codegen/models/constant_type.py @@ -56,14 +56,13 @@ def description(self, *, is_operation_file: bool) -> str: f"Default value is {self.get_declaration()}.", ) - @property - def serialization_type(self) -> str: + def serialization_type(self, **kwargs: Any) -> str: """Returns the serialization value for msrest. :return: The serialization value for msrest :rtype: str """ - return self.value_type.serialization_type + return self.value_type.serialization_type(**kwargs) def docstring_text(self, **kwargs: Any) -> str: return "constant" diff --git a/packages/http-client-python/generator/pygen/codegen/models/credential_types.py b/packages/http-client-python/generator/pygen/codegen/models/credential_types.py index 82dd74302a..7dd617235d 100644 --- a/packages/http-client-python/generator/pygen/codegen/models/credential_types.py +++ b/packages/http-client-python/generator/pygen/codegen/models/credential_types.py @@ -131,8 +131,7 @@ def get_json_template_representation( def docstring_text(self, **kwargs: Any) -> str: return "credential" - @property - def serialization_type(self) -> str: + def serialization_type(self, **kwargs: Any) -> str: return self.docstring_type() @classmethod diff --git a/packages/http-client-python/generator/pygen/codegen/models/dictionary_type.py b/packages/http-client-python/generator/pygen/codegen/models/dictionary_type.py index 38b83dcbed..e467c626e7 100644 --- a/packages/http-client-python/generator/pygen/codegen/models/dictionary_type.py +++ b/packages/http-client-python/generator/pygen/codegen/models/dictionary_type.py @@ -34,14 +34,13 @@ def __init__( def encode(self) -> Optional[str]: return self.element_type.encode if hasattr(self.element_type, "encode") else None # type: ignore - @property - def serialization_type(self) -> str: + def serialization_type(self, **kwargs: Any) -> str: """Returns the serialization value for msrest. :return: The serialization value for msrest :rtype: str """ - return f"{{{self.element_type.serialization_type}}}" + return f"{{{self.element_type.serialization_type(**kwargs)}}}" def type_annotation(self, **kwargs: Any) -> str: """The python type used for type annotation diff --git a/packages/http-client-python/generator/pygen/codegen/models/enum_type.py b/packages/http-client-python/generator/pygen/codegen/models/enum_type.py index 56e712654f..02c04c2db0 100644 --- a/packages/http-client-python/generator/pygen/codegen/models/enum_type.py +++ b/packages/http-client-python/generator/pygen/codegen/models/enum_type.py @@ -7,6 +7,8 @@ from .base import BaseType from .imports import FileImport, ImportType, TypingSection +from .utils import NamespaceType + if TYPE_CHECKING: from .code_model import CodeModel @@ -63,9 +65,8 @@ def get_json_template_representation( client_default_value_declaration=client_default_value_declaration, ) - @property - def serialization_type(self) -> str: - return self.value_type.serialization_type + def serialization_type(self, **kwargs: Any) -> str: + return self.value_type.serialization_type(**kwargs) @property def instance_check_template(self) -> str: @@ -75,7 +76,17 @@ def imports(self, **kwargs: Any) -> FileImport: file_import = FileImport(self.code_model) file_import.merge(self.value_type.imports(**kwargs)) file_import.add_submodule_import("typing", "Literal", ImportType.STDLIB, TypingSection.REGULAR) - file_import.add_submodule_import("._enums", self.enum_type.name, ImportType.LOCAL, TypingSection.REGULAR) + serialize_namespace = kwargs.get("serialize_namespace", self.code_model.namespace) + file_import.add_submodule_import( + self.code_model.get_relative_import_path( + serialize_namespace, + self.code_model.get_imported_namespace_for_model(self.enum_type.client_namespace), + module_name=self.code_model.enums_filename, + ), + self.enum_type.name, + ImportType.LOCAL, + TypingSection.REGULAR, + ) return file_import @@ -124,18 +135,18 @@ def __init__( self.value_type = value_type self.internal: bool = self.yaml_data.get("internal", False) self.cross_language_definition_id: Optional[str] = self.yaml_data.get("crossLanguageDefinitionId") + self.client_namespace: str = self.yaml_data.get("clientNamespace", code_model.namespace) def __lt__(self, other): return self.name.lower() < other.name.lower() - @property - def serialization_type(self) -> str: + def serialization_type(self, **kwargs: Any) -> str: """Returns the serialization value for msrest. :return: The serialization value for msrest :rtype: str """ - return self.value_type.serialization_type + return self.value_type.serialization_type(**kwargs) def description(self, *, is_operation_file: bool) -> str: possible_values = [self.get_declaration(v.value) for v in self.values] @@ -160,7 +171,12 @@ def type_annotation(self, **kwargs: Any) -> str: :rtype: str """ if self.code_model.options["models_mode"]: - module_name = "_models." if kwargs.get("need_module_name", True) else "" + + module_name = "" + if kwargs.get("need_model_alias", True): + serialize_namespace = kwargs.get("serialize_namespace", self.code_model.namespace) + model_alias = self.code_model.get_unique_models_alias(serialize_namespace, self.client_namespace) + module_name = f"{model_alias}." file_name = f"{self.code_model.enums_filename}." if self.internal else "" model_name = module_name + file_name + self.name # we don't need quoted annotation in operation files, and need it in model folder files. @@ -212,27 +228,34 @@ def from_yaml(cls, yaml_data: Dict[str, Any], code_model: "CodeModel") -> "EnumT ) def imports(self, **kwargs: Any) -> FileImport: - operation = kwargs.pop("operation", False) file_import = FileImport(self.code_model) + file_import.merge(self.value_type.imports(**kwargs)) if self.code_model.options["models_mode"]: file_import.add_submodule_import("typing", "Union", ImportType.STDLIB, TypingSection.CONDITIONAL) - if not operation: + + serialize_namespace = kwargs.get("serialize_namespace", self.code_model.namespace) + relative_path = self.code_model.get_relative_import_path(serialize_namespace, self.client_namespace) + alias = self.code_model.get_unique_models_alias(serialize_namespace, self.client_namespace) + serialize_namespace_type = kwargs.get("serialize_namespace_type") + called_by_property = kwargs.get("called_by_property", False) + if serialize_namespace_type in [NamespaceType.OPERATION, NamespaceType.CLIENT]: file_import.add_submodule_import( - "..", + relative_path, "models", ImportType.LOCAL, - TypingSection.TYPING, - alias="_models", + alias=alias, + typing_section=TypingSection.REGULAR, ) - file_import.merge(self.value_type.imports(operation=operation, **kwargs)) - relative_path = kwargs.pop("relative_path", None) - if self.code_model.options["models_mode"] and relative_path: - # add import for enums in operations file - file_import.add_submodule_import( - relative_path, - "models", - ImportType.LOCAL, - alias="_models", - typing_section=(TypingSection.TYPING if kwargs.get("model_typing") else TypingSection.REGULAR), - ) + elif serialize_namespace_type == NamespaceType.TYPES_FILE or ( + serialize_namespace_type == NamespaceType.MODEL and called_by_property + ): + file_import.add_submodule_import( + relative_path, + "models", + ImportType.LOCAL, + alias=alias, + typing_section=TypingSection.TYPING, + ) + + file_import.merge(self.value_type.imports(**kwargs)) return file_import diff --git a/packages/http-client-python/generator/pygen/codegen/models/imports.py b/packages/http-client-python/generator/pygen/codegen/models/imports.py index c1f827b19c..ed98052a7b 100644 --- a/packages/http-client-python/generator/pygen/codegen/models/imports.py +++ b/packages/http-client-python/generator/pygen/codegen/models/imports.py @@ -5,6 +5,7 @@ # -------------------------------------------------------------------------- from enum import Enum, auto from typing import Dict, List, Optional, Tuple, Union, Set, TYPE_CHECKING +from .._utils import get_parent_namespace if TYPE_CHECKING: from .code_model import CodeModel @@ -259,7 +260,7 @@ def to_dict( def add_msrest_import( self, *, - relative_path: str, + serialize_namespace: str, msrest_import_type: MsrestImportType, typing_section: TypingSection, ): @@ -271,21 +272,22 @@ def add_msrest_import( if msrest_import_type == MsrestImportType.SerializerDeserializer: self.add_submodule_import("msrest", "Deserializer", ImportType.THIRDPARTY, typing_section) else: + # _serialization.py is always in root namespace + imported_namespace = self.code_model.namespace if self.code_model.options["multiapi"]: - relative_path += "." + # for multiapi, the namespace is azure.mgmt.xxx.v20XX_XX_XX while _serialization.py is in azure.mgmt.xxx + imported_namespace = get_parent_namespace(imported_namespace) if msrest_import_type == MsrestImportType.Module: - self.add_submodule_import(relative_path, "_serialization", ImportType.LOCAL, typing_section) - else: self.add_submodule_import( - f"{relative_path}_serialization", - "Serializer", + self.code_model.get_relative_import_path(serialize_namespace, imported_namespace), + "_serialization", ImportType.LOCAL, typing_section, ) + else: + relative_path = self.code_model.get_relative_import_path( + serialize_namespace, imported_namespace, module_name="_serialization" + ) + self.add_submodule_import(relative_path, "Serializer", ImportType.LOCAL, typing_section) if msrest_import_type == MsrestImportType.SerializerDeserializer: - self.add_submodule_import( - f"{relative_path}_serialization", - "Deserializer", - ImportType.LOCAL, - typing_section, - ) + self.add_submodule_import(relative_path, "Deserializer", ImportType.LOCAL, typing_section) diff --git a/packages/http-client-python/generator/pygen/codegen/models/list_type.py b/packages/http-client-python/generator/pygen/codegen/models/list_type.py index 8c29ed6561..1f7d3bfda1 100644 --- a/packages/http-client-python/generator/pygen/codegen/models/list_type.py +++ b/packages/http-client-python/generator/pygen/codegen/models/list_type.py @@ -29,9 +29,8 @@ def __init__( def encode(self) -> Optional[str]: return self.element_type.encode if hasattr(self.element_type, "encode") else None # type: ignore - @property - def serialization_type(self) -> str: - return f"[{self.element_type.serialization_type}]" + def serialization_type(self, **kwargs: Any) -> str: + return f"[{self.element_type.serialization_type(**kwargs)}]" def type_annotation(self, **kwargs: Any) -> str: if ( diff --git a/packages/http-client-python/generator/pygen/codegen/models/lro_operation.py b/packages/http-client-python/generator/pygen/codegen/models/lro_operation.py index 84647a037c..8717954aa5 100644 --- a/packages/http-client-python/generator/pygen/codegen/models/lro_operation.py +++ b/packages/http-client-python/generator/pygen/codegen/models/lro_operation.py @@ -92,9 +92,9 @@ def response_type_annotation(self, **kwargs) -> str: return lro_response.type_annotation(**kwargs) return "None" - def cls_type_annotation(self, *, async_mode: bool) -> str: + def cls_type_annotation(self, *, async_mode: bool, **kwargs: Any) -> str: """We don't want the poller to show up in ClsType, so we call super() on response type annotation""" - return f"ClsType[{Response.type_annotation(self.responses[0], async_mode=async_mode)}]" + return f"ClsType[{Response.type_annotation(self.responses[0], async_mode=async_mode, **kwargs)}]" def get_poller_with_response_type(self, async_mode: bool) -> str: return self.response_type_annotation(async_mode=async_mode) @@ -132,8 +132,12 @@ def imports(self, async_mode: bool, **kwargs: Any) -> FileImport: ): # used in the case if initial operation returns none # but final call returns a model - relative_path = "..." if async_mode else ".." - file_import.add_submodule_import(f"{relative_path}_model_base", "_deserialize", ImportType.LOCAL) + serialize_namespace = kwargs.get("serialize_namespace", self.code_model.namespace) + file_import.add_submodule_import( + self.code_model.get_relative_import_path(serialize_namespace, module_name="_model_base"), + "_deserialize", + ImportType.LOCAL, + ) file_import.add_submodule_import("typing", "Union", ImportType.STDLIB, TypingSection.CONDITIONAL) file_import.add_submodule_import("typing", "cast", ImportType.STDLIB) return file_import diff --git a/packages/http-client-python/generator/pygen/codegen/models/lro_paging_operation.py b/packages/http-client-python/generator/pygen/codegen/models/lro_paging_operation.py index ae6cd4936b..8c1eea269a 100644 --- a/packages/http-client-python/generator/pygen/codegen/models/lro_paging_operation.py +++ b/packages/http-client-python/generator/pygen/codegen/models/lro_paging_operation.py @@ -20,8 +20,8 @@ def success_status_codes(self) -> List[Union[int, str, List[int]]]: def operation_type(self) -> str: return "lropaging" - def cls_type_annotation(self, *, async_mode: bool) -> str: - return f"ClsType[{Response.type_annotation(self.responses[0], async_mode=async_mode)}]" + def cls_type_annotation(self, *, async_mode: bool, **kwargs: Any) -> str: + return f"ClsType[{Response.type_annotation(self.responses[0], async_mode=async_mode, **kwargs)}]" def imports(self, async_mode: bool, **kwargs: Any) -> FileImport: lro_imports = LROOperationBase.imports(self, async_mode, **kwargs) diff --git a/packages/http-client-python/generator/pygen/codegen/models/model_type.py b/packages/http-client-python/generator/pygen/codegen/models/model_type.py index 09422a44e3..9784bc7a5a 100644 --- a/packages/http-client-python/generator/pygen/codegen/models/model_type.py +++ b/packages/http-client-python/generator/pygen/codegen/models/model_type.py @@ -7,7 +7,7 @@ from collections import OrderedDict from typing import Any, Dict, List, Optional, TYPE_CHECKING, cast import sys -from .utils import add_to_pylint_disable +from .utils import add_to_pylint_disable, NamespaceType from .base import BaseType from .constant_type import ConstantType from .property import Property @@ -82,6 +82,7 @@ def __init__( self.snake_case_name: str = self.yaml_data["snakeCaseName"] self.cross_language_definition_id: Optional[str] = self.yaml_data.get("crossLanguageDefinitionId") self.usage: int = self.yaml_data.get("usage", UsageFlags.Input.value | UsageFlags.Output.value) + self.client_namespace: str = self.yaml_data.get("clientNamespace", code_model.namespace) @property def is_usage_output(self) -> bool: @@ -254,8 +255,7 @@ class JSONModelType(ModelType): def type_annotation(self, **kwargs: Any) -> str: return "ET.Element" if self.is_xml else "JSON" - @property - def serialization_type(self) -> str: + def serialization_type(self, **kwargs: Any) -> str: return "object" def docstring_type(self, **kwargs: Any) -> str: @@ -281,13 +281,18 @@ class GeneratedModelType(ModelType): def type_annotation(self, **kwargs: Any) -> str: is_operation_file = kwargs.pop("is_operation_file", False) skip_quote = kwargs.get("skip_quote", False) - module_name = "_models." if kwargs.get("need_module_name", True) else "" + module_name = "" + if kwargs.get("need_model_alias", True): + serialize_namespace = kwargs.get("serialize_namespace", self.code_model.namespace) + model_alias = self.code_model.get_unique_models_alias(serialize_namespace, self.client_namespace) + module_name = f"{model_alias}." file_name = f"{self.code_model.models_filename}." if self.internal else "" retval = module_name + file_name + self.name return retval if is_operation_file or skip_quote else f'"{retval}"' def docstring_type(self, **kwargs: Any) -> str: - return f"~{self.code_model.namespace}.models.{self.type_annotation(need_module_name=False, skip_quote=True)}" + type_annotation = self.type_annotation(need_model_alias=False, skip_quote=True, **kwargs) + return f"~{self.code_model.namespace}.models.{type_annotation}" def docstring_text(self, **kwargs: Any) -> str: return self.name @@ -298,32 +303,43 @@ def type_description(self) -> str: def imports(self, **kwargs: Any) -> FileImport: file_import = super().imports(**kwargs) - relative_path = kwargs.pop("relative_path", None) - if relative_path: - # add import for models in operations or _types file + serialize_namespace = kwargs.get("serialize_namespace", self.code_model.namespace) + relative_path = self.code_model.get_relative_import_path(serialize_namespace, self.client_namespace) + alias = self.code_model.get_unique_models_alias(serialize_namespace, self.client_namespace) + serialize_namespace_type = kwargs.get("serialize_namespace_type") + called_by_property = kwargs.get("called_by_property", False) + # add import for models in operations or _types file + if serialize_namespace_type in [NamespaceType.OPERATION, NamespaceType.CLIENT]: file_import.add_submodule_import( relative_path, "models", ImportType.LOCAL, - alias="_models", - typing_section=(TypingSection.TYPING if kwargs.get("model_typing") else TypingSection.REGULAR), + alias=alias, ) if self.is_form_data: file_import.add_submodule_import( - relative_path, + self.code_model.get_relative_import_path(serialize_namespace), "_model_base", ImportType.LOCAL, - typing_section=(TypingSection.TYPING if kwargs.get("model_typing") else TypingSection.REGULAR), ) + elif serialize_namespace_type == NamespaceType.TYPES_FILE or ( + serialize_namespace_type == NamespaceType.MODEL and called_by_property + ): + file_import.add_submodule_import( + relative_path, + "models", + ImportType.LOCAL, + alias=alias, + typing_section=TypingSection.TYPING, + ) return file_import class MsrestModelType(GeneratedModelType): base = "msrest" - @property - def serialization_type(self) -> str: - return self.type_annotation(skip_quote=True) if self.internal else self.name + def serialization_type(self, **kwargs: Any) -> str: + return self.type_annotation(skip_quote=True, **kwargs) if self.internal else self.name @property def instance_check_template(self) -> str: @@ -338,12 +354,11 @@ def imports(self, **kwargs: Any) -> FileImport: class DPGModelType(GeneratedModelType): base = "dpg" - @property - def serialization_type(self) -> str: + def serialization_type(self, **kwargs: Any) -> str: return ( - self.type_annotation(skip_quote=True) + self.type_annotation(skip_quote=True, **kwargs) if self.internal - else self.type_annotation(need_module_name=False, skip_quote=True) + else self.type_annotation(need_model_alias=False, skip_quote=True, **kwargs) ) @property diff --git a/packages/http-client-python/generator/pygen/codegen/models/operation.py b/packages/http-client-python/generator/pygen/codegen/models/operation.py index b97ec390a3..880d1daa8b 100644 --- a/packages/http-client-python/generator/pygen/codegen/models/operation.py +++ b/packages/http-client-python/generator/pygen/codegen/models/operation.py @@ -36,6 +36,7 @@ from .parameter_list import ParameterList from .model_type import ModelType from .base import BaseType +from .combined_type import CombinedType from .request_builder import OverloadedRequestBuilder, RequestBuilder from ...utils import xml_serializable, json_serializable, NAME_LENGTH_LIMIT @@ -145,10 +146,10 @@ def pylint_disable(self, async_mode: bool) -> str: retval = add_to_pylint_disable(retval, "name-too-long") return retval - def cls_type_annotation(self, *, async_mode: bool) -> str: + def cls_type_annotation(self, *, async_mode: bool, **kwargs: Any) -> str: if self.request_builder.method.lower() == "head" and self.code_model.options["head_as_boolean"]: return "ClsType[None]" - return f"ClsType[{self.response_type_annotation(async_mode=async_mode)}]" + return f"ClsType[{self.response_type_annotation(async_mode=async_mode, **kwargs)}]" def _response_docstring_helper(self, attr_name: str, **kwargs: Any) -> str: responses_with_body = [r for r in self.responses if r.type] @@ -208,21 +209,26 @@ def non_default_errors(self) -> List[Response]: e for e in self.exceptions if "default" not in e.status_codes and e.type and isinstance(e.type, ModelType) ] - def _imports_shared(self, async_mode: bool, **kwargs: Any) -> FileImport: # pylint: disable=unused-argument + def _imports_shared(self, async_mode: bool, **kwargs: Any) -> FileImport: file_import = FileImport(self.code_model) file_import.add_submodule_import("typing", "Any", ImportType.STDLIB, TypingSection.CONDITIONAL) - response_types = [r.type_annotation(async_mode=async_mode, operation=self) for r in self.responses if r.type] + response_types = [r.type_annotation(async_mode=async_mode, **kwargs) for r in self.responses if r.type] if len(set(response_types)) > 1: file_import.add_submodule_import("typing", "Union", ImportType.STDLIB, TypingSection.CONDITIONAL) if self.added_on: + serialize_namespace = kwargs.get("serialize_namespace", self.code_model.namespace) file_import.add_submodule_import( - f"{'.' if async_mode else ''}.._validation", + self.code_model.get_relative_import_path(serialize_namespace, module_name="_validation"), "api_version_validation", ImportType.LOCAL, ) return file_import + @property + def need_import_iobase(self) -> bool: + return self.parameters.has_body and isinstance(self.parameters.body_parameter.type, CombinedType) + def imports_for_multiapi(self, async_mode: bool, **kwargs: Any) -> FileImport: if self.abstract: return FileImport(self.code_model) @@ -231,15 +237,23 @@ def imports_for_multiapi(self, async_mode: bool, **kwargs: Any) -> FileImport: file_import.merge( param.imports_for_multiapi( async_mode, - operation=self, + need_import_iobase=self.need_import_iobase, **kwargs, ) ) for response in self.responses: - file_import.merge(response.imports_for_multiapi(async_mode=async_mode, operation=self, **kwargs)) + file_import.merge( + response.imports_for_multiapi( + async_mode=async_mode, need_import_iobase=self.need_import_iobase, **kwargs + ) + ) if self.code_model.options["models_mode"]: for exception in self.exceptions: - file_import.merge(exception.imports_for_multiapi(async_mode=async_mode, operation=self, **kwargs)) + file_import.merge( + exception.imports_for_multiapi( + async_mode=async_mode, need_import_iobase=self.need_import_iobase, **kwargs + ) + ) return file_import @staticmethod @@ -266,6 +280,7 @@ def get_request_builder_import( self, request_builder: Union[RequestBuilder, OverloadedRequestBuilder], async_mode: bool, + serialize_namespace: str, ) -> FileImport: """Helper method to get a request builder import.""" file_import = FileImport(self.code_model) @@ -288,7 +303,11 @@ def get_request_builder_import( ) if self.code_model.options["builders_visibility"] == "embedded" and async_mode: file_import.add_submodule_import( - f"...{self.code_model.operations_folder_name}.{self.filename}", + self.code_model.get_relative_import_path( + serialize_namespace, + self.code_model.get_imported_namespace_for_operation(self.client_namespace), + module_name=self.filename, + ), request_builder.name, import_type=ImportType.LOCAL, ) @@ -299,24 +318,30 @@ def imports( # pylint: disable=too-many-branches, disable=too-many-statements ) -> FileImport: if self.abstract: return FileImport(self.code_model) + + serialize_namespace = kwargs.get("serialize_namespace", self.code_model.namespace) file_import = self._imports_shared(async_mode, **kwargs) for param in self.parameters.method: file_import.merge( param.imports( async_mode, - operation=self, + need_import_iobase=self.need_import_iobase, **kwargs, ) ) for response in self.responses: - file_import.merge(response.imports(async_mode=async_mode, operation=self, **kwargs)) + file_import.merge( + response.imports(async_mode=async_mode, need_import_iobase=self.need_import_iobase, **kwargs) + ) if self.code_model.options["models_mode"]: for exception in self.exceptions: file_import.merge(exception.imports(async_mode=async_mode, **kwargs)) if self.parameters.has_body and self.parameters.body_parameter.flattened: - file_import.merge(self.parameters.body_parameter.type.imports(operation=self, **kwargs)) + file_import.merge( + self.parameters.body_parameter.type.imports(need_import_iobase=self.need_import_iobase, **kwargs) + ) if not async_mode: for param in self.parameters.headers: if param.wire_name.lower() == "repeatability-request-id": @@ -354,7 +379,6 @@ def imports( # pylint: disable=too-many-branches, disable=too-many-statements if self.deprecated: file_import.add_import("warnings", ImportType.STDLIB) - relative_path = "..." if async_mode else ".." if self.has_etag: file_import.add_submodule_import( "exceptions", @@ -362,8 +386,17 @@ def imports( # pylint: disable=too-many-branches, disable=too-many-statements ImportType.SDKCORE, ) if not async_mode: - file_import.add_submodule_import(f"{relative_path}_vendor", "prep_if_match", ImportType.LOCAL) - file_import.add_submodule_import(f"{relative_path}_vendor", "prep_if_none_match", ImportType.LOCAL) + relative_path = self.code_model.get_relative_import_path(serialize_namespace, module_name="_vendor") + file_import.add_submodule_import( + relative_path, + "prep_if_match", + ImportType.LOCAL, + ) + file_import.add_submodule_import( + relative_path, + "prep_if_none_match", + ImportType.LOCAL, + ) if async_mode: file_import.add_submodule_import( "rest", @@ -377,7 +410,7 @@ def imports( # pylint: disable=too-many-branches, disable=too-many-statements ImportType.SDKCORE, ) if self.code_model.options["builders_visibility"] == "embedded" and not async_mode: - file_import.merge(self.request_builder.imports()) + file_import.merge(self.request_builder.imports(**kwargs)) file_import.add_submodule_import( f"{'' if self.code_model.is_azure_flavor else 'runtime.'}pipeline", "PipelineResponse", @@ -394,34 +427,35 @@ def imports( # pylint: disable=too-many-branches, disable=too-many-statements "distributed_trace", ImportType.SDKCORE, ) - file_import.merge(self.get_request_builder_import(self.request_builder, async_mode)) + file_import.merge(self.get_request_builder_import(self.request_builder, async_mode, serialize_namespace)) if self.overloads: file_import.add_submodule_import("typing", "overload", ImportType.STDLIB) if self.code_model.options["models_mode"] == "dpg": + relative_path = self.code_model.get_relative_import_path(serialize_namespace, module_name="_model_base") if self.parameters.has_body: if self.has_form_data_body: - file_import.add_submodule_import(relative_path, "_model_base", ImportType.LOCAL) + file_import.add_submodule_import( + self.code_model.get_relative_import_path(serialize_namespace), "_model_base", ImportType.LOCAL + ) elif xml_serializable(self.parameters.body_parameter.default_content_type): file_import.add_submodule_import( - f"{relative_path}_model_base", + relative_path, "_get_element", ImportType.LOCAL, ) elif json_serializable(self.parameters.body_parameter.default_content_type): file_import.add_submodule_import( - f"{relative_path}_model_base", + relative_path, "SdkJSONEncoder", ImportType.LOCAL, ) file_import.add_import("json", ImportType.STDLIB) if any(xml_serializable(str(r.default_content_type)) for r in self.responses): - file_import.add_submodule_import(f"{relative_path}_model_base", "_deserialize_xml", ImportType.LOCAL) + file_import.add_submodule_import(relative_path, "_deserialize_xml", ImportType.LOCAL) elif any(r.type for r in self.responses): - file_import.add_submodule_import(f"{relative_path}_model_base", "_deserialize", ImportType.LOCAL) + file_import.add_submodule_import(relative_path, "_deserialize", ImportType.LOCAL) if self.default_error_deserialization or self.non_default_errors: - file_import.add_submodule_import( - f"{relative_path}_model_base", "_failsafe_deserialize", ImportType.LOCAL - ) + file_import.add_submodule_import(relative_path, "_failsafe_deserialize", ImportType.LOCAL) return file_import def get_response_from_status(self, status_code: Optional[Union[str, int]]) -> ResponseType: diff --git a/packages/http-client-python/generator/pygen/codegen/models/operation_group.py b/packages/http-client-python/generator/pygen/codegen/models/operation_group.py index a6339a86bf..978a6c3736 100644 --- a/packages/http-client-python/generator/pygen/codegen/models/operation_group.py +++ b/packages/http-client-python/generator/pygen/codegen/models/operation_group.py @@ -10,7 +10,7 @@ from .base import BaseModel from .operation import get_operation from .imports import FileImport, ImportType, TypingSection -from .utils import add_to_pylint_disable +from .utils import add_to_pylint_disable, NamespaceType from .lro_operation import LROOperation from .lro_paging_operation import LROPagingOperation from ...utils import NAME_LENGTH_LIMIT @@ -46,6 +46,10 @@ def __init__( for op_group in self.yaml_data.get("operationGroups", []) ] self.link_lro_initial_operations() + self.client_namespace: str = self.yaml_data.get("clientNamespace", code_model.namespace) + self.has_parent_operation_group: bool = False + for og in self.operation_groups: + og.has_parent_operation_group = True @property def has_abstract_operations(self) -> bool: @@ -66,11 +70,11 @@ def base_class(self) -> str: base_classes.append(f"{self.client.name}MixinABC") return ", ".join(base_classes) - def imports_for_multiapi(self, async_mode: bool) -> FileImport: + def imports_for_multiapi(self, async_mode: bool, **kwargs) -> FileImport: file_import = FileImport(self.code_model) relative_path = ".." if async_mode else "." for operation in self.operations: - file_import.merge(operation.imports_for_multiapi(async_mode, relative_path=relative_path)) + file_import.merge(operation.imports_for_multiapi(async_mode, **kwargs)) if (self.code_model.model_types or self.code_model.enums) and self.code_model.options[ "models_mode" ] == "msrest": @@ -94,30 +98,71 @@ def need_validation(self) -> bool: """Whether any of its operations need validation""" return any(o for o in self.operations if o.need_validation) - def imports(self, async_mode: bool) -> FileImport: + def imports(self, async_mode: bool, **kwargs: Any) -> FileImport: file_import = FileImport(self.code_model) - relative_path = ("..." if async_mode else "..") + ("." if self.client.is_subclient else "") + serialize_namespace = kwargs.get("serialize_namespace", self.code_model.namespace) for operation in self.operations: - file_import.merge(operation.imports(async_mode, relative_path=relative_path)) + file_import.merge(operation.imports(async_mode, **kwargs)) if not self.code_model.options["combine_operation_files"]: for og in self.operation_groups: file_import.add_submodule_import( - ".", + self.code_model.get_relative_import_path( + serialize_namespace, + self.code_model.get_imported_namespace_for_operation(self.client_namespace, async_mode), + ), og.class_name, ImportType.LOCAL, ) + else: + for og in self.operation_groups: + namespace = self.code_model.get_serialize_namespace( + og.client_namespace, async_mode, NamespaceType.OPERATION + ) + if namespace != serialize_namespace: + file_import.add_submodule_import( + self.code_model.get_relative_import_path( + serialize_namespace, + self.code_model.get_imported_namespace_for_operation(og.client_namespace, async_mode), + ) + + f".{og.filename}", + og.class_name, + ImportType.LOCAL, + ) # for multiapi if ( (self.code_model.public_model_types) and self.code_model.options["models_mode"] == "msrest" and not self.is_mixin ): - file_import.add_submodule_import(relative_path, "models", ImportType.LOCAL, alias="_models") + file_import.add_submodule_import( + self.code_model.get_relative_import_path(serialize_namespace), + "models", + ImportType.LOCAL, + alias="_models", + ) if self.is_mixin: - file_import.add_submodule_import(".._vendor", f"{self.client.name}MixinABC", ImportType.LOCAL) + file_import.add_submodule_import( + # XxxMixinABC is always defined in _vendor of client namespace + self.code_model.get_relative_import_path( + serialize_namespace, + self.code_model.get_imported_namespace_for_client(self.client.client_namespace, async_mode), + module_name="_vendor", + ), + f"{self.client.name}MixinABC", + ImportType.LOCAL, + ) if self.has_abstract_operations: - file_import.add_submodule_import(".._vendor", "raise_if_not_implemented", ImportType.LOCAL) + file_import.add_submodule_import( + # raise_if_not_implemented is always defined in _vendor of top namespace + self.code_model.get_relative_import_path( + serialize_namespace, + self.code_model.get_imported_namespace_for_client(self.code_model.namespace, async_mode), + module_name="_vendor", + ), + "raise_if_not_implemented", + ImportType.LOCAL, + ) if all(o.abstract for o in self.operations): return file_import file_import.add_submodule_import("typing", "TypeVar", ImportType.STDLIB, TypingSection.CONDITIONAL) @@ -128,7 +173,7 @@ def imports(self, async_mode: bool) -> FileImport: @property def filename(self) -> str: - return self.operations[0].filename + return self.operations[0].filename if self.operations else "_operations" @property def is_mixin(self) -> bool: diff --git a/packages/http-client-python/generator/pygen/codegen/models/paging_operation.py b/packages/http-client-python/generator/pygen/codegen/models/paging_operation.py index dd5ee82640..c81eb870d0 100644 --- a/packages/http-client-python/generator/pygen/codegen/models/paging_operation.py +++ b/packages/http-client-python/generator/pygen/codegen/models/paging_operation.py @@ -103,8 +103,8 @@ def item_type(self) -> ModelType: def operation_type(self) -> str: return "paging" - def cls_type_annotation(self, *, async_mode: bool) -> str: - return f"ClsType[{Response.type_annotation(self.responses[0], async_mode=async_mode)}]" + def cls_type_annotation(self, *, async_mode: bool, **kwargs: Any) -> str: + return f"ClsType[{Response.type_annotation(self.responses[0], async_mode=async_mode, **kwargs)}]" def _imports_shared(self, async_mode: bool, **kwargs: Any) -> FileImport: file_import = super()._imports_shared(async_mode, **kwargs) @@ -117,7 +117,7 @@ def _imports_shared(self, async_mode: bool, **kwargs: Any) -> FileImport: and self.code_model.options["builders_visibility"] == "embedded" and not async_mode ): - file_import.merge(self.next_request_builder.imports()) + file_import.merge(self.next_request_builder.imports(**kwargs)) return file_import @property @@ -129,6 +129,7 @@ def imports(self, async_mode: bool, **kwargs: Any) -> FileImport: return FileImport(self.code_model) file_import = self._imports_shared(async_mode, **kwargs) file_import.merge(super().imports(async_mode, **kwargs)) + serialize_namespace = kwargs.get("serialize_namespace", self.code_model.namespace) if self.code_model.options["tracing"] and self.want_tracing: file_import.add_submodule_import( "azure.core.tracing.decorator", @@ -136,7 +137,9 @@ def imports(self, async_mode: bool, **kwargs: Any) -> FileImport: ImportType.SDKCORE, ) if self.next_request_builder: - file_import.merge(self.get_request_builder_import(self.next_request_builder, async_mode)) + file_import.merge( + self.get_request_builder_import(self.next_request_builder, async_mode, serialize_namespace) + ) elif any(p.is_api_version for p in self.client.parameters): file_import.add_import("urllib.parse", ImportType.STDLIB) file_import.add_submodule_import( @@ -145,10 +148,10 @@ def imports(self, async_mode: bool, **kwargs: Any) -> FileImport: ImportType.SDKCORE, ) if self.code_model.options["models_mode"] == "dpg": - relative_path = "..." if async_mode else ".." + relative_path = self.code_model.get_relative_import_path(serialize_namespace, module_name="_model_base") file_import.merge(self.item_type.imports(**kwargs)) if self.default_error_deserialization or any(r.type for r in self.responses): - file_import.add_submodule_import(f"{relative_path}_model_base", "_deserialize", ImportType.LOCAL) + file_import.add_submodule_import(relative_path, "_deserialize", ImportType.LOCAL) return file_import diff --git a/packages/http-client-python/generator/pygen/codegen/models/parameter.py b/packages/http-client-python/generator/pygen/codegen/models/parameter.py index 7703c9f2e7..6d4d5e5aa3 100644 --- a/packages/http-client-python/generator/pygen/codegen/models/parameter.py +++ b/packages/http-client-python/generator/pygen/codegen/models/parameter.py @@ -155,23 +155,23 @@ def docstring_text(self, **kwargs: Any) -> str: def docstring_type(self, **kwargs: Any) -> str: return self.type.docstring_type(**kwargs) - @property - def serialization_type(self) -> str: - return self.type.serialization_type + def serialization_type(self, **kwargs: Any) -> str: + return self.type.serialization_type(**kwargs) - def _imports_shared(self, async_mode: bool, **_: Any) -> FileImport: + def _imports_shared(self, async_mode: bool, **kwargs: Any) -> FileImport: # pylint: disable=unused-argument file_import = FileImport(self.code_model) if self.optional and self.client_default_value is None: file_import.add_submodule_import("typing", "Optional", ImportType.STDLIB) + serialize_namespace = kwargs.get("serialize_namespace", self.code_model.namespace) if self.added_on and self.implementation != "Client": file_import.add_submodule_import( - f"{'.' if async_mode else ''}.._validation", + self.code_model.get_relative_import_path(serialize_namespace, module_name="_validation"), "api_version_validation", ImportType.LOCAL, ) if isinstance(self.type, CombinedType) and self.type.name: file_import.add_submodule_import( - "..." if async_mode else "..", + self.code_model.get_relative_import_path(serialize_namespace), "_types", ImportType.LOCAL, TypingSection.TYPING, @@ -212,8 +212,8 @@ def docstring_type_keyword(self) -> str: @abc.abstractmethod def in_method_signature(self) -> bool: ... - def method_signature(self, async_mode: bool) -> str: - type_annotation = self.type_annotation(async_mode=async_mode) + def method_signature(self, async_mode: bool, **kwargs: Any) -> str: + type_annotation = self.type_annotation(async_mode=async_mode, **kwargs) if self.client_default_value is not None or self.optional: return f"{self.client_name}: {type_annotation} = {self.client_default_value_declaration}," if self.default_to_unset_sentinel: @@ -272,9 +272,9 @@ def has_json_model_type(self) -> bool: def imports(self, async_mode: bool, **kwargs: Any) -> FileImport: file_import = super().imports(async_mode, **kwargs) if self.is_form_data: - relative_path = "..." if async_mode else ".." + serialize_namespace = kwargs.get("serialize_namespace", self.code_model.namespace) file_import.add_submodule_import( - f"{relative_path}_vendor", + self.code_model.get_relative_import_path(serialize_namespace, module_name="_vendor"), "prepare_multipart_form_data", ImportType.LOCAL, ) diff --git a/packages/http-client-python/generator/pygen/codegen/models/parameter_list.py b/packages/http-client-python/generator/pygen/codegen/models/parameter_list.py index fe0210c301..87666eada9 100644 --- a/packages/http-client-python/generator/pygen/codegen/models/parameter_list.py +++ b/packages/http-client-python/generator/pygen/codegen/models/parameter_list.py @@ -200,22 +200,22 @@ def method(self) -> List[Union[ParameterType, BodyParameterType]]: """Sorted method params. First positional, then keyword only, then kwarg""" return self.positional + self.keyword_only + self.kwarg - def method_signature(self, async_mode: bool) -> List[str]: + def method_signature(self, async_mode: bool, **kwargs: Any) -> List[str]: """Method signature for this parameter list.""" return method_signature_helper( - positional=self.method_signature_positional(async_mode), - keyword_only=self.method_signature_keyword_only(async_mode), + positional=self.method_signature_positional(async_mode, **kwargs), + keyword_only=self.method_signature_keyword_only(async_mode, **kwargs), kwarg_params=self.method_signature_kwargs, ) - def method_signature_positional(self, async_mode: bool) -> List[str]: + def method_signature_positional(self, async_mode: bool, **kwargs: Any) -> List[str]: """Signature for positional parameters""" - return [parameter.method_signature(async_mode) for parameter in self.positional] + return [parameter.method_signature(async_mode, **kwargs) for parameter in self.positional] - def method_signature_keyword_only(self, async_mode: bool) -> List[str]: + def method_signature_keyword_only(self, async_mode: bool, **kwargs: Any) -> List[str]: """Signature for keyword only parameters""" result = [ - parameter.method_signature(async_mode) + parameter.method_signature(async_mode, **kwargs) for parameter in self.keyword_only if not parameter.hide_in_operation_signature ] diff --git a/packages/http-client-python/generator/pygen/codegen/models/primitive_types.py b/packages/http-client-python/generator/pygen/codegen/models/primitive_types.py index 395f3f235c..08eb8de03d 100644 --- a/packages/http-client-python/generator/pygen/codegen/models/primitive_types.py +++ b/packages/http-client-python/generator/pygen/codegen/models/primitive_types.py @@ -49,8 +49,7 @@ def default_template_representation_declaration(self) -> str: class BooleanType(PrimitiveType): - @property - def serialization_type(self) -> str: + def serialization_type(self, **kwargs: Any) -> str: return "bool" def docstring_type(self, **kwargs: Any) -> str: @@ -66,8 +65,7 @@ def __init__(self, yaml_data: Dict[str, Any], code_model: "CodeModel") -> None: super().__init__(yaml_data=yaml_data, code_model=code_model) self.type = "IO" - @property - def serialization_type(self) -> str: + def serialization_type(self, **kwargs: Any) -> str: return self.type def docstring_type(self, **kwargs: Any) -> str: @@ -84,17 +82,9 @@ def default_template_representation_declaration(self) -> str: return self.get_declaration(b"bytes") def imports(self, **kwargs: Any) -> FileImport: - from .combined_type import CombinedType - from .operation import OperationBase - file_import = FileImport(self.code_model) file_import.add_submodule_import("typing", "IO", ImportType.STDLIB) - operation = kwargs.get("operation") - if ( - isinstance(operation, OperationBase) - and operation.parameters.has_body - and isinstance(operation.parameters.body_parameter.type, CombinedType) - ): + if kwargs.get("need_import_iobase", False): file_import.add_submodule_import("io", "IOBase", ImportType.STDLIB) return file_import @@ -107,8 +97,7 @@ class BinaryIteratorType(PrimitiveType): def _iterator_name(self, **kwargs: Any) -> str: return "AsyncIterator" if kwargs.pop("async_mode") else "Iterator" - @property - def serialization_type(self) -> str: + def serialization_type(self, **kwargs: Any) -> str: return "IO" def docstring_type(self, **kwargs: Any) -> str: @@ -135,8 +124,7 @@ def instance_check_template(self) -> str: class AnyType(PrimitiveType): - @property - def serialization_type(self) -> str: + def serialization_type(self, **kwargs: Any) -> str: return "object" def docstring_type(self, **kwargs: Any) -> str: @@ -160,8 +148,7 @@ def instance_check_template(self) -> str: class AnyObjectType(PrimitiveType): - @property - def serialization_type(self) -> str: + def serialization_type(self, **kwargs: Any) -> str: return "object" def docstring_type(self, **kwargs: Any) -> str: @@ -239,8 +226,7 @@ def __init__(self, yaml_data: Dict[str, Any], code_model: "CodeModel") -> None: if yaml_data.get("encode") == "string": self.encode = "str" - @property - def serialization_type(self) -> str: + def serialization_type(self, **kwargs: Any) -> str: return "int" def docstring_type(self, **kwargs: Any) -> str: @@ -259,8 +245,7 @@ def instance_check_template(self) -> str: class FloatType(NumberType): - @property - def serialization_type(self) -> str: + def serialization_type(self, **kwargs: Any) -> str: return "float" def docstring_type(self, **kwargs: Any) -> str: @@ -279,8 +264,7 @@ def instance_check_template(self) -> str: class DecimalType(NumberType): - @property - def serialization_type(self) -> str: + def serialization_type(self, **kwargs: Any) -> str: return "decimal" def docstring_type(self, **kwargs: Any) -> str: @@ -342,8 +326,7 @@ def validation(self) -> Optional[Dict[str, Union[bool, int, str]]]: def get_declaration(self, value) -> str: return f"'{value}'" if value == '"' else f'"{value}"' - @property - def serialization_type(self) -> str: + def serialization_type(self, **kwargs: Any) -> str: return "str" def docstring_type(self, **kwargs: Any) -> str: @@ -363,8 +346,7 @@ def __init__(self, yaml_data: Dict[str, Any], code_model: "CodeModel") -> None: else "rfc7231" ) - @property - def serialization_type(self) -> str: + def serialization_type(self, **kwargs: Any) -> str: formats_to_attribute_type = { "rfc3339": "iso-8601", "rfc7231": "rfc-1123", @@ -410,8 +392,7 @@ def serialize_sample_value(value: Any) -> str: class TimeType(PrimitiveType): - @property - def serialization_type(self) -> str: + def serialization_type(self, **kwargs: Any) -> str: return "time" def docstring_type(self, **kwargs: Any) -> str: @@ -457,8 +438,7 @@ class UnixTimeType(PrimitiveType): def encode(self) -> str: return "unix-timestamp" - @property - def serialization_type(self) -> str: + def serialization_type(self, **kwargs: Any) -> str: return "unix-time" def docstring_type(self, **kwargs: Any) -> str: @@ -500,8 +480,7 @@ def serialize_sample_value(value: Any) -> str: class DateType(PrimitiveType): - @property - def serialization_type(self) -> str: + def serialization_type(self, **kwargs: Any) -> str: return "date" def docstring_type(self, **kwargs: Any) -> str: @@ -543,8 +522,7 @@ def serialize_sample_value(value: Any) -> str: class DurationType(PrimitiveType): - @property - def serialization_type(self) -> str: + def serialization_type(self, **kwargs: Any) -> str: return "duration" def docstring_type(self, **kwargs: Any) -> str: @@ -590,8 +568,7 @@ def __init__(self, yaml_data: Dict[str, Any], code_model: "CodeModel") -> None: super().__init__(yaml_data=yaml_data, code_model=code_model) self.encode = yaml_data.get("encode", "base64") - @property - def serialization_type(self) -> str: + def serialization_type(self, **kwargs: Any) -> str: if self.encode == "base64url": return "base64" return "bytearray" @@ -628,8 +605,7 @@ def imports(self, **kwargs: Any) -> FileImport: def instance_check_template(self) -> str: return f"isinstance({{}}, {self.name})" - @property - def serialization_type(self) -> str: + def serialization_type(self, **kwargs: Any) -> str: return self.name @@ -646,8 +622,12 @@ def docstring_type(self, **kwargs: Any) -> str: def imports(self, **kwargs: Any) -> FileImport: file_import = super().imports(**kwargs) - relative_path = "..." if kwargs.get("async_mode") else ".." - file_import.add_submodule_import(f"{relative_path}_vendor", self.name, ImportType.LOCAL) + serialize_namespace = kwargs.get("serialize_namespace", self.code_model.namespace) + file_import.add_submodule_import( + self.code_model.get_relative_import_path(serialize_namespace, module_name="_vendor"), + self.name, + ImportType.LOCAL, + ) return file_import @property diff --git a/packages/http-client-python/generator/pygen/codegen/models/property.py b/packages/http-client-python/generator/pygen/codegen/models/property.py index 963437d94d..f7adafd351 100644 --- a/packages/http-client-python/generator/pygen/codegen/models/property.py +++ b/packages/http-client-python/generator/pygen/codegen/models/property.py @@ -76,9 +76,8 @@ def constant(self) -> bool: def is_input(self): return not (self.constant or self.readonly or self.is_discriminator) - @property - def serialization_type(self) -> str: - return self.type.serialization_type + def serialization_type(self, **kwargs: Any) -> str: + return self.type.serialization_type(**kwargs) @property def msrest_deserialization_key(self) -> str: @@ -99,10 +98,10 @@ def is_base_discriminator(self) -> bool: def xml_metadata(self) -> Optional[Dict[str, Union[str, bool]]]: return self.yaml_data.get("xmlMetadata") - def type_annotation(self, *, is_operation_file: bool = False) -> str: + def type_annotation(self, *, is_operation_file: bool = False, **kwargs: Any) -> str: if self.is_base_discriminator: return "str" - types_type_annotation = self.type.type_annotation(is_operation_file=is_operation_file) + types_type_annotation = self.type.type_annotation(is_operation_file=is_operation_file, **kwargs) if self.optional and self.client_default_value is None: return f"Optional[{types_type_annotation}]" return types_type_annotation @@ -144,12 +143,13 @@ def imports(self, **kwargs) -> FileImport: file_import = FileImport(self.code_model) if self.is_discriminator and isinstance(self.type, EnumType): return file_import - file_import.merge(self.type.imports(**kwargs, relative_path="..", model_typing=True)) + file_import.merge(self.type.imports(**kwargs)) if self.optional and self.client_default_value is None: file_import.add_submodule_import("typing", "Optional", ImportType.STDLIB) if self.code_model.options["models_mode"] == "dpg": + serialize_namespace = kwargs.get("serialize_namespace", self.code_model.namespace) file_import.add_submodule_import( - ".._model_base", + self.code_model.get_relative_import_path(serialize_namespace, module_name="_model_base"), "rest_discriminator" if self.is_discriminator else "rest_field", ImportType.LOCAL, ) diff --git a/packages/http-client-python/generator/pygen/codegen/models/request_builder.py b/packages/http-client-python/generator/pygen/codegen/models/request_builder.py index 439d3944f7..159bd516c1 100644 --- a/packages/http-client-python/generator/pygen/codegen/models/request_builder.py +++ b/packages/http-client-python/generator/pygen/codegen/models/request_builder.py @@ -85,15 +85,12 @@ def response_docstring_text(self, **kwargs) -> str: def response_docstring_type(self, **kwargs) -> str: return f"~{self.code_model.core_library}.rest.HttpRequest" - def imports(self) -> FileImport: + def imports(self, **kwargs) -> FileImport: file_import = FileImport(self.code_model) - relative_path = ".." - if not self.code_model.options["builders_visibility"] == "embedded" and self.group_name: - relative_path = "..." if self.group_name else ".." if self.abstract: return file_import for parameter in self.parameters.method: - file_import.merge(parameter.imports(async_mode=False, relative_path=relative_path, operation=self)) + file_import.merge(parameter.imports(async_mode=False, **kwargs)) file_import.add_submodule_import( "rest", @@ -109,11 +106,7 @@ def imports(self) -> FileImport: ) file_import.add_submodule_import("typing", "Any", ImportType.STDLIB, typing_section=TypingSection.CONDITIONAL) file_import.add_msrest_import( - relative_path=( - "..." - if (not self.code_model.options["builders_visibility"] == "embedded" and self.group_name) - else ".." - ), + serialize_namespace=kwargs.get("serialize_namespace", self.code_model.namespace), msrest_import_type=MsrestImportType.Serializer, typing_section=TypingSection.REGULAR, ) diff --git a/packages/http-client-python/generator/pygen/codegen/models/response.py b/packages/http-client-python/generator/pygen/codegen/models/response.py index c36a98a371..6c3fcca659 100644 --- a/packages/http-client-python/generator/pygen/codegen/models/response.py +++ b/packages/http-client-python/generator/pygen/codegen/models/response.py @@ -29,9 +29,8 @@ def __init__( self.wire_name: str = yaml_data["wireName"] self.type = type - @property - def serialization_type(self) -> str: - return self.type.serialization_type + def serialization_type(self, **kwargs: Any) -> str: + return self.type.serialization_type(**kwargs) @classmethod def from_yaml(cls, yaml_data: Dict[str, Any], code_model: "CodeModel") -> "ResponseHeader": @@ -88,10 +87,9 @@ def is_stream_response(self) -> bool: ) return retval - @property - def serialization_type(self) -> str: + def serialization_type(self, **kwargs: Any) -> str: if self.type: - return self.type.serialization_type + return self.type.serialization_type(**kwargs) return "None" def type_annotation(self, **kwargs: Any) -> str: @@ -120,9 +118,9 @@ def _imports_shared(self, **kwargs: Any) -> FileImport: if self.nullable: file_import.add_submodule_import("typing", "Optional", ImportType.STDLIB) if isinstance(self.type, CombinedType) and self.type.name: - async_mode = kwargs.get("async_mode", False) + serialize_namespace = kwargs.get("serialize_namespace", self.code_model.namespace) file_import.add_submodule_import( - "..." if async_mode else "..", + self.code_model.get_relative_import_path(serialize_namespace), "_types", ImportType.LOCAL, TypingSection.TYPING, diff --git a/packages/http-client-python/generator/pygen/codegen/models/utils.py b/packages/http-client-python/generator/pygen/codegen/models/utils.py index e8472382e2..6cf97920ee 100644 --- a/packages/http-client-python/generator/pygen/codegen/models/utils.py +++ b/packages/http-client-python/generator/pygen/codegen/models/utils.py @@ -5,6 +5,8 @@ # -------------------------------------------------------------------------- from typing import TypeVar, Dict +from enum import Enum + T = TypeVar("T") OrderedSet = Dict[T, None] @@ -19,3 +21,12 @@ def add_to_pylint_disable(curr_str: str, entry: str) -> str: if curr_str: return f"{curr_str},{entry}" return f" # pylint: disable={entry}" + + +class NamespaceType(str, Enum): + """Special signal for impports""" + + MODEL = "model" + OPERATION = "operation" + CLIENT = "client" + TYPES_FILE = "types_file" diff --git a/packages/http-client-python/generator/pygen/codegen/serializers/__init__.py b/packages/http-client-python/generator/pygen/codegen/serializers/__init__.py index c6ad4cee49..7e97fb455b 100644 --- a/packages/http-client-python/generator/pygen/codegen/serializers/__init__.py +++ b/packages/http-client-python/generator/pygen/codegen/serializers/__init__.py @@ -4,7 +4,8 @@ # license information. # -------------------------------------------------------------------------- import logging -from typing import List, Optional, Any, Union +from collections import namedtuple +from typing import List, Any, Union from pathlib import Path from jinja2 import PackageLoader, Environment, FileSystemLoader, StrictUndefined @@ -15,6 +16,8 @@ OverloadedRequestBuilder, CodeModel, Client, + ModelType, + EnumType, ) from .enum_serializer import EnumSerializer from .general_serializer import GeneralSerializer @@ -34,7 +37,6 @@ extract_sample_name, get_namespace_from_package_name, get_namespace_config, - get_all_operation_groups_recursively, ) _LOGGER = logging.getLogger(__name__) @@ -53,6 +55,7 @@ ] _REGENERATE_FILES = {"setup.py", "MANIFEST.in"} +AsyncInfo = namedtuple("AsyncInfo", ["async_mode", "async_path"]) # extract sub folders. For example, source_file_path is like: @@ -85,54 +88,11 @@ def has_aio_folder(self) -> bool: def has_operations_folder(self) -> bool: return self.code_model.options["show_operations"] and bool(self.code_model.has_operations) - def _serialize_namespace_level(self, env: Environment, namespace_path: Path, clients: List[Client]) -> None: - # if there was a patch file before, we keep it - self._keep_patch_file(namespace_path / Path("_patch.py"), env) - if self.has_aio_folder: - self._keep_patch_file(namespace_path / Path("aio") / Path("_patch.py"), env) - - if self.has_operations_folder: - self._keep_patch_file( - namespace_path / Path(self.code_model.operations_folder_name) / Path("_patch.py"), - env, - ) - if self.has_aio_folder: - self._keep_patch_file( - namespace_path / Path("aio") / Path(self.code_model.operations_folder_name) / Path("_patch.py"), - env, - ) - self._serialize_and_write_top_level_folder(env=env, namespace_path=namespace_path, clients=clients) - - if any(c for c in self.code_model.clients if c.operation_groups): - if self.code_model.options["builders_visibility"] != "embedded": - self._serialize_and_write_rest_layer(env=env, namespace_path=namespace_path) - if self.has_aio_folder: - self._serialize_and_write_aio_top_level_folder( - env=env, - namespace_path=namespace_path, - clients=clients, - ) - - if self.has_operations_folder: - self._serialize_and_write_operations_folder(clients, env=env, namespace_path=namespace_path) - if self.code_model.options["multiapi"]: - self._serialize_and_write_metadata(env=env, namespace_path=namespace_path) - if self.code_model.options["package_mode"]: - self._serialize_and_write_package_files(namespace_path=namespace_path) - - if ( - self.code_model.options["show_operations"] - and self.code_model.has_operations - and self.code_model.options["generate_sample"] - ): - self._serialize_and_write_sample(env, namespace_path) - - if ( - self.code_model.options["show_operations"] - and self.code_model.has_operations - and self.code_model.options["generate_test"] - ): - self._serialize_and_write_test(env, namespace_path) + @property + def serialize_loop(self) -> List[AsyncInfo]: + sync_loop = AsyncInfo(async_mode=False, async_path="") + async_loop = AsyncInfo(async_mode=True, async_path="aio/") + return [sync_loop, async_loop] if self.has_aio_folder else [sync_loop] def serialize(self) -> None: env = Environment( @@ -144,54 +104,71 @@ def serialize(self) -> None: lstrip_blocks=True, ) - namespace_path = ( - Path(".") if self.code_model.options["no_namespace_folders"] else Path(*self._name_space().split(".")) - ) - - p = namespace_path.parent general_serializer = GeneralSerializer(code_model=self.code_model, env=env, async_mode=False) - while p != Path("."): - # write pkgutil init file - self.write_file( - p / Path("__init__.py"), - general_serializer.serialize_pkgutil_init_file(), - ) - p = p.parent - - # serialize main module - self._serialize_namespace_level( - env, - namespace_path, - [c for c in self.code_model.clients if c.has_operations], - ) - # serialize sub modules - for ( - subnamespace, - clients, - ) in self.code_model.subnamespace_to_clients.items(): - subnamespace_path = namespace_path / Path(subnamespace) - self._serialize_namespace_level(env, subnamespace_path, [c for c in clients if c.has_operations]) - - if self.code_model.options["models_mode"] and (self.code_model.model_types or self.code_model.enums): - self._keep_patch_file(namespace_path / Path("models") / Path("_patch.py"), env) - - if self.code_model.options["models_mode"] and (self.code_model.model_types or self.code_model.enums): - self._serialize_and_write_models_folder(env=env, namespace_path=namespace_path) - if not self.code_model.options["models_mode"]: - # keep models file if users ended up just writing a models file - if self.read_file(namespace_path / Path("models.py")): + for client_namespace, client_namespace_type in self.code_model.client_namespace_types.items(): + exec_path = self.exec_path(client_namespace) + if client_namespace == "": + # Write the setup file + if self.code_model.options["basic_setup_py"]: + self.write_file(exec_path / Path("setup.py"), general_serializer.serialize_setup_file()) + + # add packaging files in root namespace (e.g. setup.py, README.md, etc.) + if self.code_model.options["package_mode"]: + self._serialize_and_write_package_files(client_namespace) + + # write apiview_mapping_python.json + if self.code_model.options.get("emit_cross_language_definition_file"): + self.write_file( + exec_path / Path("apiview_mapping_python.json"), + general_serializer.serialize_cross_language_definition_file(), + ) + + # add generated samples and generated tests + if self.code_model.options["show_operations"] and self.code_model.has_operations: + if self.code_model.options["generate_sample"]: + self._serialize_and_write_sample(env, namespace=client_namespace) + if self.code_model.options["generate_test"]: + self._serialize_and_write_test(env, namespace=client_namespace) + elif client_namespace_type.clients: + # add clients folder if there are clients in this namespace + self._serialize_client_and_config_files(client_namespace, client_namespace_type.clients, env) + else: + # add pkgutil init file if no clients in this namespace self.write_file( - namespace_path / Path("models.py"), - self.read_file(namespace_path / Path("models.py")), + exec_path / Path("__init__.py"), + general_serializer.serialize_pkgutil_init_file(), ) - if self.code_model.named_unions: - self.write_file( - namespace_path / Path("_types.py"), - TypesSerializer(code_model=self.code_model, env=env).serialize(), - ) - def _serialize_and_write_package_files(self, namespace_path: Path) -> None: - root_of_sdk = self._package_root_folder(namespace_path) + # _model_base.py/_serialization.py/_vendor.py/py.typed/_types.py/_validation.py + # is always put in top level namespace + if self.code_model.is_top_namespace(client_namespace): + self._serialize_and_write_top_level_folder(env=env, namespace=client_namespace) + + # add models folder if there are models in this namespace + if (client_namespace_type.models or client_namespace_type.enums) and self.code_model.options["models_mode"]: + self._serialize_and_write_models_folder( + env=env, + namespace=client_namespace, + models=client_namespace_type.models, + enums=client_namespace_type.enums, + ) + + if not self.code_model.options["models_mode"]: + # keep models file if users ended up just writing a models file + model_path = exec_path / Path("models.py") + if self.read_file(model_path): + self.write_file(model_path, self.read_file(model_path)) + + # add operations folder if there are operations in this namespace + if client_namespace_type.operation_groups: + self._serialize_and_write_operations_folder( + client_namespace_type.operation_groups, env=env, namespace=client_namespace + ) + if self.code_model.options["multiapi"]: + self._serialize_and_write_metadata(env=env, namespace=client_namespace) + + def _serialize_and_write_package_files(self, client_namespace: str) -> None: + root_of_sdk = self.exec_path(client_namespace) if self.code_model.options["package_mode"] in VALID_PACKAGE_MODE: env = Environment( loader=PackageLoader("pygen.codegen", "templates/packaging_templates"), @@ -230,25 +207,31 @@ def _keep_patch_file(self, path_file: Path, env: Environment): PatchSerializer(env=env, code_model=self.code_model).serialize(), ) - def _serialize_and_write_models_folder(self, env: Environment, namespace_path: Path) -> None: + def _serialize_and_write_models_folder( + self, env: Environment, namespace: str, models: List[ModelType], enums: List[EnumType] + ) -> None: # Write the models folder - models_path = namespace_path / Path("models") + models_path = self.exec_path(namespace + ".models") serializer = DpgModelSerializer if self.code_model.options["models_mode"] == "dpg" else MsrestModelSerializer - if self.code_model.model_types: + if models: self.write_file( models_path / Path(f"{self.code_model.models_filename}.py"), - serializer(code_model=self.code_model, env=env).serialize(), + serializer(code_model=self.code_model, env=env, client_namespace=namespace, models=models).serialize(), ) - if self.code_model.enums: + if enums: self.write_file( models_path / Path(f"{self.code_model.enums_filename}.py"), - EnumSerializer(code_model=self.code_model, env=env).serialize(), + EnumSerializer( + code_model=self.code_model, env=env, client_namespace=namespace, enums=enums + ).serialize(), ) self.write_file( models_path / Path("__init__.py"), - ModelInitSerializer(code_model=self.code_model, env=env).serialize(), + ModelInitSerializer(code_model=self.code_model, env=env, models=models, enums=enums).serialize(), ) + self._keep_patch_file(models_path / Path("_patch.py"), env) + def _serialize_and_write_rest_layer(self, env: Environment, namespace_path: Path) -> None: rest_path = namespace_path / Path(self.code_model.rest_layer_name) group_names = {rb.group_name for c in self.code_model.clients for rb in c.request_builders} @@ -292,89 +275,57 @@ def _serialize_and_write_single_rest_layer( ).serialize_init(), ) - def _serialize_and_write_operations_file( - self, - env: Environment, - clients: List[Client], - namespace_path: Path, - operation_group: Optional[OperationGroup] = None, - ) -> None: - filename = operation_group.filename if operation_group else "_operations" - # write first sync file - operation_group_serializer = OperationGroupsSerializer( - code_model=self.code_model, - clients=clients, - env=env, - async_mode=False, - operation_group=operation_group, - ) - self.write_file( - namespace_path / Path(self.code_model.operations_folder_name) / Path(f"{filename}.py"), - operation_group_serializer.serialize(), - ) - - if self.has_aio_folder: - # write async operation group and operation files - operation_group_async_serializer = OperationGroupsSerializer( - code_model=self.code_model, - clients=clients, - env=env, - async_mode=True, - operation_group=operation_group, - ) - self.write_file( - (namespace_path / Path("aio") / Path(self.code_model.operations_folder_name) / Path(f"{filename}.py")), - operation_group_async_serializer.serialize(), - ) - def _serialize_and_write_operations_folder( - self, clients: List[Client], env: Environment, namespace_path: Path + self, operation_groups: List[OperationGroup], env: Environment, namespace: str ) -> None: - # write sync operations init file - operations_init_serializer = OperationsInitSerializer( - code_model=self.code_model, clients=clients, env=env, async_mode=False - ) - self.write_file( - namespace_path / Path(self.code_model.operations_folder_name) / Path("__init__.py"), - operations_init_serializer.serialize(), - ) - - # write async operations init file - if self.has_aio_folder: - operations_async_init_serializer = OperationsInitSerializer( - code_model=self.code_model, clients=clients, env=env, async_mode=True + operations_folder_name = self.code_model.operations_folder_name(namespace) + exec_path = self.exec_path(namespace) + for async_mode, async_path in self.serialize_loop: + prefix_path = f"{async_path}{operations_folder_name}" + # write init file + operations_init_serializer = OperationsInitSerializer( + code_model=self.code_model, operation_groups=operation_groups, env=env, async_mode=async_mode ) self.write_file( - namespace_path / Path("aio") / Path(self.code_model.operations_folder_name) / Path("__init__.py"), - operations_async_init_serializer.serialize(), + exec_path / Path(f"{prefix_path}/__init__.py"), + operations_init_serializer.serialize(), ) - if self.code_model.options["combine_operation_files"]: - self._serialize_and_write_operations_file( - env=env, - namespace_path=namespace_path, - clients=clients, - ) - else: - for operation_group in get_all_operation_groups_recursively(self.code_model.clients): - self._serialize_and_write_operations_file( + # write operations file + OgLoop = namedtuple("OgLoop", ["operation_groups", "filename"]) + if self.code_model.options["combine_operation_files"]: + loops = [OgLoop(operation_groups, "_operations")] + else: + loops = [OgLoop([og], og.filename) for og in operation_groups] + for ogs, filename in loops: + operation_group_serializer = OperationGroupsSerializer( + code_model=self.code_model, + operation_groups=ogs, env=env, - namespace_path=namespace_path, - operation_group=operation_group, - clients=clients, + async_mode=async_mode, + client_namespace=namespace, ) + self.write_file( + exec_path / Path(f"{prefix_path}/{filename}.py"), + operation_group_serializer.serialize(), + ) + + # if there was a patch file before, we keep it + self._keep_patch_file(exec_path / Path(f"{prefix_path}/_patch.py"), env) def _serialize_and_write_version_file( self, - namespace_path: Path, + namespace: str, general_serializer: GeneralSerializer, ): + exec_path = self.exec_path(namespace) + def _read_version_file(original_version_file_name: str) -> str: - return self.read_file(namespace_path / original_version_file_name) + return self.read_file(exec_path / original_version_file_name) def _write_version_file(original_version_file_name: str) -> None: self.write_file( - namespace_path / Path("_version.py"), + exec_path / Path("_version.py"), _read_version_file(original_version_file_name), ) @@ -385,103 +336,103 @@ def _write_version_file(original_version_file_name: str) -> None: _write_version_file(original_version_file_name="version.py") elif self.code_model.options["package_version"]: self.write_file( - namespace_path / Path("_version.py"), + exec_path / Path("_version.py"), general_serializer.serialize_version_file(), ) def _serialize_client_and_config_files( self, - namespace_path: Path, - general_serializer: GeneralSerializer, - async_mode: bool, + namespace: str, clients: List[Client], + env: Environment, ) -> None: - if self.code_model.has_operations: - namespace_path = namespace_path / Path("aio") if async_mode else namespace_path - self.write_file( - namespace_path / Path(f"{self.code_model.client_filename}.py"), - general_serializer.serialize_service_client_file(clients), + exec_path = self.exec_path(namespace) + for async_mode, async_path in self.serialize_loop: + general_serializer = GeneralSerializer( + code_model=self.code_model, env=env, async_mode=async_mode, client_namespace=namespace ) + # when there is client.py, there must be __init__.py self.write_file( - namespace_path / Path("_configuration.py"), - general_serializer.serialize_config_file(clients), + exec_path / Path(f"{async_path}__init__.py"), + general_serializer.serialize_init_file([c for c in clients if c.has_operations]), ) - def _serialize_and_write_top_level_folder( - self, env: Environment, namespace_path: Path, clients: List[Client] - ) -> None: - general_serializer = GeneralSerializer(code_model=self.code_model, env=env, async_mode=False) + # if there was a patch file before, we keep it + self._keep_patch_file(exec_path / Path(f"{async_path}_patch.py"), env) - self.write_file( - namespace_path / Path("__init__.py"), - general_serializer.serialize_init_file(clients), - ) + if self.code_model.clients_has_operations(clients): - # Write the service client - self._serialize_client_and_config_files(namespace_path, general_serializer, async_mode=False, clients=clients) - if self.code_model.need_vendored_code(async_mode=False): - self.write_file( - namespace_path / Path("_vendor.py"), - general_serializer.serialize_vendor_file(clients), - ) + # write client file + self.write_file( + exec_path / Path(f"{async_path}{self.code_model.client_filename}.py"), + general_serializer.serialize_service_client_file(clients), + ) + + # write config file + self.write_file( + exec_path / Path(f"{async_path}_configuration.py"), + general_serializer.serialize_config_file(clients), + ) + + # sometimes we need define additional Mixin class for client in _vendor.py + self._serialize_and_write_vendor_file(env, namespace) - self._serialize_and_write_version_file(namespace_path, general_serializer) + def _serialize_and_write_vendor_file(self, env: Environment, namespace: str) -> None: + exec_path = self.exec_path(namespace) + # write _vendor.py + for async_mode, async_path in self.serialize_loop: + if self.code_model.need_vendored_code(async_mode=async_mode, client_namespace=namespace): + self.write_file( + exec_path / Path(f"{async_path}_vendor.py"), + GeneralSerializer( + code_model=self.code_model, env=env, async_mode=async_mode, client_namespace=namespace + ).serialize_vendor_file(), + ) + + def _serialize_and_write_top_level_folder(self, env: Environment, namespace: str) -> None: + exec_path = self.exec_path(namespace) + # write _vendor.py + self._serialize_and_write_vendor_file(env, namespace) + + general_serializer = GeneralSerializer(code_model=self.code_model, env=env, async_mode=False) + + # write _version.py + self._serialize_and_write_version_file(namespace, general_serializer) # write the empty py.typed file - self.write_file(namespace_path / Path("py.typed"), "# Marker file for PEP 561.") + self.write_file(exec_path / Path("py.typed"), "# Marker file for PEP 561.") + # write _serialization.py if not self.code_model.options["client_side_validation"] and not self.code_model.options["multiapi"]: self.write_file( - namespace_path / Path("_serialization.py"), + exec_path / Path("_serialization.py"), general_serializer.serialize_serialization_file(), ) + + # write _model_base.py if self.code_model.options["models_mode"] == "dpg": self.write_file( - namespace_path / Path("_model_base.py"), + exec_path / Path("_model_base.py"), general_serializer.serialize_model_base_file(), ) + # write _validation.py if any(og for client in self.code_model.clients for og in client.operation_groups if og.need_validation): self.write_file( - namespace_path / Path("_validation.py"), + exec_path / Path("_validation.py"), general_serializer.serialize_validation_file(), ) - if self.code_model.options.get("emit_cross_language_definition_file"): - self.write_file( - Path("./apiview_mapping_python.json"), - general_serializer.serialize_cross_language_definition_file(), - ) - - # Write the setup file - if self.code_model.options["basic_setup_py"]: - self.write_file(Path("setup.py"), general_serializer.serialize_setup_file()) - - def _serialize_and_write_aio_top_level_folder( - self, env: Environment, namespace_path: Path, clients: List[Client] - ) -> None: - aio_general_serializer = GeneralSerializer(code_model=self.code_model, env=env, async_mode=True) - - aio_path = namespace_path / Path("aio") - - # Write the __init__ file - self.write_file( - aio_path / Path("__init__.py"), - aio_general_serializer.serialize_init_file(clients), - ) - # Write the service client - self._serialize_client_and_config_files( - namespace_path, aio_general_serializer, async_mode=True, clients=clients - ) - if self.code_model.need_vendored_code(async_mode=True): + # write _types.py + if self.code_model.named_unions: self.write_file( - aio_path / Path("_vendor.py"), - aio_general_serializer.serialize_vendor_file(clients), + exec_path / Path("_types.py"), + TypesSerializer(code_model=self.code_model, env=env).serialize(), ) - def _serialize_and_write_metadata(self, env: Environment, namespace_path: Path) -> None: - metadata_serializer = MetadataSerializer(self.code_model, env) - self.write_file(namespace_path / Path("_metadata.json"), metadata_serializer.serialize()) + def _serialize_and_write_metadata(self, env: Environment, namespace: str) -> None: + metadata_serializer = MetadataSerializer(self.code_model, env, client_namespace=namespace) + self.write_file(self.exec_path(namespace) / Path("_metadata.json"), metadata_serializer.serialize()) @property def _namespace_from_package_name(self) -> str: @@ -493,9 +444,17 @@ def _name_space(self) -> str: return self._namespace_from_package_name - # find root folder where "setup.py" is - def _package_root_folder(self, namespace_path: Path) -> Path: - return namespace_path / Path("../" * (self._name_space().count(".") + 1)) + @property + def exec_path_compensation(self) -> Path: + """Assume the process is running in the root folder of the package. If not, we need the path compensation.""" + return ( + Path("../" * (self._name_space().count(".") + 1)) + if self.code_model.options["no_namespace_folders"] + else Path(".") + ) + + def exec_path(self, namespace: str) -> Path: + return self.exec_path_compensation / Path(*namespace.split(".")) @property def _additional_folder(self) -> Path: @@ -506,8 +465,8 @@ def _additional_folder(self) -> Path: return Path("/".join(namespace_config.split(".")[num_of_package_namespace:])) return Path("") - def _serialize_and_write_sample(self, env: Environment, namespace_path: Path): - out_path = self._package_root_folder(namespace_path) / Path("generated_samples") + def _serialize_and_write_sample(self, env: Environment, namespace: str): + out_path = self.exec_path(namespace) / Path("generated_samples") for client in self.code_model.clients: for op_group in client.operation_groups: for operation in op_group.operations: @@ -539,15 +498,15 @@ def _serialize_and_write_sample(self, env: Environment, namespace_path: Path): log_error = f"error happens in sample {file}: {e}" _LOGGER.error(log_error) - def _serialize_and_write_test(self, env: Environment, namespace_path: Path): + def _serialize_and_write_test(self, env: Environment, namespace: str): self.code_model.for_test = True - out_path = self._package_root_folder(namespace_path) / Path("generated_tests") + out_path = self.exec_path(namespace) / Path("generated_tests") general_serializer = TestGeneralSerializer(code_model=self.code_model, env=env) self.write_file(out_path / "conftest.py", general_serializer.serialize_conftest()) if not self.code_model.options["azure_arm"]: - for is_async in (True, False): - async_suffix = "_async" if is_async else "" - general_serializer.is_async = is_async + for async_mode in (True, False): + async_suffix = "_async" if async_mode else "" + general_serializer.async_mode = async_mode self.write_file( out_path / f"testpreparer{async_suffix}.py", general_serializer.serialize_testpreparer(), @@ -560,9 +519,9 @@ def _serialize_and_write_test(self, env: Environment, namespace_path: Path): ): continue test_serializer = TestSerializer(self.code_model, env, client=client, operation_group=og) - for is_async in (True, False): + for async_mode in (True, False): try: - test_serializer.is_async = is_async + test_serializer.async_mode = async_mode self.write_file( out_path / f"{to_snake_case(test_serializer.test_class_name)}.py", test_serializer.serialize_test(), diff --git a/packages/http-client-python/generator/pygen/codegen/serializers/base_serializer.py b/packages/http-client-python/generator/pygen/codegen/serializers/base_serializer.py index 0ac623166a..39c36e185c 100644 --- a/packages/http-client-python/generator/pygen/codegen/serializers/base_serializer.py +++ b/packages/http-client-python/generator/pygen/codegen/serializers/base_serializer.py @@ -3,6 +3,7 @@ # Licensed under the MIT License. See License.txt in the project root for # license information. # -------------------------------------------------------------------------- +from typing import Optional from jinja2 import Environment from ..models import ( FileImport, @@ -13,9 +14,26 @@ class BaseSerializer: """Base serializer for SDK root level files""" - def __init__(self, code_model: CodeModel, env: Environment): + def __init__( + self, + code_model: CodeModel, + env: Environment, + async_mode: bool = False, + *, + client_namespace: Optional[str] = None + ): self.code_model = code_model self.env = env + self.async_mode = async_mode + self.client_namespace = code_model.namespace if client_namespace is None else client_namespace def init_file_import(self) -> FileImport: return FileImport(self.code_model) + + # get namespace of serialize file from client namespace. + # For async API, serialize namespace will have additional suffix '.aio' compared with client namespace; + # For models, there will be additional '.models'; + # For operations, there will be additional '.operations' or '._operations'; + @property + def serialize_namespace(self) -> str: + return self.code_model.get_serialize_namespace(self.client_namespace, async_mode=self.async_mode) diff --git a/packages/http-client-python/generator/pygen/codegen/serializers/builder_serializer.py b/packages/http-client-python/generator/pygen/codegen/serializers/builder_serializer.py index 2293b5e6b4..98c406d48e 100644 --- a/packages/http-client-python/generator/pygen/codegen/serializers/builder_serializer.py +++ b/packages/http-client-python/generator/pygen/codegen/serializers/builder_serializer.py @@ -32,6 +32,7 @@ ParameterListType, ByteArraySchema, ) +from ..models.utils import NamespaceType from .parameter_serializer import ParameterSerializer, PopKwargType from ..models.parameter_list import ParameterType from . import utils @@ -188,10 +189,19 @@ def is_json_model_type(parameters: ParameterListType) -> bool: class _BuilderBaseSerializer(Generic[BuilderType]): - def __init__(self, code_model: CodeModel, async_mode: bool) -> None: + def __init__(self, code_model: CodeModel, async_mode: bool, client_namespace: str) -> None: self.code_model = code_model self.async_mode = async_mode - self.parameter_serializer = ParameterSerializer() + self.client_namespace = client_namespace + self.parameter_serializer = ParameterSerializer(self.serialize_namespace) + + @property + def serialize_namespace(self) -> str: + return self.code_model.get_serialize_namespace( + self.client_namespace, + async_mode=self.async_mode, + client_namespace_type=NamespaceType.OPERATION, + ) @property @abstractmethod @@ -230,14 +240,18 @@ def _method_signature(self, builder: BuilderType) -> str: function_def=self._function_def, method_name=builder.name, need_self_param=self._need_self_param, - method_param_signatures=builder.method_signature(self.async_mode), + method_param_signatures=builder.method_signature( + self.async_mode, serialize_namespace=self.serialize_namespace + ), pylint_disable=builder.pylint_disable(self.async_mode), ) def method_signature_and_response_type_annotation( self, builder: BuilderType, *, want_decorators: Optional[bool] = True ) -> str: - response_type_annotation = builder.response_type_annotation(async_mode=self.async_mode) + response_type_annotation = builder.response_type_annotation( + async_mode=self.async_mode, serialize_namespace=self.serialize_namespace + ) method_signature = self._method_signature(builder) decorators = self.decorators(builder) decorators_str = "" @@ -286,6 +300,7 @@ def param_description(self, builder: BuilderType) -> List[str]: ) docstring_type = param.docstring_type( async_mode=self.async_mode, + serialize_namespace=self.serialize_namespace, ) description_list.append(f":{param.docstring_type_keyword} {param.client_name}: {docstring_type}") return description_list @@ -361,6 +376,7 @@ def pipeline_name(self) -> str: class RequestBuilderSerializer(_BuilderBaseSerializer[RequestBuilderType]): + def description_and_summary(self, builder: RequestBuilderType) -> List[str]: retval = super().description_and_summary(builder) retval += [ @@ -616,13 +632,18 @@ def pop_kwargs_from_signature(self, builder: OperationType) -> List[str]: for p in builder.parameters.parameters: if p.hide_in_operation_signature: kwargs.append(f'{p.client_name} = kwargs.pop("{p.client_name}", None)') - cls_annotation = builder.cls_type_annotation(async_mode=self.async_mode) + cls_annotation = builder.cls_type_annotation( + async_mode=self.async_mode, serialize_namespace=self.serialize_namespace + ) kwargs.append(f"cls: {cls_annotation} = kwargs.pop(\n 'cls', None\n)") return kwargs def response_docstring(self, builder: OperationType) -> List[str]: response_str = f":return: {builder.response_docstring_text(async_mode=self.async_mode)}" - rtype_str = f":rtype: {builder.response_docstring_type(async_mode=self.async_mode)}" + response_docstring_type = builder.response_docstring_type( + async_mode=self.async_mode, serialize_namespace=self.serialize_namespace + ) + rtype_str = f":rtype: {response_docstring_type}" return [ response_str, rtype_str, @@ -670,9 +691,10 @@ def _serialize_body_parameter(self, builder: OperationType) -> List[str]: if self.code_model.options["models_mode"] == "msrest": is_xml_cmd = _xml_config(send_xml, builder.parameters.body_parameter.content_types) serialization_ctxt_cmd = f", {ser_ctxt_name}={ser_ctxt_name}" if xml_serialization_ctxt else "" + serialization_type = body_param.type.serialization_type(serialize_namespace=self.serialize_namespace) create_body_call = ( f"_{body_kwarg_name} = self._serialize.body({body_param.client_name}, " - f"'{body_param.type.serialization_type}'{is_xml_cmd}{serialization_ctxt_cmd})" + f"'{serialization_type}'{is_xml_cmd}{serialization_ctxt_cmd})" ) elif self.code_model.options["models_mode"] == "dpg": if json_serializable(body_param.default_content_type): @@ -903,7 +925,7 @@ def response_headers(self, response: Response) -> List[str]: retval: List[str] = [ ( f"response_headers['{response_header.wire_name}']=self._deserialize(" - f"'{response_header.serialization_type}', response.headers.get('{response_header.wire_name}'))" + f"'{response_header.serialization_type(serialize_namespace=self.serialize_namespace)}', response.headers.get('{response_header.wire_name}'))" # pylint: disable=line-too-long ) for response_header in response.headers ] @@ -911,7 +933,7 @@ def response_headers(self, response: Response) -> List[str]: retval.append("") return retval - def response_deserialization( + def response_deserialization( # pylint: disable=too-many-statements self, builder: OperationType, response: Response, @@ -937,7 +959,8 @@ def response_deserialization( pylint_disable = " # pylint: disable=protected-access" if self.code_model.options["models_mode"] == "msrest": deserialize_code.append("deserialized = self._deserialize(") - deserialize_code.append(f" '{response.serialization_type}',{pylint_disable}") + serialization_type = response.serialization_type(serialize_namespace=self.serialize_namespace) + deserialize_code.append(f" '{serialization_type}',{pylint_disable}") deserialize_code.append(" pipeline_response.http_response") deserialize_code.append(")") elif self.code_model.options["models_mode"] == "dpg": @@ -955,9 +978,10 @@ def response_deserialization( if xml_serializable(str(response.default_content_type)): deserialize_func = "_deserialize_xml" deserialize_code.append(f"deserialized = {deserialize_func}(") - deserialize_code.append( - f" {response.type.type_annotation(is_operation_file=True)},{pylint_disable}" + type_annotation = response.type.type_annotation( + is_operation_file=True, serialize_namespace=self.serialize_namespace ) + deserialize_code.append(f" {type_annotation},{pylint_disable}") deserialize_code.append(f" response.{response_attr}(){response.result_property}{format_filed}") deserialize_code.append(")") @@ -1002,11 +1026,14 @@ def handle_error_response(self, builder: OperationType) -> List[str]: if isinstance(e.status_codes[0], int): for status_code in e.status_codes: retval.append(f" {condition} response.status_code == {status_code}:") + type_annotation = e.type.type_annotation( # type: ignore + is_operation_file=True, skip_quote=True, serialize_namespace=self.serialize_namespace + ) if self.code_model.options["models_mode"] == "dpg": - retval.append(f" error = _failsafe_deserialize({e.type.type_annotation(is_operation_file=True, skip_quote=True)}, response.json())") # type: ignore # pylint: disable=line-too-long + retval.append(f" error = _failsafe_deserialize({type_annotation}, response.json())") else: retval.append( - f" error = self._deserialize.failsafe_deserialize({e.type.type_annotation(is_operation_file=True, skip_quote=True)}, " # type: ignore # pylint: disable=line-too-long + f" error = self._deserialize.failsafe_deserialize({type_annotation}, " "pipeline_response)" ) # add build-in error type @@ -1044,11 +1071,14 @@ def handle_error_response(self, builder: OperationType) -> List[str]: retval.append( f" {condition} {e.status_codes[0][0]} <= response.status_code <= {e.status_codes[0][1]}:" ) + type_annotation = e.type.type_annotation( # type: ignore + is_operation_file=True, skip_quote=True, serialize_namespace=self.serialize_namespace + ) if self.code_model.options["models_mode"] == "dpg": - retval.append(f" error = _failsafe_deserialize({e.type.type_annotation(is_operation_file=True, skip_quote=True)}, response.json())") # type: ignore # pylint: disable=line-too-long + retval.append(f" error = _failsafe_deserialize({type_annotation}, response.json())") else: retval.append( - f" error = self._deserialize.failsafe_deserialize({e.type.type_annotation(is_operation_file=True, skip_quote=True)}, " # type: ignore # pylint: disable=line-too-long + f" error = self._deserialize.failsafe_deserialize({type_annotation}, " "pipeline_response)" ) condition = "elif" @@ -1198,13 +1228,6 @@ class OperationSerializer(_OperationSerializer[Operation]): ... class _PagingOperationSerializer(_OperationSerializer[PagingOperationType]): - def __init__(self, code_model: CodeModel, async_mode: bool) -> None: - # for pylint reasons need to redefine init - # probably because inheritance is going too deep - super().__init__(code_model, async_mode) - self.code_model = code_model - self.async_mode = async_mode - self.parameter_serializer = ParameterSerializer() def serialize_path(self, builder: PagingOperationType) -> List[str]: return self.parameter_serializer.serialize_path(builder.parameters.path, self.serializer_name) @@ -1294,10 +1317,10 @@ def _extract_data_callback(self, builder: PagingOperationType) -> List[str]: deserialized = "pipeline_response.http_response.json()" if self.code_model.options["models_mode"] == "msrest": suffix = ".http_response" if hasattr(builder, "initial_operation") else "" - deserialize_type = response.serialization_type + deserialize_type = response.serialization_type(serialize_namespace=self.serialize_namespace) pylint_disable = " # pylint: disable=protected-access" if isinstance(response.type, ModelType) and not response.type.internal: - deserialize_type = f'"{response.serialization_type}"' + deserialize_type = f'"{response.serialization_type(serialize_namespace=self.serialize_namespace)}"' pylint_disable = "" deserialized = ( f"self._deserialize(\n {deserialize_type},{pylint_disable}\n pipeline_response{suffix}\n)" @@ -1312,7 +1335,9 @@ def _extract_data_callback(self, builder: PagingOperationType) -> List[str]: access = f".{item_name}" if self.code_model.options["models_mode"] == "msrest" else f'["{item_name}"]' list_of_elem_deserialized = "" if self.code_model.options["models_mode"] == "dpg": - item_type = builder.item_type.type_annotation(is_operation_file=True) + item_type = builder.item_type.type_annotation( + is_operation_file=True, serialize_namespace=self.serialize_namespace + ) list_of_elem_deserialized = f"_deserialize({item_type}, deserialized{access})" else: list_of_elem_deserialized = f"deserialized{access}" @@ -1363,14 +1388,6 @@ class PagingOperationSerializer(_PagingOperationSerializer[PagingOperation]): .. class _LROOperationSerializer(_OperationSerializer[LROOperationType]): - def __init__(self, code_model: CodeModel, async_mode: bool) -> None: - # for pylint reasons need to redefine init - # probably because inheritance is going too deep - super().__init__(code_model, async_mode) - self.code_model = code_model - self.async_mode = async_mode - self.parameter_serializer = ParameterSerializer() - def serialize_path(self, builder: LROOperationType) -> List[str]: return self.parameter_serializer.serialize_path(builder.parameters.path, self.serializer_name) @@ -1512,6 +1529,7 @@ def get_operation_serializer( builder: Operation, code_model, async_mode: bool, + client_namespace: str, ) -> Union[ OperationSerializer, PagingOperationSerializer, @@ -1530,4 +1548,4 @@ def get_operation_serializer( ret_cls = LROOperationSerializer elif builder.operation_type == "paging": ret_cls = PagingOperationSerializer - return ret_cls(code_model, async_mode) + return ret_cls(code_model, async_mode, client_namespace) diff --git a/packages/http-client-python/generator/pygen/codegen/serializers/client_serializer.py b/packages/http-client-python/generator/pygen/codegen/serializers/client_serializer.py index b99f471fbd..6ac6de68bb 100644 --- a/packages/http-client-python/generator/pygen/codegen/serializers/client_serializer.py +++ b/packages/http-client-python/generator/pygen/codegen/serializers/client_serializer.py @@ -12,9 +12,10 @@ class ClientSerializer: - def __init__(self, client: Client) -> None: + def __init__(self, client: Client, serialize_namespace: str) -> None: self.client = client - self.parameter_serializer = ParameterSerializer() + self.parameter_serializer = ParameterSerializer(serialize_namespace) + self.serialize_namespace = serialize_namespace def _init_signature(self, async_mode: bool) -> str: pylint_disable = "" @@ -24,7 +25,9 @@ def _init_signature(self, async_mode: bool) -> str: function_def="def", method_name="__init__", need_self_param=True, - method_param_signatures=self.client.parameters.method_signature(async_mode), + method_param_signatures=self.client.parameters.method_signature( + async_mode, serialize_namespace=self.serialize_namespace + ), pylint_disable=pylint_disable, ) @@ -244,9 +247,10 @@ def serialize_path(self) -> List[str]: class ConfigSerializer: - def __init__(self, client: Client) -> None: + def __init__(self, client: Client, serialize_namespace: str) -> None: self.client = client - self.parameter_serializer = ParameterSerializer() + self.parameter_serializer = ParameterSerializer(serialize_namespace) + self.serialize_namespace = serialize_namespace def _init_signature(self, async_mode: bool) -> str: return self.parameter_serializer.serialize_method( diff --git a/packages/http-client-python/generator/pygen/codegen/serializers/enum_serializer.py b/packages/http-client-python/generator/pygen/codegen/serializers/enum_serializer.py index 4b9ce87e3f..7b2c719136 100644 --- a/packages/http-client-python/generator/pygen/codegen/serializers/enum_serializer.py +++ b/packages/http-client-python/generator/pygen/codegen/serializers/enum_serializer.py @@ -3,13 +3,27 @@ # Licensed under the MIT License. See License.txt in the project root for # license information. # -------------------------------------------------------------------------- - +from typing import Optional, List +from jinja2 import Environment from .base_serializer import BaseSerializer -from ..models import FileImport +from ..models import FileImport, CodeModel, EnumType class EnumSerializer(BaseSerializer): + + def __init__( + self, + code_model: CodeModel, + env: Environment, + async_mode: bool = False, + *, + enums: List[EnumType], + client_namespace: Optional[str] = None + ): + super().__init__(code_model, env, async_mode=async_mode, client_namespace=client_namespace) + self.enums = enums + def serialize(self) -> str: # Generate the enum file template = self.env.get_template("enum_container.py.jinja2") - return template.render(code_model=self.code_model, file_import=FileImport(self.code_model)) + return template.render(code_model=self.code_model, file_import=FileImport(self.code_model), enums=self.enums) diff --git a/packages/http-client-python/generator/pygen/codegen/serializers/general_serializer.py b/packages/http-client-python/generator/pygen/codegen/serializers/general_serializer.py index 2a2c1c24d6..57e774d99b 100644 --- a/packages/http-client-python/generator/pygen/codegen/serializers/general_serializer.py +++ b/packages/http-client-python/generator/pygen/codegen/serializers/general_serializer.py @@ -5,15 +5,14 @@ # -------------------------------------------------------------------------- import json from typing import Any, List -from jinja2 import Environment from .import_serializer import FileImportSerializer, TypingSection from ..models.imports import MsrestImportType, FileImport from ..models import ( ImportType, - CodeModel, TokenCredentialType, Client, ) +from ..models.utils import NamespaceType from .client_serializer import ClientSerializer, ConfigSerializer from .base_serializer import BaseSerializer @@ -21,10 +20,6 @@ class GeneralSerializer(BaseSerializer): """General serializer for SDK root level files""" - def __init__(self, code_model: CodeModel, env: Environment, async_mode: bool): - super().__init__(code_model, env) - self.async_mode = async_mode - def serialize_setup_file(self) -> str: template = self.env.get_template("packaging_templates/setup.py.jinja2") params = {} @@ -65,6 +60,7 @@ def serialize_init_file(self, clients: List[Client]) -> str: code_model=self.code_model, clients=clients, async_mode=self.async_mode, + serialize_namespace=self.serialize_namespace, ) def serialize_service_client_file(self, clients: List[Client]) -> str: @@ -72,7 +68,13 @@ def serialize_service_client_file(self, clients: List[Client]) -> str: imports = FileImport(self.code_model) for client in clients: - imports.merge(client.imports(self.async_mode)) + imports.merge( + client.imports( + self.async_mode, + serialize_namespace=self.serialize_namespace, + serialize_namespace_type=NamespaceType.CLIENT, + ) + ) return template.render( code_model=self.code_model, @@ -80,14 +82,16 @@ def serialize_service_client_file(self, clients: List[Client]) -> str: async_mode=self.async_mode, get_serializer=ClientSerializer, imports=FileImportSerializer(imports), + serialize_namespace=self.serialize_namespace, ) - def serialize_vendor_file(self, clients: List[Client]) -> str: + def serialize_vendor_file(self) -> str: template = self.env.get_template("vendor.py.jinja2") + clients = self.code_model.get_clients(self.client_namespace) # configure imports file_import = FileImport(self.code_model) - if self.code_model.need_mixin_abc: + if self.code_model.need_vendored_mixin(self.client_namespace): file_import.add_submodule_import( "abc", "ABC", @@ -100,7 +104,7 @@ def serialize_vendor_file(self, clients: List[Client]) -> str: TypingSection.TYPING, ) file_import.add_msrest_import( - relative_path=".." if self.async_mode else ".", + serialize_namespace=self.serialize_namespace, msrest_import_type=MsrestImportType.SerializerDeserializer, typing_section=TypingSection.TYPING, ) @@ -111,14 +115,14 @@ def serialize_vendor_file(self, clients: List[Client]) -> str: f"{client.name}Configuration", ImportType.LOCAL, ) - if self.code_model.has_etag: + if self.code_model.need_vendored_etag(self.client_namespace): file_import.add_submodule_import("typing", "Optional", ImportType.STDLIB) file_import.add_submodule_import( "", "MatchConditions", ImportType.SDKCORE, ) - if self.code_model.has_form_data and self.code_model.options["models_mode"] == "dpg" and not self.async_mode: + if self.code_model.need_vendored_form_data(self.async_mode, self.client_namespace): file_import.add_submodule_import("typing", "IO", ImportType.STDLIB) file_import.add_submodule_import("typing", "Tuple", ImportType.STDLIB) file_import.add_submodule_import("typing", "Union", ImportType.STDLIB) @@ -146,19 +150,27 @@ def serialize_vendor_file(self, clients: List[Client]) -> str: ), async_mode=self.async_mode, clients=clients, + client_namespace=self.client_namespace, ) def serialize_config_file(self, clients: List[Client]) -> str: template = self.env.get_template("config_container.py.jinja2") imports = FileImport(self.code_model) for client in self.code_model.clients: - imports.merge(client.config.imports(self.async_mode)) + imports.merge( + client.config.imports( + self.async_mode, + serialize_namespace=self.serialize_namespace, + serialize_namespace_type=NamespaceType.CLIENT, + ) + ) return template.render( code_model=self.code_model, async_mode=self.async_mode, imports=FileImportSerializer(imports), get_serializer=ConfigSerializer, clients=clients, + serialize_namespace=self.serialize_namespace, ) def serialize_version_file(self) -> str: @@ -181,7 +193,7 @@ def serialize_validation_file(self) -> str: def serialize_cross_language_definition_file(self) -> str: cross_langauge_def_dict = { - f"{self.code_model.namespace}.models.{model.name}": model.cross_language_definition_id + f"{model.client_namespace}.models.{model.name}": model.cross_language_definition_id for model in self.code_model.public_model_types } cross_langauge_def_dict.update( diff --git a/packages/http-client-python/generator/pygen/codegen/serializers/metadata_serializer.py b/packages/http-client-python/generator/pygen/codegen/serializers/metadata_serializer.py index 35d374983f..d02fbeae60 100644 --- a/packages/http-client-python/generator/pygen/codegen/serializers/metadata_serializer.py +++ b/packages/http-client-python/generator/pygen/codegen/serializers/metadata_serializer.py @@ -16,6 +16,7 @@ CodeModel, ) from .builder_serializer import get_operation_serializer +from .base_serializer import BaseSerializer from .import_serializer import FileImportSerializer @@ -104,11 +105,10 @@ def _mixin_typing_definitions( return sync_mixin_typing_definitions, async_mixin_typing_definitions -class MetadataSerializer: - def __init__(self, code_model: CodeModel, env: Environment) -> None: - self.code_model = code_model +class MetadataSerializer(BaseSerializer): + def __init__(self, code_model: CodeModel, env: Environment, *, client_namespace: Optional[str] = None): + super().__init__(code_model, env, client_namespace=client_namespace) self.client = self.code_model.clients[0] # we only do one client for multiapi - self.env = env def _choose_api_version(self) -> Tuple[str, List[str]]: chosen_version = "" @@ -161,6 +161,10 @@ def _is_paging(operation): self.code_model.options["package_version"] = "0.1.0" template = self.env.get_template("metadata.json.jinja2") + client_serialize_namespace = self.code_model.get_serialize_namespace(self.client_namespace, async_mode=False) + client_serialize_namespace_async = self.code_model.get_serialize_namespace( + self.client_namespace, async_mode=True + ) return template.render( code_model=self.code_model, chosen_version=chosen_version, @@ -176,23 +180,37 @@ def _is_paging(operation): async_mixin_imports=async_mixin_imports, sync_mixin_typing_definitions=sync_mixin_typing_definitions, async_mixin_typing_definitions=async_mixin_typing_definitions, - sync_client_imports=_json_serialize_imports(self.client.imports_for_multiapi(async_mode=False).to_dict()), - async_client_imports=_json_serialize_imports(self.client.imports_for_multiapi(async_mode=True).to_dict()), + sync_client_imports=_json_serialize_imports( + self.client.imports_for_multiapi( + async_mode=False, serialize_namespace=client_serialize_namespace + ).to_dict() + ), + async_client_imports=_json_serialize_imports( + self.client.imports_for_multiapi( + async_mode=True, serialize_namespace=client_serialize_namespace_async + ).to_dict() + ), sync_config_imports=_json_serialize_imports( - self.client.config.imports_for_multiapi(async_mode=False).to_dict() + self.client.config.imports_for_multiapi( + async_mode=False, serialize_namespace=client_serialize_namespace + ).to_dict() ), async_config_imports=_json_serialize_imports( - self.client.config.imports_for_multiapi(async_mode=True).to_dict() + self.client.config.imports_for_multiapi( + async_mode=True, serialize_namespace=client_serialize_namespace_async + ).to_dict() ), get_async_operation_serializer=functools.partial( get_operation_serializer, code_model=self.client.code_model, async_mode=True, + client_namespace=self.client_namespace, ), get_sync_operation_serializer=functools.partial( get_operation_serializer, code_model=self.client.code_model, async_mode=False, + client_namespace=self.client_namespace, ), has_credential=bool(self.client.credential), ) diff --git a/packages/http-client-python/generator/pygen/codegen/serializers/model_init_serializer.py b/packages/http-client-python/generator/pygen/codegen/serializers/model_init_serializer.py index 5df688adbb..2b6a664798 100644 --- a/packages/http-client-python/generator/pygen/codegen/serializers/model_init_serializer.py +++ b/packages/http-client-python/generator/pygen/codegen/serializers/model_init_serializer.py @@ -3,19 +3,24 @@ # Licensed under the MIT License. See License.txt in the project root for # license information. # -------------------------------------------------------------------------- +from typing import List from jinja2 import Environment -from ..models import CodeModel +from ..models import CodeModel, ModelType, EnumType class ModelInitSerializer: - def __init__(self, code_model: CodeModel, env: Environment) -> None: + def __init__( + self, code_model: CodeModel, env: Environment, *, models: List[ModelType], enums: List[EnumType] + ) -> None: self.code_model = code_model self.env = env + self.models = models + self.enums = enums def serialize(self) -> str: - schemas = [s.name for s in self.code_model.public_model_types] + schemas = [s.name for s in self.code_model.get_public_model_types(self.models)] schemas.sort() - enums = [e.name for e in self.code_model.enums if not e.internal] if self.code_model.enums else None + enums = [e.name for e in self.enums if not e.internal] if self.enums else None if enums: enums.sort() diff --git a/packages/http-client-python/generator/pygen/codegen/serializers/model_serializer.py b/packages/http-client-python/generator/pygen/codegen/serializers/model_serializer.py index 6ad0fab40a..4659af47d2 100644 --- a/packages/http-client-python/generator/pygen/codegen/serializers/model_serializer.py +++ b/packages/http-client-python/generator/pygen/codegen/serializers/model_serializer.py @@ -3,13 +3,14 @@ # Licensed under the MIT License. See License.txt in the project root for # license information. # -------------------------------------------------------------------------- -from typing import List +from typing import List, Optional from abc import ABC, abstractmethod from ..models import ModelType, Property, ConstantType, EnumValue from ..models.imports import FileImport, TypingSection, MsrestImportType, ImportType from .import_serializer import FileImportSerializer from .base_serializer import BaseSerializer +from ..models.utils import NamespaceType def _documentation_string(prop: Property, description_keyword: str, docstring_type_keyword: str) -> List[str]: @@ -22,6 +23,12 @@ def _documentation_string(prop: Property, description_keyword: str, docstring_ty class _ModelSerializer(BaseSerializer, ABC): + def __init__( + self, code_model, env, async_mode=False, *, models: List[ModelType], client_namespace: Optional[str] = None + ): + super().__init__(code_model, env, async_mode, client_namespace=client_namespace) + self.models = models + @abstractmethod def imports(self) -> FileImport: ... @@ -33,6 +40,7 @@ def serialize(self) -> str: imports=FileImportSerializer(self.imports()), str=str, serializer=self, + models=self.models, ) @abstractmethod @@ -63,12 +71,12 @@ def initialize_discriminator_property(model: ModelType, prop: Property) -> str: typing = "str" return f"self.{prop.client_name}: {typing} = {discriminator_value}" - @staticmethod - def initialize_standard_property(prop: Property): + def initialize_standard_property(self, prop: Property): if not (prop.optional or prop.client_default_value is not None): - return f"{prop.client_name}: {prop.type_annotation()},{prop.pylint_disable()}" + type_annotation = prop.type_annotation(serialize_namespace=self.serialize_namespace) + return f"{prop.client_name}: {type_annotation},{prop.pylint_disable()}" return ( - f"{prop.client_name}: {prop.type_annotation()} = " + f"{prop.client_name}: {prop.type_annotation(serialize_namespace=self.serialize_namespace)} = " f"{prop.client_default_value_declaration},{prop.pylint_disable()}" ) @@ -127,19 +135,29 @@ def pylint_disable(self, model: ModelType) -> str: def global_pylint_disables(self) -> str: return "" + @property + def serialize_namespace(self) -> str: + return self.code_model.get_serialize_namespace(self.client_namespace, client_namespace_type=NamespaceType.MODEL) + class MsrestModelSerializer(_ModelSerializer): def imports(self) -> FileImport: file_import = FileImport(self.code_model) file_import.add_msrest_import( - relative_path="..", + serialize_namespace=self.serialize_namespace, msrest_import_type=MsrestImportType.Module, typing_section=TypingSection.REGULAR, ) - for model in self.code_model.model_types: + for model in self.models: file_import.merge(model.imports(is_operation_file=False)) for param in self._init_line_parameters(model): - file_import.merge(param.imports()) + file_import.merge( + param.imports( + serialize_namespace=self.serialize_namespace, + serialize_namespace_type=NamespaceType.MODEL, + called_by_property=True, + ) + ) return file_import @@ -209,19 +227,41 @@ def super_call(self, model: ModelType) -> List[str]: def imports(self) -> FileImport: file_import = FileImport(self.code_model) - file_import.add_submodule_import( - "..", - "_model_base", - ImportType.LOCAL, - TypingSection.REGULAR, - ) - - for model in self.code_model.model_types: + if any(not m.parents for m in self.models): + file_import.add_submodule_import( + self.code_model.get_relative_import_path(self.serialize_namespace), + "_model_base", + ImportType.LOCAL, + TypingSection.REGULAR, + ) + for model in self.models: if model.base == "json": continue - file_import.merge(model.imports(is_operation_file=False)) + file_import.merge( + model.imports( + is_operation_file=False, + serialize_namespace=self.serialize_namespace, + serialize_namespace_type=NamespaceType.MODEL, + ) + ) for prop in model.properties: - file_import.merge(prop.imports()) + file_import.merge( + prop.imports( + serialize_namespace=self.serialize_namespace, + serialize_namespace_type=NamespaceType.MODEL, + called_by_property=True, + ) + ) + for parent in model.parents: + if parent.client_namespace != model.client_namespace: + file_import.add_submodule_import( + self.code_model.get_relative_import_path( + self.serialize_namespace, + self.code_model.get_imported_namespace_for_model(parent.client_namespace), + ), + parent.name, + ImportType.LOCAL, + ) if model.is_polymorphic: file_import.add_submodule_import("typing", "Dict", ImportType.STDLIB) if not model.internal and self.init_line(model): @@ -258,8 +298,7 @@ def get_properties_to_declare(model: ModelType) -> List[Property]: raise ValueError("We do not generate anonymous properties") return properties_to_declare - @staticmethod - def declare_property(prop: Property) -> str: + def declare_property(self, prop: Property) -> str: args = [] if prop.client_name != prop.wire_name or prop.is_discriminator: args.append(f'name="{prop.wire_name}"') @@ -283,7 +322,8 @@ def declare_property(prop: Property) -> str: if prop.is_discriminator and isinstance(prop.type, (ConstantType, EnumValue)) and prop.type.value else "" ) - generated_code = f'{prop.client_name}: {prop.type_annotation()} = {field}({", ".join(args)})' + type_annotation = prop.type_annotation(serialize_namespace=self.serialize_namespace) + generated_code = f'{prop.client_name}: {type_annotation} = {field}({", ".join(args)})' # there is 4 spaces indentation so original line length limit 120 - 4 = 116 pylint_disable = ( " # pylint: disable=line-too-long" @@ -324,7 +364,7 @@ def properties_to_pass_to_super(model: ModelType) -> str: def global_pylint_disables(self) -> str: result = [] - for model in self.code_model.model_types: + for model in self.models: if self.need_init(model): for item in self.pylint_disable_items(model): if item: diff --git a/packages/http-client-python/generator/pygen/codegen/serializers/operation_groups_serializer.py b/packages/http-client-python/generator/pygen/codegen/serializers/operation_groups_serializer.py index 4c04efbcd2..c95236178f 100644 --- a/packages/http-client-python/generator/pygen/codegen/serializers/operation_groups_serializer.py +++ b/packages/http-client-python/generator/pygen/codegen/serializers/operation_groups_serializer.py @@ -7,15 +7,14 @@ import functools from jinja2 import Environment -from .utils import get_all_operation_groups_recursively from ..models import ( CodeModel, OperationGroup, RequestBuilder, OverloadedRequestBuilder, - Client, FileImport, ) +from ..models.utils import NamespaceType from .import_serializer import FileImportSerializer from .builder_serializer import ( get_operation_serializer, @@ -28,23 +27,22 @@ class OperationGroupsSerializer(BaseSerializer): def __init__( self, code_model: CodeModel, - clients: List[Client], + operation_groups: List[OperationGroup], env: Environment, async_mode: bool, - operation_group: Optional[OperationGroup] = None, + *, + client_namespace: Optional[str] = None, ): - super().__init__(code_model, env) - self.clients = clients + super().__init__(code_model, env, async_mode, client_namespace=client_namespace) + self.operation_groups = operation_groups self.async_mode = async_mode - self.operation_group = operation_group def _get_request_builders( self, operation_group: OperationGroup ) -> List[Union[OverloadedRequestBuilder, RequestBuilder]]: return [ r - for client in self.clients - for r in client.request_builders + for r in operation_group.client.request_builders if r.client.name == operation_group.client.name and r.group_name == operation_group.identify_name and not r.is_overload @@ -52,17 +50,20 @@ def _get_request_builders( and not r.is_lro # lro has already initial builder ] - def serialize(self) -> str: - if self.operation_group: - operation_groups = [self.operation_group] - else: - operation_groups = get_all_operation_groups_recursively(self.clients) + @property + def serialize_namespace(self) -> str: + return self.code_model.get_serialize_namespace( + self.client_namespace, async_mode=self.async_mode, client_namespace_type=NamespaceType.OPERATION + ) + def serialize(self) -> str: imports = FileImport(self.code_model) - for operation_group in operation_groups: + for operation_group in self.operation_groups: imports.merge( operation_group.imports( async_mode=self.async_mode, + serialize_namespace=self.serialize_namespace, + serialize_namespace_type=NamespaceType.OPERATION, ) ) @@ -70,7 +71,7 @@ def serialize(self) -> str: return template.render( code_model=self.code_model, - operation_groups=operation_groups, + operation_groups=self.operation_groups, imports=FileImportSerializer( imports, async_mode=self.async_mode, @@ -80,10 +81,12 @@ def serialize(self) -> str: get_operation_serializer, code_model=self.code_model, async_mode=self.async_mode, + client_namespace=self.client_namespace, ), request_builder_serializer=RequestBuilderSerializer( self.code_model, async_mode=False, + client_namespace=self.client_namespace, ), get_request_builders=self._get_request_builders, ) diff --git a/packages/http-client-python/generator/pygen/codegen/serializers/operations_init_serializer.py b/packages/http-client-python/generator/pygen/codegen/serializers/operations_init_serializer.py index d0d30c03eb..0b2a0685fd 100644 --- a/packages/http-client-python/generator/pygen/codegen/serializers/operations_init_serializer.py +++ b/packages/http-client-python/generator/pygen/codegen/serializers/operations_init_serializer.py @@ -6,20 +6,19 @@ from typing import List from jinja2 import Environment -from ..models.operation_group import OperationGroup -from ..models import CodeModel, Client +from ..models import CodeModel, OperationGroup class OperationsInitSerializer: def __init__( self, code_model: CodeModel, - clients: List[Client], + operation_groups: List[OperationGroup], env: Environment, async_mode: bool, ) -> None: self.code_model = code_model - self.clients = clients + self.operation_groups = [og for og in operation_groups if not og.has_parent_operation_group] self.env = env self.async_mode = async_mode @@ -27,11 +26,7 @@ def operation_group_imports(self) -> List[str]: def _get_filename(operation_group: OperationGroup) -> str: return "_operations" if self.code_model.options["combine_operation_files"] else operation_group.filename - return [ - f"from .{_get_filename(og)} import {og.class_name} # type: ignore" - for client in self.clients - for og in client.operation_groups - ] + return [f"from .{_get_filename(og)} import {og.class_name} # type: ignore" for og in self.operation_groups] def serialize(self) -> str: operation_group_init_template = self.env.get_template("operations_folder_init.py.jinja2") @@ -40,5 +35,5 @@ def serialize(self) -> str: code_model=self.code_model, async_mode=self.async_mode, operation_group_imports=self.operation_group_imports, - clients=self.clients, + operation_groups=self.operation_groups, ) diff --git a/packages/http-client-python/generator/pygen/codegen/serializers/parameter_serializer.py b/packages/http-client-python/generator/pygen/codegen/serializers/parameter_serializer.py index f18cd7e1ec..1a94530a0f 100644 --- a/packages/http-client-python/generator/pygen/codegen/serializers/parameter_serializer.py +++ b/packages/http-client-python/generator/pygen/codegen/serializers/parameter_serializer.py @@ -52,8 +52,11 @@ class PopKwargType(Enum): class ParameterSerializer: - @staticmethod - def serialize_parameter(parameter: ParameterType, serializer_name: str) -> str: + + def __init__(self, serialize_namespace: str) -> None: + self.serialize_namespace = serialize_namespace + + def serialize_parameter(self, parameter: ParameterType, serializer_name: str) -> str: optional_parameters = [] if parameter.skip_url_encoding: @@ -88,7 +91,7 @@ def serialize_parameter(parameter: ParameterType, serializer_name: str) -> str: parameters = [ f'"{origin_name.lstrip("_")}"', "q" if parameter.explode else origin_name, - f"'{type.serialization_type}'", + f"'{type.serialization_type(serialize_namespace=self.serialize_namespace)}'", *optional_parameters, ] parameters_line = ", ".join(parameters) @@ -106,8 +109,8 @@ def serialize_parameter(parameter: ParameterType, serializer_name: str) -> str: return f"[{serialize_line} if q is not None else '' for q in {origin_name}]" return serialize_line - @staticmethod def serialize_path( + self, parameters: Union[ List[Parameter], List[RequestBuilderParameter], @@ -121,7 +124,7 @@ def serialize_path( [ ' "{}": {},'.format( path_parameter.wire_name, - ParameterSerializer.serialize_parameter(path_parameter, serializer_name), + self.serialize_parameter(path_parameter, serializer_name), ) for path_parameter in parameters ] @@ -129,8 +132,8 @@ def serialize_path( retval.append("}") return retval - @staticmethod def serialize_query_header( + self, param: Parameter, kwarg_name: str, serializer_name: str, @@ -146,7 +149,7 @@ def serialize_query_header( set_parameter = "_{}['{}'] = {}".format( kwarg_name, param.wire_name, - ParameterSerializer.serialize_parameter(param, serializer_name), + self.serialize_parameter(param, serializer_name), ) if not param.optional and (param.in_method_signature or param.constant): retval = [set_parameter] diff --git a/packages/http-client-python/generator/pygen/codegen/serializers/request_builders_serializer.py b/packages/http-client-python/generator/pygen/codegen/serializers/request_builders_serializer.py index f982984364..2f7d4a95c4 100644 --- a/packages/http-client-python/generator/pygen/codegen/serializers/request_builders_serializer.py +++ b/packages/http-client-python/generator/pygen/codegen/serializers/request_builders_serializer.py @@ -7,6 +7,7 @@ from jinja2 import Environment from ..models import FileImport +from ..models.utils import NamespaceType from .import_serializer import FileImportSerializer from ..models import CodeModel, RequestBuilderType from .builder_serializer import RequestBuilderSerializer @@ -39,6 +40,12 @@ def serialize_init(self) -> str: request_builders=[r for r in self.request_builders if not r.is_overload], ) + @property + def serialize_namespace(self) -> str: + return self.code_model.get_serialize_namespace( + self.client_namespace, client_namespace_type=NamespaceType.OPERATION + ) + def serialize_request_builders(self) -> str: template = self.env.get_template("request_builders.py.jinja2") @@ -48,5 +55,7 @@ def serialize_request_builders(self) -> str: imports=FileImportSerializer( self.imports, ), - request_builder_serializer=RequestBuilderSerializer(self.code_model, async_mode=False), + request_builder_serializer=RequestBuilderSerializer( + self.code_model, async_mode=False, client_namespace=self.client_namespace + ), ) diff --git a/packages/http-client-python/generator/pygen/codegen/serializers/sample_serializer.py b/packages/http-client-python/generator/pygen/codegen/serializers/sample_serializer.py index 024c4d4034..cc587e5179 100644 --- a/packages/http-client-python/generator/pygen/codegen/serializers/sample_serializer.py +++ b/packages/http-client-python/generator/pygen/codegen/serializers/sample_serializer.py @@ -20,7 +20,7 @@ BodyParameter, FileImport, ) -from .utils import get_namespace_config, get_namespace_from_package_name +from .._utils import get_parent_namespace _LOGGER = logging.getLogger(__name__) @@ -44,15 +44,12 @@ def __init__( def _imports(self) -> FileImportSerializer: imports = FileImport(self.code_model) - namespace_from_package_name = get_namespace_from_package_name(self.code_model.options["package_name"]) - namespace_config = get_namespace_config(self.code_model.namespace, self.code_model.options["multiapi"]) - namespace = namespace_from_package_name or namespace_config - # mainly for "azure-mgmt-rdbms" - if not self.code_model.options["multiapi"] and namespace_config.count(".") > namespace_from_package_name.count( - "." - ): - namespace = namespace_config - client = self.code_model.clients[0] + client = self.operation_group.client + namespace = ( + get_parent_namespace(client.client_namespace) + if self.code_model.options["multiapi"] + else client.client_namespace + ) imports.add_submodule_import(namespace, client.name, ImportType.LOCAL) credential_type = getattr(client.credential, "type", None) if isinstance(credential_type, TokenCredentialType): diff --git a/packages/http-client-python/generator/pygen/codegen/serializers/test_serializer.py b/packages/http-client-python/generator/pygen/codegen/serializers/test_serializer.py index 7ae55ab093..3925f34117 100644 --- a/packages/http-client-python/generator/pygen/codegen/serializers/test_serializer.py +++ b/packages/http-client-python/generator/pygen/codegen/serializers/test_serializer.py @@ -35,18 +35,18 @@ def is_common_operation(operation_type: str) -> bool: class TestName: - def __init__(self, code_model: CodeModel, client_name: str, *, is_async: bool = False) -> None: + def __init__(self, code_model: CodeModel, client_name: str, *, async_mode: bool = False) -> None: self.code_model = code_model self.client_name = client_name - self.is_async = is_async + self.async_mode = async_mode @property def async_suffix_capt(self) -> str: - return "Async" if self.is_async else "" + return "Async" if self.async_mode else "" @property def create_client_name(self) -> str: - return "create_async_client" if self.is_async else "create_client" + return "create_async_client" if self.async_mode else "create_client" @property def prefix(self) -> str: @@ -72,12 +72,12 @@ def __init__( params: Dict[str, Any], operation: OperationType, *, - is_async: bool = False, + async_mode: bool = False, ) -> None: self.operation_groups = operation_groups self.params = params self.operation = operation - self.is_async = is_async + self.async_mode = async_mode @property def name(self) -> str: @@ -93,7 +93,7 @@ def operation_group_prefix(self) -> str: @property def response(self) -> str: - if self.is_async: + if self.async_mode: if is_lro(self.operation.operation_type): return "response = await (await " if is_common_operation(self.operation.operation_type): @@ -107,14 +107,14 @@ def lro_comment(self) -> str: @property def operation_suffix(self) -> str: if is_lro(self.operation.operation_type): - extra = ")" if self.is_async else "" + extra = ")" if self.async_mode else "" return f"{extra}.result(){self.lro_comment}" return "" @property def extra_operation(self) -> str: if is_paging(self.operation.operation_type): - async_str = "async " if self.is_async else "" + async_str = "async " if self.async_mode else "" return f"result = [r {async_str}for r in response]" return "" @@ -128,43 +128,39 @@ def __init__( testcases: List[TestCase], test_class_name: str, *, - is_async: bool = False, + async_mode: bool = False, ) -> None: - super().__init__(code_model, client_name, is_async=is_async) + super().__init__(code_model, client_name, async_mode=async_mode) self.operation_group = operation_group self.testcases = testcases self.test_class_name = test_class_name class TestGeneralSerializer(BaseSerializer): - def __init__(self, code_model: CodeModel, env: Environment, *, is_async: bool = False) -> None: - super().__init__(code_model, env) - self.is_async = is_async @property def aio_str(self) -> str: - return ".aio" if self.is_async else "" + return ".aio" if self.async_mode else "" @property def test_names(self) -> List[TestName]: - return [TestName(self.code_model, c.name, is_async=self.is_async) for c in self.code_model.clients] + return [TestName(self.code_model, c.name, async_mode=self.async_mode) for c in self.code_model.clients] def add_import_client(self, imports: FileImport) -> None: - namespace = get_namespace_from_package_name(self.code_model.options["package_name"]) for client in self.code_model.clients: - imports.add_submodule_import(namespace + self.aio_str, client.name, ImportType.STDLIB) + imports.add_submodule_import(client.client_namespace + self.aio_str, client.name, ImportType.STDLIB) @property def import_clients(self) -> FileImportSerializer: imports = self.init_file_import() imports.add_submodule_import("devtools_testutils", "AzureRecordedTestCase", ImportType.STDLIB) - if not self.is_async: + if not self.async_mode: imports.add_import("functools", ImportType.STDLIB) imports.add_submodule_import("devtools_testutils", "PowerShellPreparer", ImportType.STDLIB) self.add_import_client(imports) - return FileImportSerializer(imports, self.is_async) + return FileImportSerializer(imports, self.async_mode) def serialize_conftest(self) -> str: return self.env.get_template("conftest.py.jinja2").render( @@ -188,17 +184,17 @@ def __init__( *, client: Client, operation_group: OperationGroup, - is_async: bool = False, + async_mode: bool = False, ) -> None: - super().__init__(code_model, env, is_async=is_async) + super().__init__(code_model, env, async_mode=async_mode) self.client = client self.operation_group = operation_group @property def import_test(self) -> FileImportSerializer: imports = self.init_file_import() - test_name = TestName(self.code_model, self.client.name, is_async=self.is_async) - async_suffix = "_async" if self.is_async else "" + test_name = TestName(self.code_model, self.client.name, async_mode=self.async_mode) + async_suffix = "_async" if self.async_mode else "" imports.add_submodule_import( "devtools_testutils" if self.code_model.options["azure_arm"] else "testpreparer" + async_suffix, test_name.base_test_class_name, @@ -216,7 +212,7 @@ def import_test(self) -> FileImportSerializer: ) if self.code_model.options["azure_arm"]: self.add_import_client(imports) - return FileImportSerializer(imports, self.is_async) + return FileImportSerializer(imports, self.async_mode) @property def breadth_search_operation_group(self) -> List[List[OperationGroup]]: @@ -263,7 +259,7 @@ def get_test(self) -> Test: operation_groups=operation_groups, params=operation_params, operation=operation, - is_async=self.is_async, + async_mode=self.async_mode, ) testcases.append(testcase) if not testcases: @@ -275,12 +271,12 @@ def get_test(self) -> Test: operation_group=self.operation_group, testcases=testcases, test_class_name=self.test_class_name, - is_async=self.is_async, + async_mode=self.async_mode, ) @property def test_class_name(self) -> str: - test_name = TestName(self.code_model, self.client.name, is_async=self.is_async) + test_name = TestName(self.code_model, self.client.name, async_mode=self.async_mode) class_name = "" if self.operation_group.is_mixin else self.operation_group.class_name return f"Test{test_name.prefix}{class_name}{test_name.async_suffix_capt}" diff --git a/packages/http-client-python/generator/pygen/codegen/serializers/types_serializer.py b/packages/http-client-python/generator/pygen/codegen/serializers/types_serializer.py index 3e143f5209..749d8ca240 100644 --- a/packages/http-client-python/generator/pygen/codegen/serializers/types_serializer.py +++ b/packages/http-client-python/generator/pygen/codegen/serializers/types_serializer.py @@ -4,6 +4,7 @@ # license information. # -------------------------------------------------------------------------- from ..models.imports import FileImport, ImportType +from ..models.utils import NamespaceType from .import_serializer import FileImportSerializer from .base_serializer import BaseSerializer @@ -18,7 +19,11 @@ def imports(self) -> FileImport: ImportType.STDLIB, ) for nu in self.code_model.named_unions: - file_import.merge(nu.imports(relative_path=".", model_typing=True, is_types_file=True)) + file_import.merge( + nu.imports( + serialize_namespace=self.serialize_namespace, serialize_namespace_type=NamespaceType.TYPES_FILE + ) + ) return file_import def serialize(self) -> str: diff --git a/packages/http-client-python/generator/pygen/codegen/serializers/utils.py b/packages/http-client-python/generator/pygen/codegen/serializers/utils.py index 7cd6f7c926..e3defdd349 100644 --- a/packages/http-client-python/generator/pygen/codegen/serializers/utils.py +++ b/packages/http-client-python/generator/pygen/codegen/serializers/utils.py @@ -4,11 +4,9 @@ # license information. # -------------------------------------------------------------------------- import json -from typing import Optional, List, Any +from typing import Optional, Any from pathlib import Path -from ..models import Client, OperationGroup - def method_signature_and_response_type_annotation_template( *, @@ -35,18 +33,6 @@ def get_namespace_from_package_name(package_name: Optional[str]) -> str: return (package_name or "").replace("-", ".") -def get_all_operation_groups_recursively(clients: List[Client]) -> List[OperationGroup]: - operation_groups = [] - queue = [] - for client in clients: - queue.extend(client.operation_groups) - while queue: - operation_groups.append(queue.pop(0)) - if operation_groups[-1].operation_groups: - queue.extend(operation_groups[-1].operation_groups) - return operation_groups - - def _improve_json_string(template_representation: str) -> Any: origin = template_representation.split("\n") final = [] diff --git a/packages/http-client-python/generator/pygen/codegen/templates/client_container.py.jinja2 b/packages/http-client-python/generator/pygen/codegen/templates/client_container.py.jinja2 index 33de339aff..25d4dca8d7 100644 --- a/packages/http-client-python/generator/pygen/codegen/templates/client_container.py.jinja2 +++ b/packages/http-client-python/generator/pygen/codegen/templates/client_container.py.jinja2 @@ -7,6 +7,6 @@ {{ imports }} {% for client in clients %} - {% set serializer = get_serializer(client) %} + {% set serializer = get_serializer(client, serialize_namespace) %} {% include "client.py.jinja2" %} {% endfor %} diff --git a/packages/http-client-python/generator/pygen/codegen/templates/config_container.py.jinja2 b/packages/http-client-python/generator/pygen/codegen/templates/config_container.py.jinja2 index 9a3c263e2f..72179cd890 100644 --- a/packages/http-client-python/generator/pygen/codegen/templates/config_container.py.jinja2 +++ b/packages/http-client-python/generator/pygen/codegen/templates/config_container.py.jinja2 @@ -11,6 +11,6 @@ VERSION = "unknown" {% endif %} {% for client in clients %} - {% set serializer = get_serializer(client) %} + {% set serializer = get_serializer(client, serialize_namespace) %} {% include "config.py.jinja2" %} {% endfor %} diff --git a/packages/http-client-python/generator/pygen/codegen/templates/enum_container.py.jinja2 b/packages/http-client-python/generator/pygen/codegen/templates/enum_container.py.jinja2 index 099fbb072a..55439c7ed5 100644 --- a/packages/http-client-python/generator/pygen/codegen/templates/enum_container.py.jinja2 +++ b/packages/http-client-python/generator/pygen/codegen/templates/enum_container.py.jinja2 @@ -5,6 +5,6 @@ from enum import Enum from {{ code_model.core_library }}{{ "" if code_model.is_azure_flavor else ".utils" }} import CaseInsensitiveEnumMeta -{% for enum in code_model.enums | sort %} +{% for enum in enums | sort %} {% include "enum.py.jinja2" %} {% endfor %} diff --git a/packages/http-client-python/generator/pygen/codegen/templates/init.py.jinja2 b/packages/http-client-python/generator/pygen/codegen/templates/init.py.jinja2 index ce16adcbcf..3d05188843 100644 --- a/packages/http-client-python/generator/pygen/codegen/templates/init.py.jinja2 +++ b/packages/http-client-python/generator/pygen/codegen/templates/init.py.jinja2 @@ -8,7 +8,7 @@ from .{{ client.filename }} import {{ client.name }} # type: ignore {% endfor %} {% endif %} {% if not async_mode and code_model.options['package_version']%} -from ._version import VERSION +from {{ code_model.get_relative_import_path(serialize_namespace, module_name="_version") }} import VERSION __version__ = VERSION {% endif %} diff --git a/packages/http-client-python/generator/pygen/codegen/templates/model_container.py.jinja2 b/packages/http-client-python/generator/pygen/codegen/templates/model_container.py.jinja2 index b2ec433efc..fb0337e360 100644 --- a/packages/http-client-python/generator/pygen/codegen/templates/model_container.py.jinja2 +++ b/packages/http-client-python/generator/pygen/codegen/templates/model_container.py.jinja2 @@ -6,7 +6,7 @@ {% endif %} {{ imports }} -{% for model in code_model.model_types %} +{% for model in models %} {% if model.base == "dpg" %} {% include "model_dpg.py.jinja2" %} {% elif model.base == "msrest" %} diff --git a/packages/http-client-python/generator/pygen/codegen/templates/operations_folder_init.py.jinja2 b/packages/http-client-python/generator/pygen/codegen/templates/operations_folder_init.py.jinja2 index e4ad6e4368..9fbe74c65c 100644 --- a/packages/http-client-python/generator/pygen/codegen/templates/operations_folder_init.py.jinja2 +++ b/packages/http-client-python/generator/pygen/codegen/templates/operations_folder_init.py.jinja2 @@ -7,11 +7,9 @@ {{ op_tools.serialize(operation_group_imports()) }} {{ keywords.patch_imports() }} __all__ = [ - {% for client in clients %} - {% for operation_group in client.operation_groups %} + {% for operation_group in operation_groups %} '{{ operation_group.class_name }}', - {% endfor %} - {% endfor %} + {% endfor %} ] {{ keywords.extend_all }} _patch_sdk() diff --git a/packages/http-client-python/generator/pygen/codegen/templates/test.py.jinja2 b/packages/http-client-python/generator/pygen/codegen/templates/test.py.jinja2 index 25f8da7b53..7d5dded167 100644 --- a/packages/http-client-python/generator/pygen/codegen/templates/test.py.jinja2 +++ b/packages/http-client-python/generator/pygen/codegen/templates/test.py.jinja2 @@ -1,7 +1,7 @@ {% set prefix_lower = test.prefix|lower %} {% set client_var = "self.client" if code_model.options["azure_arm"] else "client" %} -{% set async = "async " if test.is_async else "" %} -{% set async_suffix = "_async" if test.is_async else "" %} +{% set async = "async " if test.async_mode else "" %} +{% set async_suffix = "_async" if test.async_mode else "" %} # coding=utf-8 {{ code_model.options['license_header'] }} import pytest @@ -15,7 +15,7 @@ AZURE_LOCATION = "eastus" class {{ test.test_class_name }}({{ test.base_test_class_name }}): {% if code_model.options["azure_arm"] %} def setup_method(self, method): - {% if test.is_async %} + {% if test.async_mode %} self.client = self.create_mgmt_client({{ test.client_name }}, is_async=True) {% else %} self.client = self.create_mgmt_client({{ test.client_name }}) diff --git a/packages/http-client-python/generator/pygen/codegen/templates/testpreparer.py.jinja2 b/packages/http-client-python/generator/pygen/codegen/templates/testpreparer.py.jinja2 index b3b15f3727..930d9825fd 100644 --- a/packages/http-client-python/generator/pygen/codegen/templates/testpreparer.py.jinja2 +++ b/packages/http-client-python/generator/pygen/codegen/templates/testpreparer.py.jinja2 @@ -3,7 +3,7 @@ {{ imports }} {% for test_name in test_names %} -{% set extra_async = ", is_async=True" if test_name.is_async else ""%} +{% set extra_async = ", is_async=True" if test_name.async_mode else ""%} {% set prefix_lower = test_name.prefix|lower %} class {{ test_name.base_test_class_name }}(AzureRecordedTestCase): @@ -15,7 +15,7 @@ class {{ test_name.base_test_class_name }}(AzureRecordedTestCase): endpoint=endpoint, ) -{% if not test_name.is_async %} +{% if not test_name.async_mode %} {{ test_name.preparer_name }} = functools.partial( PowerShellPreparer, "{{ prefix_lower }}", diff --git a/packages/http-client-python/generator/pygen/codegen/templates/vendor.py.jinja2 b/packages/http-client-python/generator/pygen/codegen/templates/vendor.py.jinja2 index 512e3f2a17..b92ef3b5a2 100644 --- a/packages/http-client-python/generator/pygen/codegen/templates/vendor.py.jinja2 +++ b/packages/http-client-python/generator/pygen/codegen/templates/vendor.py.jinja2 @@ -3,7 +3,7 @@ {{ imports }} -{% if code_model.need_mixin_abc %} +{% if code_model.need_vendored_mixin(client_namespace) %} {% for client in clients | selectattr("has_mixin") %} {% set pylint_disable = "# pylint: disable=name-too-long" if (client.name | length) + ("MixinABC" | length) > 40 else "" %} class {{ client.name }}MixinABC( {{ pylint_disable }} @@ -16,7 +16,7 @@ class {{ client.name }}MixinABC( {{ pylint_disable }} _deserialize: "Deserializer" {% endfor %} {% endif %} -{% if code_model.has_abstract_operations %} +{% if code_model.need_vendored_abstract(client_namespace) %} def raise_if_not_implemented(cls, abstract_methods): not_implemented = [f for f in abstract_methods if not callable(getattr(cls, f, None))] @@ -27,7 +27,7 @@ def raise_if_not_implemented(cls, abstract_methods): ) {% endif %} -{% if code_model.has_etag %} +{% if code_model.need_vendored_etag(client_namespace) %} def quote_etag(etag: Optional[str]) -> Optional[str]: if not etag or etag == "*": return etag @@ -57,7 +57,7 @@ def prep_if_none_match(etag: Optional[str], match_condition: Optional[MatchCondi return "*" return None {% endif %} -{% if code_model.has_form_data and code_model.options["models_mode"] == "dpg" and not async_mode %} +{% if code_model.need_vendored_form_data(async_mode, client_namespace) %} # file-like tuple could be `(filename, IO (or bytes))` or `(filename, IO (or bytes), content_type)` FileContent = Union[str, bytes, IO[str], IO[bytes]] diff --git a/packages/http-client-python/generator/pygen/preprocess/__init__.py b/packages/http-client-python/generator/pygen/preprocess/__init__.py index cfddddb860..d1407930f0 100644 --- a/packages/http-client-python/generator/pygen/preprocess/__init__.py +++ b/packages/http-client-python/generator/pygen/preprocess/__init__.py @@ -501,10 +501,6 @@ def update_yaml(self, yaml_data: Dict[str, Any]) -> None: for client in yaml_data["clients"]: self.update_client(client) self.update_operation_groups(yaml_data, client) - for clients in yaml_data["subnamespaceToClients"].values(): - for client in clients: - self.update_client(client) - self.update_operation_groups(yaml_data, client) if yaml_data.get("namespace"): yaml_data["namespace"] = pad_builtin_namespaces(yaml_data["namespace"]) diff --git a/packages/http-client-python/generator/test/azure/mock_api_tests/asynctests/test_client_namespace_async.py b/packages/http-client-python/generator/test/azure/mock_api_tests/asynctests/test_client_namespace_async.py new file mode 100644 index 0000000000..206cac35a9 --- /dev/null +++ b/packages/http-client-python/generator/test/azure/mock_api_tests/asynctests/test_client_namespace_async.py @@ -0,0 +1,31 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- +# import pytest +# from client.clientnamespace.aio import ClientNamespaceFirstClient +# from client.clientnamespace.first.models import FirstClientResult + +# from client.clientnamespace.second.aio import ClientNamespaceSecondClient +# from client.clientnamespace.second.models import SecondClientResult +# from client.clientnamespace.second.sub.models import SecondClientEnumType + + +# @pytest.fixture +# async def first_client(): +# async with ClientNamespaceFirstClient() as client: +# yield client + +# @pytest.fixture +# async def second_client(): +# async with ClientNamespaceSecondClient() as client: +# yield client + +# @pytest.mark.asyncio +# async def test_get_first(first_client: ClientNamespaceFirstClient): +# assert await first_client.get_first() == FirstClientResult(name="first") + +# @pytest.mark.asyncio +# async def test_get_second(second_client: ClientNamespaceSecondClient): +# assert await second_client.get_second() == SecondClientResult(type=SecondClientEnumType.SECOND) diff --git a/packages/http-client-python/generator/test/azure/mock_api_tests/test_client_namespace.py b/packages/http-client-python/generator/test/azure/mock_api_tests/test_client_namespace.py new file mode 100644 index 0000000000..201b6107d9 --- /dev/null +++ b/packages/http-client-python/generator/test/azure/mock_api_tests/test_client_namespace.py @@ -0,0 +1,29 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- +# import pytest +# from client.clientnamespace import ClientNamespaceFirstClient +# from client.clientnamespace.first.models import FirstClientResult + +# from client.clientnamespace.second import ClientNamespaceSecondClient +# from client.clientnamespace.second.models import SecondClientResult +# from client.clientnamespace.second.sub.models import SecondClientEnumType + + +# @pytest.fixture +# def first_client(): +# with ClientNamespaceFirstClient() as client: +# yield client + +# @pytest.fixture +# def second_client(): +# with ClientNamespaceSecondClient() as client: +# yield client + +# def test_get_first(first_client: ClientNamespaceFirstClient): +# assert first_client.get_first() == FirstClientResult(name="first") + +# def test_get_second(second_client: ClientNamespaceSecondClient): +# assert second_client.get_second() == SecondClientResult(type=SecondClientEnumType.SECOND) diff --git a/packages/http-client-python/generator/test/azure/mock_api_tests/test_resiliency_srv_driven_async.py b/packages/http-client-python/generator/test/azure/mock_api_tests/test_resiliency_srv_driven_async.py index 14f3163273..290e3b68cb 100644 --- a/packages/http-client-python/generator/test/azure/mock_api_tests/test_resiliency_srv_driven_async.py +++ b/packages/http-client-python/generator/test/azure/mock_api_tests/test_resiliency_srv_driven_async.py @@ -96,6 +96,7 @@ async def test_add_optional_param_from_one_optional(): @pytest.mark.asyncio async def test_break_the_glass(): from azure.core.rest import HttpRequest + request = HttpRequest(method="DELETE", url="/add-operation") async with V1Client( endpoint="http://localhost:3000", diff --git a/packages/http-client-python/generator/test/azure/requirements.txt b/packages/http-client-python/generator/test/azure/requirements.txt index 6864376776..cd2fa0481f 100644 --- a/packages/http-client-python/generator/test/azure/requirements.txt +++ b/packages/http-client-python/generator/test/azure/requirements.txt @@ -26,6 +26,7 @@ mypy==1.14.1 -e ./generated/azure-example-basic -e ./generated/azure-resource-manager-common-properties -e ./generated/azure-resource-manager-resources +# -e ./generated/client-namespace -e ./generated/azure-payload-pageable -e ./generated/client-naming -e ./generated/client-structure-default diff --git a/packages/http-client-python/package-lock.json b/packages/http-client-python/package-lock.json index 46fe0e3ff2..12f2f4aca6 100644 --- a/packages/http-client-python/package-lock.json +++ b/packages/http-client-python/package-lock.json @@ -1,12 +1,12 @@ { "name": "@typespec/http-client-python", - "version": "0.5.1", + "version": "0.6.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@typespec/http-client-python", - "version": "0.5.1", + "version": "0.6.0", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/packages/http-client-python/package.json b/packages/http-client-python/package.json index 934fcae1bf..16ea813f14 100644 --- a/packages/http-client-python/package.json +++ b/packages/http-client-python/package.json @@ -1,6 +1,6 @@ { "name": "@typespec/http-client-python", - "version": "0.5.1", + "version": "0.6.0", "author": "Microsoft Corporation", "description": "TypeSpec emitter for Python SDKs", "homepage": "https://typespec.io",