Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
0853790
use .multipartOptions
msyyc Jul 31, 2024
c1cb5fc
test for advanced multipart
msyyc Jul 31, 2024
b58923f
update typing for file type
msyyc Jul 31, 2024
4059961
update docstring type
msyyc Jul 31, 2024
8e152a7
filter unused model
msyyc Aug 1, 2024
99df939
fix usage
msyyc Aug 1, 2024
9d03371
review
msyyc Aug 1, 2024
11748a1
inv
msyyc Aug 1, 2024
523c1b1
Merge branch 'main' of https://github.com/Azure/autorest.python into …
msyyc Aug 1, 2024
dd200c0
Update types.ts
msyyc Aug 5, 2024
f08497f
merge
msyyc Aug 13, 2024
06c19ee
Merge branch 'advanced-multipart-implementation' of https://github.co…
msyyc Aug 13, 2024
e712e35
test
msyyc Aug 13, 2024
6c94b84
changelog
msyyc Aug 13, 2024
a8d49c8
lint
msyyc Aug 13, 2024
909288b
ci
msyyc Aug 13, 2024
e5c4aa9
regenerate
msyyc Aug 13, 2024
c43878f
fix ci
msyyc Aug 13, 2024
c6b4336
Merge branch 'main' into advanced-multipart-implementation
msyyc Aug 13, 2024
e0b32a0
Merge branch 'main' into advanced-multipart-implementation
msyyc Aug 14, 2024
dfad160
Merge branch 'main' into advanced-multipart-implementation
msyyc Aug 14, 2024
dd3c751
merge
msyyc Aug 15, 2024
f2556e8
pnpm install
msyyc Aug 15, 2024
89caa85
review
msyyc Aug 15, 2024
51dc805
Merge branch 'main' into advanced-multipart-implementation
msyyc Aug 15, 2024
7af96d8
review
msyyc Aug 15, 2024
1d2edfb
Merge branch 'advanced-multipart-implementation' of https://github.co…
msyyc Aug 15, 2024
d81d91a
review
msyyc Aug 15, 2024
d1588ce
merge
msyyc Aug 16, 2024
493e119
review
msyyc Aug 16, 2024
1c94593
review
msyyc Aug 16, 2024
f91635b
regenerate
msyyc Aug 16, 2024
685a788
regenerate
msyyc Aug 16, 2024
b5efa05
Merge branch 'main' into advanced-multipart-implementation
msyyc Aug 19, 2024
11f2653
Update launch.json
msyyc Aug 19, 2024
a2a0754
Merge branch 'main' into advanced-multipart-implementation
msyyc Aug 19, 2024
a4e3c49
Merge branch 'main' into advanced-multipart-implementation
msyyc Aug 20, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
changeKind: feature
packages:
- "@azure-tools/typespec-python"
---

Support advanced multipart for `@multipartBody`
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
"homepage": "https://github.com/Azure/autorest.python#readme",
"devDependencies": {
"@actions/github": "6.0.0",
"@azure-tools/cadl-ranch": "~0.14.1",
"@azure-tools/cadl-ranch": "~0.14.2",
"@chronus/chronus": "^0.10.2",
"@chronus/github": "^0.3.2",
"@typespec/prettier-plugin-typespec": "~0.58.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
UnixTimeType,
SdkCoreType,
DecimalType,
MultiPartFileType,
)
from .enum_type import EnumType, EnumValue
from .base import BaseType
Expand Down Expand Up @@ -149,6 +150,7 @@
"unixtime": UnixTimeType,
"credential": StringType,
"sdkcore": SdkCoreType,
"multipartfile": MultiPartFileType,
}
_LOGGER = logging.getLogger(__name__)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -624,3 +624,29 @@ def instance_check_template(self) -> str:
@property
def serialization_type(self) -> str:
return self.name


class MultiPartFileType(PrimitiveType):
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the new type, we don't need special logic in docstring/typing annotation for multipart properties anymore. And it is much easier to evolve in the future.

def __init__(self, yaml_data: Dict[str, Any], code_model: "CodeModel") -> None:
super().__init__(yaml_data=yaml_data, code_model=code_model)
self.name = "FileType"

def type_annotation(self, **kwargs: Any) -> str:
return self.name

def docstring_type(self, **kwargs: Any) -> str:
return f"~{self.code_model.namespace}._vendor.{self.name}"

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)
return file_import

