diff --git a/src/Kiota.Builder/Refiners/PythonRefiner.cs b/src/Kiota.Builder/Refiners/PythonRefiner.cs index db824c1e12..2d85a39f1a 100644 --- a/src/Kiota.Builder/Refiners/PythonRefiner.cs +++ b/src/Kiota.Builder/Refiners/PythonRefiner.cs @@ -70,6 +70,7 @@ public override Task RefineAsync(CodeNamespace generatedCode, CancellationToken CorrectCoreType(generatedCode, CorrectMethodType, CorrectPropertyType, CorrectImplements); cancellationToken.ThrowIfCancellationRequested(); CorrectCoreTypesForBackingStore(generatedCode, "field(default_factory=BackingStoreFactorySingleton(backing_store_factory=None).backing_store_factory.create_backing_store, repr=False)"); + SnakeCaseNamespaceNames(generatedCode); ShortenOversizedNamespaceSegments(generatedCode); AddPropertiesAndMethodTypesImports(generatedCode, true, true, true, codeTypeFilter); AddParsableImplementsForModelClasses(generatedCode, "Parsable"); @@ -158,6 +159,26 @@ public override Task RefineAsync(CodeNamespace generatedCode, CancellationToken }, cancellationToken); } + private static void SnakeCaseNamespaceNames(CodeElement currentElement) + { + if (currentElement is CodeNamespace codeNamespace && + !string.IsNullOrEmpty(codeNamespace.Name)) + { + var normalizedName = string.Join('.', + codeNamespace.Name + .Split('.', StringSplitOptions.RemoveEmptyEntries) + .Select(static x => x.ToSnakeCase())); + if (!codeNamespace.Name.Equals(normalizedName, StringComparison.Ordinal)) + { + if (codeNamespace.Parent is CodeNamespace parentNamespace) + parentNamespace.RenameChildElement(codeNamespace.Name, normalizedName); + else + codeNamespace.Name = normalizedName; + } + } + CrawlTree(currentElement, SnakeCaseNamespaceNames); + } + private const string MultipartBodyClassName = "MultipartBody"; private const string AbstractionsPackageName = "kiota_abstractions"; private const string SerializationModuleName = $"{AbstractionsPackageName}.serialization"; diff --git a/src/Kiota.Builder/Writers/Python/PythonWriter.cs b/src/Kiota.Builder/Writers/Python/PythonWriter.cs index 97138f7a77..d0a5310fbb 100644 --- a/src/Kiota.Builder/Writers/Python/PythonWriter.cs +++ b/src/Kiota.Builder/Writers/Python/PythonWriter.cs @@ -1,4 +1,7 @@ -using Kiota.Builder.PathSegmenters; +using System; +using System.Linq; +using Kiota.Builder.Extensions; +using Kiota.Builder.PathSegmenters; namespace Kiota.Builder.Writers.Python; @@ -6,13 +9,18 @@ public class PythonWriter : LanguageWriter { public PythonWriter(string rootPath, string clientNamespaceName, bool usesBackingStore = false) { - PathSegmenter = new PythonPathSegmenter(rootPath, clientNamespaceName); + ArgumentNullException.ThrowIfNull(clientNamespaceName); + var normalizedClientNamespaceName = string.Join('.', + clientNamespaceName + .Split('.', StringSplitOptions.RemoveEmptyEntries) + .Select(static x => x.ToSnakeCase())); + PathSegmenter = new PythonPathSegmenter(rootPath, normalizedClientNamespaceName); var conventionService = new PythonConventionService(); - AddOrReplaceCodeElementWriter(new CodeClassDeclarationWriter(conventionService, clientNamespaceName)); + AddOrReplaceCodeElementWriter(new CodeClassDeclarationWriter(conventionService, normalizedClientNamespaceName)); AddOrReplaceCodeElementWriter(new CodeBlockEndWriter()); AddOrReplaceCodeElementWriter(new CodeEnumWriter(conventionService)); - AddOrReplaceCodeElementWriter(new CodeMethodWriter(conventionService, clientNamespaceName, usesBackingStore)); - AddOrReplaceCodeElementWriter(new CodePropertyWriter(conventionService, clientNamespaceName)); + AddOrReplaceCodeElementWriter(new CodeMethodWriter(conventionService, normalizedClientNamespaceName, usesBackingStore)); + AddOrReplaceCodeElementWriter(new CodePropertyWriter(conventionService, normalizedClientNamespaceName)); AddOrReplaceCodeElementWriter(new CodeTypeWriter(conventionService)); AddOrReplaceCodeElementWriter(new CodeNameSpaceWriter(conventionService)); } diff --git a/tests/Kiota.Builder.Tests/Export/PublicAPIExportServiceTests.cs b/tests/Kiota.Builder.Tests/Export/PublicAPIExportServiceTests.cs index 6a7f1a314f..47cc2c9a20 100644 --- a/tests/Kiota.Builder.Tests/Export/PublicAPIExportServiceTests.cs +++ b/tests/Kiota.Builder.Tests/Export/PublicAPIExportServiceTests.cs @@ -180,16 +180,16 @@ private static void ValidateExportGo(HashSet exportContents) private static void ValidateExportPython(HashSet exportContents) { Assert.NotEmpty(exportContents); - Assert.Contains("exportNamespace.Graph-->BaseRequestBuilder", exportContents); // captures class inheritance - Assert.Contains("exportNamespace.models.microsoft.graph.User~~>AdditionalDataHolder; Parsable", exportContents);// captures implemented interfaces - Assert.Contains("exportNamespace.models.microsoft.graph.User::|public|id():str", exportContents);// captures property getter location,type and access inheritance - Assert.Contains("exportNamespace.models.microsoft.graph.User::|public|id(value:str):None", exportContents);// captures property setter location,type and access inheritance - Assert.Contains("exportNamespace.me.MeRequestBuilder::|public|constructor(path_parameters:Union[str, dict[str, Any]]; request_adapter:RequestAdapter):None", exportContents); // captures constructors, their parameters(name and types), return and access - Assert.Contains("exportNamespace.me.get.GetRequestBuilder::|public|to_get_request_information(request_configuration?:RequestConfiguration[QueryParameters]):RequestInformation", exportContents);// captures methods, their parameters(name and types), return and access - Assert.Contains("exportNamespace.models.microsoft.graph.User::|static|public|create_from_discriminator_value(parse_node:ParseNode):User", exportContents);// captures static methods too :) - Assert.Contains("exportNamespace.models.microsoft.graph.Importance::0000-Low", exportContents);// captures enum members - Assert.Contains("exportNamespace.models.microsoft.graph.User::|public|other_names():list[str]", exportContents);// captures collection info in language specific format - Assert.Contains("exportNamespace.models.microsoft.graph.User::|public|other_names(value:list[str]):None", exportContents);// captures collection info in language specific format + Assert.Contains("export_namespace.Graph-->BaseRequestBuilder", exportContents); // captures class inheritance + Assert.Contains("export_namespace.models.microsoft.graph.User~~>AdditionalDataHolder; Parsable", exportContents);// captures implemented interfaces + Assert.Contains("export_namespace.models.microsoft.graph.User::|public|id():str", exportContents);// captures property getter location,type and access inheritance + Assert.Contains("export_namespace.models.microsoft.graph.User::|public|id(value:str):None", exportContents);// captures property setter location,type and access inheritance + Assert.Contains("export_namespace.me.MeRequestBuilder::|public|constructor(path_parameters:Union[str, dict[str, Any]]; request_adapter:RequestAdapter):None", exportContents); // captures constructors, their parameters(name and types), return and access + Assert.Contains("export_namespace.me.get.GetRequestBuilder::|public|to_get_request_information(request_configuration?:RequestConfiguration[QueryParameters]):RequestInformation", exportContents);// captures methods, their parameters(name and types), return and access + Assert.Contains("export_namespace.models.microsoft.graph.User::|static|public|create_from_discriminator_value(parse_node:ParseNode):User", exportContents);// captures static methods too :) + Assert.Contains("export_namespace.models.microsoft.graph.Importance::0000-Low", exportContents);// captures enum members + Assert.Contains("export_namespace.models.microsoft.graph.User::|public|other_names():list[str]", exportContents);// captures collection info in language specific format + Assert.Contains("export_namespace.models.microsoft.graph.User::|public|other_names(value:list[str]):None", exportContents);// captures collection info in language specific format } private static void ValidateExportTypeScript(HashSet exportContents) diff --git a/tests/Kiota.Builder.Tests/KiotaBuilderTests.cs b/tests/Kiota.Builder.Tests/KiotaBuilderTests.cs index 16a52c8fa8..5ecaf49bb7 100644 --- a/tests/Kiota.Builder.Tests/KiotaBuilderTests.cs +++ b/tests/Kiota.Builder.Tests/KiotaBuilderTests.cs @@ -848,7 +848,8 @@ public async Task DoesNotAddSuperflousFieldsToModelsAsync(GenerationLanguage lan var node = builder.CreateUriSpace(document); var codeModel = builder.CreateSourceModel(node); await builder.ApplyLanguageRefinementAsync(generationConfiguration, codeModel, TestContext.Current.CancellationToken); - var requestBuilderNamespace = codeModel.FindNamespaceByName("ApiSdk.api.all"); + var requestBuilderNamespace = codeModel.FindNamespaceByName( + language == GenerationLanguage.Python ? "api_sdk.api.all" : "ApiSdk.api.all"); Assert.NotNull(requestBuilderNamespace); if (language == GenerationLanguage.TypeScript || language == GenerationLanguage.Go) {// these languages use CodeFiles diff --git a/tests/Kiota.Builder.Tests/Refiners/PythonLanguageRefinerTests.cs b/tests/Kiota.Builder.Tests/Refiners/PythonLanguageRefinerTests.cs index 4f4e158147..da18a3f7ed 100644 --- a/tests/Kiota.Builder.Tests/Refiners/PythonLanguageRefinerTests.cs +++ b/tests/Kiota.Builder.Tests/Refiners/PythonLanguageRefinerTests.cs @@ -40,6 +40,27 @@ public async Task AddsDefaultImportsAsync() await ILanguageRefiner.RefineAsync(new GenerationConfiguration { Language = GenerationLanguage.Python }, graphNS, cancellationToken: TestContext.Current.CancellationToken); Assert.Contains("annotations", declaration.Usings.Select(x => x.Name)); } + + [Fact] + public async Task ConvertsNamespaceSegmentsToSnakeCaseAsync() + { + var clientNamespace = root.AddNamespace("graphSdk"); + var childNamespace = root.AddNamespace("graphSdk.someNamespace"); + + await ILanguageRefiner.RefineAsync( + new GenerationConfiguration + { + Language = GenerationLanguage.Python, + ClientNamespaceName = "graphSdk", + }, + root, + cancellationToken: TestContext.Current.CancellationToken); + + Assert.Equal("graph_sdk", clientNamespace.Name); + Assert.Equal("graph_sdk.some_namespace", childNamespace.Name); + Assert.Same(childNamespace, root.FindNamespaceByName("graph_sdk.some_namespace")); + } + [Fact] public async Task AddsQueryParameterMapperMethodAsync() { diff --git a/tests/Kiota.Builder.Tests/Writers/Python/PythonWriterTests.cs b/tests/Kiota.Builder.Tests/Writers/Python/PythonWriterTests.cs index b375c3c26c..1b5aaab704 100644 --- a/tests/Kiota.Builder.Tests/Writers/Python/PythonWriterTests.cs +++ b/tests/Kiota.Builder.Tests/Writers/Python/PythonWriterTests.cs @@ -1,5 +1,8 @@ using System; +using System.IO; +using System.Linq; +using Kiota.Builder.CodeDOM; using Kiota.Builder.Writers.Python; using Xunit; @@ -17,4 +20,29 @@ public void Instantiates() Assert.Throws(() => new PythonWriter(null, "graph")); Assert.Throws(() => new PythonWriter("./", null)); } + + [Fact] + public void NormalizesClientNamespacePrefix() + { + var outputPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + try + { + var writer = new PythonWriter(outputPath, "graphSdk"); + var root = CodeNamespace.InitRootNamespace(); + var childNamespace = root.AddNamespace("graph_sdk.some_namespace"); + var model = childNamespace.AddClass(new CodeClass { Name = "TestModel" }); + + var path = writer.PathSegmenter!.GetPath(childNamespace, model.First(), false); + + Assert.EndsWith( + Path.Combine("some_namespace", "test_model.py"), + path, + StringComparison.Ordinal); + } + finally + { + if (Directory.Exists(outputPath)) + Directory.Delete(outputPath, true); + } + } }