@property
def default_template_representation_declaration(self) -> str:
return '"filetype"' if self.code_model.for_test else "filetype"

@property
def instance_check_template(self) -> str:
return f"isinstance({{}}, {self.name})"
Original file line number Diff line number Diff line change
Expand Up @@ -97,12 +97,9 @@ def is_base_discriminator(self) -> bool:
return self.is_discriminator and self.is_polymorphic and cast(ConstantType, self.type).value is None

def type_annotation(self, *, is_operation_file: bool = False) -> str:
types_type_annotation = self.type.type_annotation(is_operation_file=is_operation_file)
if self.is_multipart_file_input:
# we only support FileType or list of FileType
types_type_annotation = types_type_annotation.replace("bytes", "FileType")
if self.is_base_discriminator:
return "str"
types_type_annotation = self.type.type_annotation(is_operation_file=is_operation_file)
if self.optional and self.client_default_value is None:
return f"Optional[{types_type_annotation}]"
return types_type_annotation
Expand All @@ -115,9 +112,6 @@ def get_json_template_representation(
*,
client_default_value_declaration: Optional[str] = None,
) -> Any:
if self.is_multipart_file_input:
file_type_str = '"filetype"' if self.code_model.for_test else "filetype"
return f"[{file_type_str}]" if self.type.type == "list" else file_type_str
if self.client_default_value:
client_default_value_declaration = self.get_declaration(self.client_default_value)
# make sure there is no \n otherwise the json template will be invalid
Expand Down Expand Up @@ -156,8 +150,6 @@ def imports(self, **kwargs) -> FileImport:
"rest_discriminator" if self.is_discriminator else "rest_field",
ImportType.LOCAL,
)
if self.is_multipart_file_input:
file_import.add_submodule_import(".._vendor", "FileType", ImportType.LOCAL)
return file_import

@classmethod
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,6 @@ def serialize_vendor_file(self, clients: List[Client]) -> str:
file_import.add_submodule_import("typing", "Union", ImportType.STDLIB)
file_import.add_submodule_import("typing", "Optional", ImportType.STDLIB)
file_import.add_submodule_import("typing", "Mapping", ImportType.STDLIB)
file_import.add_submodule_import("typing", "Sequence", ImportType.STDLIB)
file_import.add_submodule_import("typing", "Dict", ImportType.STDLIB)
file_import.add_submodule_import("typing", "Any", ImportType.STDLIB)
file_import.add_submodule_import("typing", "List", ImportType.STDLIB)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,6 @@ FileType = Union[
Tuple[Optional[str], FileContent, Optional[str]],
]

FilesType = Union[Mapping[str, FileType], Sequence[Tuple[str, FileType]]]

def serialize_multipart_data_entry(data_entry: Any) -> Any:
if isinstance(data_entry, (list, tuple, dict, Model)):
return json.dumps(data_entry, cls=SdkJSONEncoder, exclude_readonly=True)
Expand Down
2 changes: 1 addition & 1 deletion packages/typespec-python/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@
"@azure-tools/typespec-azure-resource-manager": "~0.45.0",
"@azure-tools/typespec-autorest": "~0.45.0",
"@azure-tools/cadl-ranch-expect": "~0.15.1",
"@azure-tools/cadl-ranch-specs": "~0.35.3",
"@azure-tools/cadl-ranch-specs": "~0.35.4",
"@types/js-yaml": "~4.0.5",
"@types/node": "^18.16.3",
"@types/yargs": "17.0.32",
Expand Down
3 changes: 3 additions & 0 deletions packages/typespec-python/scripts/regenerate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,9 @@ async function getSubdirectories(baseDir: string, flags: RegenerateFlags): Promi
// after xml support, remove this check
if (mainTspRelativePath.includes("xml")) return;

// after fix test generation for nested operation group, remove this check
if (mainTspRelativePath.includes("client-operation-group")) return;

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will make another PR to fix test generation for this case

const hasMainTsp = await promises
.access(mainTspPath)
.then(() => true)
Expand Down
21 changes: 12 additions & 9 deletions packages/typespec-python/src/code-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
UsageFlags,
getCrossLanguagePackageId,
} from "@azure-tools/typespec-client-generator-core";
import { KnownTypes, emitEndpointType, getType, simpleTypesMap, typesMap } from "./types.js";
import { KnownTypes, disableGenerationMap, emitEndpointType, getType, simpleTypesMap, typesMap } from "./types.js";
import { emitParamBase, getImplementation, removeUnderscoresFromNamespace } from "./utils.js";
import { emitBasicHttpMethod, emitLroHttpMethod, emitLroPagingHttpMethod, emitPagingHttpMethod } from "./http.js";
import { PythonSdkContext } from "./lib.js";
Expand Down Expand Up @@ -218,25 +218,28 @@ export function emitCodeModel<TServiceOperation extends SdkServiceOperation>(
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) {
Copy link
Copy Markdown
Member Author

@msyyc msyyc Aug 16, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Put loop for models/enums behind emitClients so that we could collect models which will not be generated (e.g. models for multipart file part) and disable them by not calling getType to add them into typesMap

if (model.name === "" || (model.usage & UsageFlags.Spread) > 0) {
continue;
}
getType(sdkContext, model);
if (!disableGenerationMap.has(model)) {
Comment thread
tadelesh marked this conversation as resolved.
getType(sdkContext, model);
}
}
for (const sdkEnum of sdkPackage.enums) {
if (sdkEnum.usage === UsageFlags.ApiVersionEnum) {
continue;
}
getType(sdkContext, sdkEnum);
}
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);
}
}
codeModel["types"] = [...typesMap.values(), ...Object.values(KnownTypes), ...simpleTypesMap.values()];
codeModel["crossLanguagePackageId"] = ignoreDiagnostics(getCrossLanguagePackageId(sdkContext));
return codeModel;
Expand Down
51 changes: 48 additions & 3 deletions packages/typespec-python/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { PythonSdkContext } from "./lib.js";

export const typesMap = new Map<SdkType, Record<string, any>>();
export const simpleTypesMap = new Map<string | null, Record<string, any>>();
export const disableGenerationMap = new Set<SdkType>();

export interface CredentialType {
kind: "Credential";
Expand All @@ -34,6 +35,11 @@ export interface CredentialTypeUnion {
types: CredentialType[];
}

interface MultiPartFileType {
kind: "multipartfile";
type: SdkType;
}

function isEmptyModel(type: SdkType): boolean {
// object, {} will be treated as empty model, user defined empty model will not
return (
Expand All @@ -59,7 +65,7 @@ export function getSimpleTypeResult(result: Record<string, any>): Record<string,

export function getType<TServiceOperation extends SdkServiceOperation>(
context: PythonSdkContext<TServiceOperation>,
type: CredentialType | CredentialTypeUnion | Type | SdkType,
type: CredentialType | CredentialTypeUnion | Type | SdkType | MultiPartFileType,
fromBody = false,
): Record<string, any> {
switch (type.kind) {
Expand Down Expand Up @@ -109,11 +115,29 @@ export function getType<TServiceOperation extends SdkServiceOperation>(
return KnownTypes.any;
case "nullable":
return getType(context, type.type);
case "multipartfile":
return emitMultiPartFile(context, type);
default:
throw Error(`Not supported ${type.kind}`);
}
}

function emitMultiPartFile<TServiceOperation extends SdkServiceOperation>(
context: PythonSdkContext<TServiceOperation>,
type: MultiPartFileType,
): Record<string, any> {
if (type.type.kind === "array") {
return getSimpleTypeResult({
type: "list",
elementType: getType(context, createMultiPartFileType(type.type.valueType)),
});
}
return getSimpleTypeResult({
type: type.kind,
description: type.type.description,
});
}

function emitCredential(credential: SdkCredentialType): Record<string, any> {
let credential_type: Record<string, any> = {};
const scheme = credential.scheme;
Expand Down Expand Up @@ -173,21 +197,42 @@ function visibilityMapping(visibility?: Visibility[]): string[] | undefined {
return result;
}

function createMultiPartFileType(type: SdkType): MultiPartFileType {
return { kind: "multipartfile", type };
}

function addDisableGenerationMap(type: SdkType): void {
if (disableGenerationMap.has(type)) return;

disableGenerationMap.add(type);
if (type.kind === "model" && type.baseModel) {
addDisableGenerationMap(type.baseModel);
} else if (type.kind === "array") {
addDisableGenerationMap(type.valueType);
}
}

function emitProperty<TServiceOperation extends SdkServiceOperation>(
context: PythonSdkContext<TServiceOperation>,
property: SdkBodyModelPropertyType,
): Record<string, any> {
const isMultipartFileInput = property.multipartOptions?.isFilePart;
const sourceType = isMultipartFileInput ? createMultiPartFileType(property.type) : property.type;
if (isMultipartFileInput) {
// Python convert all the type of file part to FileType so clear these models' usage so that they won't be generated
addDisableGenerationMap(property.type);
Comment thread
tadelesh marked this conversation as resolved.
}
return {
clientName: camelToSnakeCase(property.name),
wireName: property.serializedName,
type: getType(context, property.type),
type: getType(context, sourceType),
optional: property.optional,
description: property.description,
addedOn: getAddedOn(context, property),
visibility: visibilityMapping(property.visibility),
isDiscriminator: property.discriminator,
flatten: property.flatten,
isMultipartFileInput: property.isMultipartFileInput,
isMultipartFileInput: isMultipartFileInput,
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,24 @@
"payload.multipart.models.Address": "Payload.MultiPart.Address",
"payload.multipart.models.AnonymousModelRequest": "anonymousModel.Request.anonymous",
"payload.multipart.models.BinaryArrayPartsRequest": "Payload.MultiPart.BinaryArrayPartsRequest",
"payload.multipart.models.ComplexHttpPartsModelRequest": "Payload.MultiPart.ComplexHttpPartsModelRequest",
"payload.multipart.models.ComplexPartsRequest": "Payload.MultiPart.ComplexPartsRequest",
"payload.multipart.models.JsonArrayPartsRequest": "Payload.MultiPart.JsonArrayPartsRequest",
"payload.multipart.models.FileWithHttpPartOptionalContentTypeRequest": "Payload.MultiPart.FileWithHttpPartOptionalContentTypeRequest",
"payload.multipart.models.FileWithHttpPartRequiredContentTypeRequest": "Payload.MultiPart.FileWithHttpPartRequiredContentTypeRequest",
"payload.multipart.models.FileWithHttpPartSpecificContentTypeRequest": "Payload.MultiPart.FileWithHttpPartSpecificContentTypeRequest",
"payload.multipart.models.JsonPartRequest": "Payload.MultiPart.JsonPartRequest",
"payload.multipart.models.MultiBinaryPartsRequest": "Payload.MultiPart.MultiBinaryPartsRequest",
"payload.multipart.models.MultiPartRequest": "Payload.MultiPart.MultiPartRequest",
"payload.multipart.MultiPartClient.form_data.basic": "Payload.MultiPart.FormData.basic",
"payload.multipart.MultiPartClient.form_data.complex": "Payload.MultiPart.FormData.complex",
"payload.multipart.MultiPartClient.form_data.json_part": "Payload.MultiPart.FormData.jsonPart",
"payload.multipart.MultiPartClient.form_data.binary_array_parts": "Payload.MultiPart.FormData.binaryArrayParts",
"payload.multipart.MultiPartClient.form_data.json_array_parts": "Payload.MultiPart.FormData.jsonArrayParts",
"payload.multipart.MultiPartClient.form_data.multi_binary_parts": "Payload.MultiPart.FormData.multiBinaryParts",
"payload.multipart.MultiPartClient.form_data.check_file_name_and_content_type": "Payload.MultiPart.FormData.checkFileNameAndContentType",
"payload.multipart.MultiPartClient.form_data.anonymous_model": "Payload.MultiPart.FormData.anonymousModel"
"payload.multipart.MultiPartClient.form_data.anonymous_model": "Payload.MultiPart.FormData.anonymousModel",
"payload.multipart.MultiPartClient.form_data.file_with_http_part_specific_content_type": "Payload.MultiPart.FormData.fileWithHttpPartSpecificContentType",
"payload.multipart.MultiPartClient.form_data.file_with_http_part_required_content_type": "Payload.MultiPart.FormData.fileWithHttpPartRequiredContentType",
"payload.multipart.MultiPartClient.form_data.file_with_http_part_optional_content_type": "Payload.MultiPart.FormData.fileWithHttpPartOptionalContentType",
"payload.multipart.MultiPartClient.form_data.complex_with_http_part": "Payload.MultiPart.FormData.complexWithHttpPart"
}
}
Loading