From a9a0dd191c8f316b76edd36da552a18868841686 Mon Sep 17 00:00:00 2001 From: Samwel K <40166690+samwelkanda@users.noreply.github.com> Date: Wed, 22 Mar 2023 16:50:03 +0300 Subject: [PATCH] Task/python imports refactor (#2379) * Add python relative import manager * Add a codeusingwriter for python * Add option to include parent namespace when adding discriminator mapping usings * Call method to add discriminator mapping usings to parent classes * Add type checking import * Remove lazy import using * Update class declaration writer * Update property writer to add local imports for request builders with no parameters * Add logic to write discriminator method body * Write local imports in indexers and request builders with parameters * Add deferred imports for request executors and deserializers * Use snake case to match variable name passed to request builder * Update python writer with missing parameters * Add incluparentnamespace parameter when crawling tree * Add code class declaration writer tests * Add property writer tests * Add codemethodwriter tests * Fix formatting issues * Update method writer tests * Add codeusing writer tests * Fix formatting * Add changelog entry * Add type annotation for discriminator fields variable * Update test * Fix fields type hint * Bump test coverage in classdeclarationwriter * Add python code element order comparer * Add code renderer for python * Write methods before properties * Add python element comparer tests * Fix formatting * Update CHANGELOG * Remove suppressions for passing it tests * Apply suggestions from code review --------- Co-authored-by: Vincent Biret --- CHANGELOG.md | 4 + it/config.json | 12 - src/Kiota.Builder/CodeElementOrderComparer.cs | 6 +- .../CodeElementOrderComparerPython.cs | 34 +++ .../CodeRenderers/CodeRenderer.cs | 5 +- .../CodeRenderers/PythonCodeRenderer.cs | 9 + .../Refiners/CommonLanguageRefiner.cs | 20 +- src/Kiota.Builder/Refiners/PythonRefiner.cs | 9 +- .../Python/CodeClassDeclarationWriter.cs | 87 ++---- .../Writers/Python/CodeMethodWriter.cs | 37 ++- .../Writers/Python/CodePropertyWriter.cs | 7 +- .../Writers/Python/CodeUsingWriter.cs | 135 ++++++++++ .../Writers/Python/PythonConventionService.cs | 2 +- .../Python/PythonRelativeImportManager.cs | 55 ++++ .../Writers/Python/PythonWriter.cs | 6 +- .../CodeDOM/CodeElementComparerPythonTests.cs | 89 +++++++ .../Python/CodeClassDeclarationWriterTests.cs | 37 ++- .../Writers/Python/CodeMethodWriterTests.cs | 251 +++++++++++++++++- .../Writers/Python/CodePropertyWriterTests.cs | 23 +- .../Writers/Python/CodeUsingWriterTests.cs | 70 +++++ 20 files changed, 792 insertions(+), 106 deletions(-) create mode 100644 src/Kiota.Builder/CodeElementOrderComparerPython.cs create mode 100644 src/Kiota.Builder/CodeRenderers/PythonCodeRenderer.cs create mode 100644 src/Kiota.Builder/Writers/Python/CodeUsingWriter.cs create mode 100644 src/Kiota.Builder/Writers/Python/PythonRelativeImportManager.cs create mode 100644 tests/Kiota.Builder.Tests/CodeDOM/CodeElementComparerPythonTests.cs create mode 100644 tests/Kiota.Builder.Tests/Writers/Python/CodeUsingWriterTests.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 45e6d5ed8a..f3902ed5e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fixed a bug where lookup of reference ids failed for AllOf more than one level up. - Fixed a bug where a CLI client would not set the content types for requests. (Shell) +- Fixed linting errors by re-ordering methods and properties in Python. +- Changed python import mechanism to facilitate code completion. [#2380](https://github.com/microsoft/kiota/issues/2380) +- Fixed a bug where discriminator methods were missing possible types in Python [#2381](https://github.com/microsoft/kiota/issues/2381) - Fixed a bug where boolean or number enums would be mapped to enums instead of primitive types. [#2367](https://github.com/microsoft/kiota/issues/2367) - Fixed a bug where CSharp inherited constructor name was incorrect. [#2351](https://github.com/microsoft/kiota/issues/2351) - Fixed a bug where java refiner would emit method's parameters types without normalizing the name. @@ -25,6 +28,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fixed a bug where go refiner would emit incorrect code when inlining error parents - Fixed a bug in PHP where the base URL path parameter key didn't match the URI template. + ## [1.0.1] - 2023-03-11 - Fixed a bug where double would not be mapped properly. diff --git a/it/config.json b/it/config.json index 954b81e5ec..7e4916f3f3 100644 --- a/it/config.json +++ b/it/config.json @@ -74,19 +74,11 @@ { "Language": "php", "Rationale": "https://github.com/microsoft/kiota/issues/2351" - }, - { - "Language": "python", - "Rationale": "https://github.com/microsoft/kiota/issues/2351" } ] }, "./tests/Kiota.Builder.IntegrationTests/NoUnderscoresInModel.yaml": { "Suppressions": [ - { - "Language": "python", - "Rationale": "https://github.com/microsoft/kiota/issues/2361" - }, { "Language": "ruby", "Rationale": "https://github.com/microsoft/kiota/issues/2374" @@ -124,10 +116,6 @@ { "Language": "php", "Rationale": "https://github.com/microsoft/kiota/issues/2378" - }, - { - "Language": "python", - "Rationale": "https://github.com/microsoft/kiota/issues/2381" } ] }, diff --git a/src/Kiota.Builder/CodeElementOrderComparer.cs b/src/Kiota.Builder/CodeElementOrderComparer.cs index 619aecc450..d2d64d40d6 100644 --- a/src/Kiota.Builder/CodeElementOrderComparer.cs +++ b/src/Kiota.Builder/CodeElementOrderComparer.cs @@ -36,8 +36,10 @@ protected virtual int GetTypeFactor(CodeElement element) _ => 0, }; } - private static readonly int methodKindWeight = 10; - protected static int GetMethodKindFactor(CodeElement element) + + protected virtual int methodKindWeight { get; } = 10; + + protected virtual int GetMethodKindFactor(CodeElement element) { if (element is CodeMethod method) return method.Kind switch diff --git a/src/Kiota.Builder/CodeElementOrderComparerPython.cs b/src/Kiota.Builder/CodeElementOrderComparerPython.cs new file mode 100644 index 0000000000..a15461e36e --- /dev/null +++ b/src/Kiota.Builder/CodeElementOrderComparerPython.cs @@ -0,0 +1,34 @@ +using Kiota.Builder.CodeDOM; + +namespace Kiota.Builder; +public class CodeElementOrderComparerPython : CodeElementOrderComparer +{ + protected override int GetTypeFactor(CodeElement element) + { + return element switch + { + CodeUsing => 1, + ClassDeclaration => 2, + InterfaceDeclaration => 3, + CodeMethod => 4, + CodeIndexer => 5, + CodeProperty => 6, + CodeClass => 7, + BlockEnd => 8, + _ => 0, + }; + } + protected override int methodKindWeight { get; } = 200; + protected override int GetMethodKindFactor(CodeElement element) + { + if (element is CodeMethod method) + return method.Kind switch + { + CodeMethodKind.ClientConstructor => 1, + CodeMethodKind.Constructor => 0, + CodeMethodKind.RawUrlConstructor => 3, + _ => 2, + }; + return 0; + } +} diff --git a/src/Kiota.Builder/CodeRenderers/CodeRenderer.cs b/src/Kiota.Builder/CodeRenderers/CodeRenderer.cs index a60d5a9ba9..fd20967c94 100644 --- a/src/Kiota.Builder/CodeRenderers/CodeRenderer.cs +++ b/src/Kiota.Builder/CodeRenderers/CodeRenderer.cs @@ -15,11 +15,11 @@ namespace Kiota.Builder.CodeRenderers; /// public class CodeRenderer { - public CodeRenderer(GenerationConfiguration configuration) + public CodeRenderer(GenerationConfiguration configuration, CodeElementOrderComparer? elementComparer = null) { ArgumentNullException.ThrowIfNull(configuration); _configuration = configuration; - _rendererElementComparer = configuration.ShouldRenderMethodsOutsideOfClasses ? new CodeElementOrderComparerWithExternalMethods() : new CodeElementOrderComparer(); + _rendererElementComparer = elementComparer ?? (configuration.ShouldRenderMethodsOutsideOfClasses ? new CodeElementOrderComparerWithExternalMethods() : new CodeElementOrderComparer()); } public async Task RenderCodeNamespaceToSingleFileAsync(LanguageWriter writer, CodeElement codeElement, string outputFile, CancellationToken cancellationToken) { @@ -91,6 +91,7 @@ public static CodeRenderer GetCodeRender(GenerationConfiguration config) => config.Language switch { GenerationLanguage.TypeScript => new TypeScriptCodeRenderer(config), + GenerationLanguage.Python => new PythonCodeRenderer(config), _ => new CodeRenderer(config), }; diff --git a/src/Kiota.Builder/CodeRenderers/PythonCodeRenderer.cs b/src/Kiota.Builder/CodeRenderers/PythonCodeRenderer.cs new file mode 100644 index 0000000000..c207545a3c --- /dev/null +++ b/src/Kiota.Builder/CodeRenderers/PythonCodeRenderer.cs @@ -0,0 +1,9 @@ +using System.Linq; +using Kiota.Builder.CodeDOM; +using Kiota.Builder.Configuration; + +namespace Kiota.Builder.CodeRenderers; +public class PythonCodeRenderer : CodeRenderer +{ + public PythonCodeRenderer(GenerationConfiguration configuration) : base(configuration, new CodeElementOrderComparerPython()) { } +} diff --git a/src/Kiota.Builder/Refiners/CommonLanguageRefiner.cs b/src/Kiota.Builder/Refiners/CommonLanguageRefiner.cs index 866507e2a5..9d35c3f58c 100644 --- a/src/Kiota.Builder/Refiners/CommonLanguageRefiner.cs +++ b/src/Kiota.Builder/Refiners/CommonLanguageRefiner.cs @@ -994,7 +994,7 @@ protected static void AddParentClassToErrorClasses(CodeElement currentElement, s } CrawlTree(currentElement, x => AddParentClassToErrorClasses(x, parentClassName, parentClassNamespace, addNamespaceToInheritDeclaration)); } - protected static void AddDiscriminatorMappingsUsingsToParentClasses(CodeElement currentElement, string parseNodeInterfaceName, bool addFactoryMethodImport = false, bool addUsings = true) + protected static void AddDiscriminatorMappingsUsingsToParentClasses(CodeElement currentElement, string parseNodeInterfaceName, bool addFactoryMethodImport = false, bool addUsings = true, bool includeParentNamespace = false) { if (currentElement is CodeMethod currentMethod && currentMethod.Parent is CodeClass parentClass && @@ -1004,7 +1004,21 @@ currentMethod.Parent is CodeClass parentClass && (parentClass.DiscriminatorInformation?.HasBasicDiscriminatorInformation ?? false) && parentClass.GetImmediateParentOfType() is CodeNamespace parentClassNamespace) { - if (addUsings) + if (addUsings && includeParentNamespace) + declaration.AddUsings(parentClass.DiscriminatorInformation.DiscriminatorMappings + .Select(static x => x.Value) + .OfType() + .Where(static x => x.TypeDefinition != null) + .Select(x => new CodeUsing + { + Name = x.TypeDefinition!.GetImmediateParentOfType().Name, + Declaration = new CodeType + { + Name = x.TypeDefinition.Name, + TypeDefinition = x.TypeDefinition, + }, + }).ToArray()); + else if (addUsings && !includeParentNamespace) declaration.AddUsings(parentClass.DiscriminatorInformation.DiscriminatorMappings .Select(static x => x.Value) .OfType() @@ -1039,7 +1053,7 @@ type.TypeDefinition is CodeClass modelClass && }); } } - CrawlTree(currentElement, x => AddDiscriminatorMappingsUsingsToParentClasses(x, parseNodeInterfaceName, addFactoryMethodImport, addUsings)); + CrawlTree(currentElement, x => AddDiscriminatorMappingsUsingsToParentClasses(x, parseNodeInterfaceName, addFactoryMethodImport, addUsings, includeParentNamespace)); } protected static void ReplaceLocalMethodsByGlobalFunctions(CodeElement currentElement, Func nameUpdateCallback, Func? usingsCallback, params CodeMethodKind[] kindsToReplace) { diff --git a/src/Kiota.Builder/Refiners/PythonRefiner.cs b/src/Kiota.Builder/Refiners/PythonRefiner.cs index e7759544db..fea1ba72de 100644 --- a/src/Kiota.Builder/Refiners/PythonRefiner.cs +++ b/src/Kiota.Builder/Refiners/PythonRefiner.cs @@ -90,6 +90,12 @@ public override Task Refine(CodeNamespace generatedCode, CancellationToken cance AddQueryParameterMapperMethod( generatedCode ); + AddDiscriminatorMappingsUsingsToParentClasses( + generatedCode, + "ParseNode", + addUsings: true, + includeParentNamespace: true + ); RemoveHandlerFromRequestBuilder(generatedCode); }, cancellationToken); } @@ -97,8 +103,7 @@ public override Task Refine(CodeNamespace generatedCode, CancellationToken cance private const string AbstractionsPackageName = "kiota_abstractions"; private static readonly AdditionalUsingEvaluator[] defaultUsingEvaluators = { new (static x => x is CodeClass, "__future__", "annotations"), - new (static x => x is CodeClass, "typing", "Any, Callable, Dict, List, Optional, Union"), - new (static x => x is CodeClass, $"{AbstractionsPackageName}.utils", "lazy_import"), + new (static x => x is CodeClass, "typing", "Any, Callable, Dict, List, Optional, TYPE_CHECKING, Union"), new (static x => x is CodeProperty prop && prop.IsOfKind(CodePropertyKind.RequestAdapter), $"{AbstractionsPackageName}.request_adapter", "RequestAdapter"), new (static x => x is CodeMethod method && method.IsOfKind(CodeMethodKind.RequestGenerator), diff --git a/src/Kiota.Builder/Writers/Python/CodeClassDeclarationWriter.cs b/src/Kiota.Builder/Writers/Python/CodeClassDeclarationWriter.cs index ee434f11b4..4c0048939e 100644 --- a/src/Kiota.Builder/Writers/Python/CodeClassDeclarationWriter.cs +++ b/src/Kiota.Builder/Writers/Python/CodeClassDeclarationWriter.cs @@ -1,22 +1,33 @@ using System; using System.Linq; - using Kiota.Builder.CodeDOM; using Kiota.Builder.Extensions; namespace Kiota.Builder.Writers.Python; public class CodeClassDeclarationWriter : BaseElementWriter { - - public CodeClassDeclarationWriter(PythonConventionService conventionService) : base(conventionService) + private readonly CodeUsingWriter _codeUsingWriter; + public CodeClassDeclarationWriter(PythonConventionService conventionService, string clientNamespaceName) : base(conventionService) { + _codeUsingWriter = new(clientNamespaceName); } public override void WriteCodeElement(ClassDeclaration codeElement, LanguageWriter writer) { ArgumentNullException.ThrowIfNull(codeElement); ArgumentNullException.ThrowIfNull(writer); - WriteExternalImports(codeElement, writer); // external imports before internal imports - WriteInternalImports(codeElement, writer); + var parentNamespace = codeElement.GetImmediateParentOfType(); + _codeUsingWriter.WriteExternalImports(codeElement, writer); // external imports before internal imports + _codeUsingWriter.WriteConditionalInternalImports(codeElement, writer, parentNamespace); + + if (codeElement.Parent is CodeClass parentClass) + { + if (codeElement.Inherits != null) + _codeUsingWriter.WriteDeferredImport(parentClass, codeElement.Inherits.Name, writer); + foreach (var implement in codeElement.Implements) + _codeUsingWriter.WriteDeferredImport(parentClass, implement.Name, writer); + + } + var abcClass = !codeElement.Implements.Any() ? string.Empty : $"{codeElement.Implements.Select(static x => x.Name.ToFirstCharacterUpperCase()).Aggregate((x, y) => x + ", " + y)}"; var derivation = codeElement.Inherits is CodeType inheritType && @@ -24,72 +35,16 @@ public override void WriteCodeElement(ClassDeclaration codeElement, LanguageWrit !string.IsNullOrEmpty(inheritSymbol) ? inheritSymbol : abcClass; + + + if (codeElement.Parent?.Parent is CodeClass) { writer.WriteLine("@dataclass"); } writer.WriteLine($"class {codeElement.Name.ToFirstCharacterUpperCase()}({derivation}):"); writer.IncreaseIndent(); - if (codeElement.Parent is CodeClass parentClass) - conventions.WriteShortDescription(parentClass.Documentation.Description, writer); - } - - private static void WriteExternalImports(ClassDeclaration codeElement, LanguageWriter writer) - { - var externalImportSymbolsAndPaths = codeElement.Usings - .Where(static x => x.IsExternal) - .Select(x => (x.Name, string.Empty, x.Declaration?.Name)) - .GroupBy(x => x.Item3) - .OrderBy(x => x.Key); - if (externalImportSymbolsAndPaths.Any()) - { - foreach (var codeUsing in externalImportSymbolsAndPaths) - if (!string.IsNullOrWhiteSpace(codeUsing.Key)) - { - if (codeUsing.Key == "-") - writer.WriteLine($"import {codeUsing.Select(x => GetAliasedSymbol(x.Item1, x.Item2)).Distinct().OrderBy(x => x).Aggregate((x, y) => x + ", " + y)}"); - else - writer.WriteLine($"from {codeUsing.Key.ToSnakeCase()} import {codeUsing.Select(x => GetAliasedSymbol(x.Item1, x.Item2)).Distinct().OrderBy(x => x).Aggregate((x, y) => x + ", " + y)}"); - } - writer.WriteLine(); - } - } - - private static void WriteInternalImports(ClassDeclaration codeElement, LanguageWriter writer) - { - var internalImportSymbolsAndPaths = codeElement.Usings - .Where(x => !x.IsExternal) - .Select(static x => GetImportPathForUsing(x)) - .GroupBy(x => x.Item3) - .Where(x => !string.IsNullOrEmpty(x.Key)) - .OrderBy(x => x.Key); - if (internalImportSymbolsAndPaths.Any()) - { - foreach (var codeUsing in internalImportSymbolsAndPaths) - foreach (var symbol in codeUsing.Select(static x => GetAliasedSymbol(x.Item1, x.Item2)).Distinct().OrderBy(static x => x)) - writer.WriteLine($"{symbol} = lazy_import('{codeUsing.Key.ToSnakeCase().Replace("._", ".")}.{symbol}')"); - writer.WriteLine(); - } - } - private static string GetAliasedSymbol(string symbol, string alias) - { - return string.IsNullOrEmpty(alias) ? symbol : $"{symbol} as {alias}"; - } - - /// - /// Returns the import path for the given using and import context namespace. - /// - /// The using to import into the current namespace context - /// The import symbol, it's alias if any and the import path - private static (string, string, string) GetImportPathForUsing(CodeUsing codeUsing) - { - var typeDef = codeUsing.Declaration?.TypeDefinition; - if (typeDef == null) - return (codeUsing.Name, codeUsing.Alias, ""); // it's relative to the folder, with no declaration or type definition (default failsafe) - - var importSymbol = typeDef.Name.ToSnakeCase(); - - var importPath = typeDef.GetImmediateParentOfType().Name; - return (importSymbol, codeUsing.Alias, importPath); + if (codeElement.Parent is CodeClass parent) + conventions.WriteShortDescription(parent.Documentation.Description, writer); } } diff --git a/src/Kiota.Builder/Writers/Python/CodeMethodWriter.cs b/src/Kiota.Builder/Writers/Python/CodeMethodWriter.cs index 2ad44548a6..e261121b22 100644 --- a/src/Kiota.Builder/Writers/Python/CodeMethodWriter.cs +++ b/src/Kiota.Builder/Writers/Python/CodeMethodWriter.cs @@ -8,8 +8,10 @@ namespace Kiota.Builder.Writers.Python; public class CodeMethodWriter : BaseElementWriter { - public CodeMethodWriter(PythonConventionService conventionService, bool usesBackingStore) : base(conventionService) + private readonly CodeUsingWriter _codeUsingWriter; + public CodeMethodWriter(PythonConventionService conventionService, string clientNamespaceName, bool usesBackingStore) : base(conventionService) { + _codeUsingWriter = new(clientNamespaceName); _usesBackingStore = usesBackingStore; } private readonly bool _usesBackingStore; @@ -73,6 +75,9 @@ public override void WriteCodeElement(CodeMethod codeElement, LanguageWriter wri case CodeMethodKind.QueryParametersMapper: WriteQueryParametersMapper(codeElement, parentClass, writer); break; + case CodeMethodKind.Factory: + WriteFactoryMethodBody(codeElement, parentClass, writer); + break; case CodeMethodKind.RawUrlConstructor: throw new InvalidOperationException("RawUrlConstructor is not supported in python"); case CodeMethodKind.RequestBuilderBackwardCompatibility: @@ -83,8 +88,32 @@ public override void WriteCodeElement(CodeMethod codeElement, LanguageWriter wri } writer.CloseBlock(string.Empty); } + private const string DiscriminatorMappingVarName = "mapping_value"; + private const string NodeVarName = "mapping_value_node"; + private void WriteFactoryMethodBody(CodeMethod codeElement, CodeClass parentClass, LanguageWriter writer) + { + var parseNodeParameter = codeElement.Parameters.OfKind(CodeParameterKind.ParseNode) ?? throw new InvalidOperationException("Factory method should have a ParseNode parameter"); + var writeDiscriminatorValueRead = parentClass.DiscriminatorInformation.ShouldWriteParseNodeCheck && !parentClass.DiscriminatorInformation.ShouldWriteDiscriminatorForIntersectionType; + if (writeDiscriminatorValueRead) + { + writer.WriteLine($"{NodeVarName} = {parseNodeParameter.Name.ToSnakeCase()}.get_child_node(\"{parentClass.DiscriminatorInformation.DiscriminatorPropertyName}\")"); + writer.StartBlock($"if {NodeVarName}:"); + writer.WriteLine($"{DiscriminatorMappingVarName} = {NodeVarName}.get_str_value()"); + foreach (var mappedType in parentClass.DiscriminatorInformation.DiscriminatorMappings.OrderBy(static x => x.Key)) + { + writer.StartBlock($"if {DiscriminatorMappingVarName} == \"{mappedType.Key}\":"); + var mappedTypeName = mappedType.Value.AllTypes.First().Name; + _codeUsingWriter.WriteDeferredImport(parentClass, mappedTypeName, writer); + writer.WriteLine($"return {mappedTypeName.ToSnakeCase()}.{mappedTypeName.ToFirstCharacterUpperCase()}()"); + writer.DecreaseIndent(); + } + writer.DecreaseIndent(); + } + writer.WriteLine($"return {parentClass.Name.ToFirstCharacterUpperCase()}()"); + } private void WriteIndexerBody(CodeMethod codeElement, CodeClass parentClass, string returnType, LanguageWriter writer) { + _codeUsingWriter.WriteDeferredImport(parentClass, codeElement.ReturnType.Name, writer); if (parentClass.GetPropertyOfKind(CodePropertyKind.PathParameters) is CodeProperty pathParametersProperty && codeElement.OriginalIndexer != null) conventions.AddParametersAssignment(writer, pathParametersProperty.Type, $"self.{pathParametersProperty.Name}", @@ -93,6 +122,7 @@ private void WriteIndexerBody(CodeMethod codeElement, CodeClass parentClass, str } private void WriteRequestBuilderWithParametersBody(CodeMethod codeElement, CodeClass parentClass, string returnType, LanguageWriter writer) { + _codeUsingWriter.WriteDeferredImport(parentClass, codeElement.ReturnType.Name, writer); var codePathParameters = codeElement.Parameters .Where(x => x.IsOfKind(CodeParameterKind.Path)); conventions.AddRequestBuilderBody(parentClass, returnType, writer, pathParameters: codePathParameters); @@ -281,7 +311,8 @@ private static void WriteDefaultMethodBody(CodeMethod codeElement, LanguageWrite } private void WriteDeserializerBody(CodeMethod codeElement, CodeClass parentClass, LanguageWriter writer, bool inherits) { - writer.WriteLine("fields = {"); + _codeUsingWriter.WriteInternalImports(parentClass, writer); + writer.WriteLine("fields: Dict[str, Callable[[Any], None]] = {"); writer.IncreaseIndent(); foreach (var otherProp in parentClass.GetPropertiesOfKind(CodePropertyKind.Custom).Where(static x => !x.ExistsInBaseType)) { @@ -322,6 +353,7 @@ private void WriteRequestExecutorBody(CodeMethod codeElement, RequestParams requ var errorMappingVarName = "None"; if (codeElement.ErrorMappings.Any()) { + _codeUsingWriter.WriteInternalErrorMappingImports(parentClass, writer); errorMappingVarName = "error_mapping"; writer.WriteLine($"{errorMappingVarName}: Dict[str, ParsableFactory] = {{"); writer.IncreaseIndent(); @@ -335,6 +367,7 @@ private void WriteRequestExecutorBody(CodeMethod codeElement, RequestParams requ writer.IncreaseIndent(); writer.WriteLine("raise Exception(\"Http core is null\") "); writer.DecreaseIndent(); + _codeUsingWriter.WriteDeferredImport(parentClass, codeElement.ReturnType.Name, writer); writer.WriteLine($"return await self.request_adapter.{genericTypeForSendMethod}(request_info,{newFactoryParameter} {errorMappingVarName})"); } private string GetReturnTypeWithoutCollectionSymbol(CodeMethod codeElement, string fullTypeName) diff --git a/src/Kiota.Builder/Writers/Python/CodePropertyWriter.cs b/src/Kiota.Builder/Writers/Python/CodePropertyWriter.cs index 36c089e87e..f121223e65 100644 --- a/src/Kiota.Builder/Writers/Python/CodePropertyWriter.cs +++ b/src/Kiota.Builder/Writers/Python/CodePropertyWriter.cs @@ -5,7 +5,11 @@ namespace Kiota.Builder.Writers.Python; public class CodePropertyWriter : BaseElementWriter { - public CodePropertyWriter(PythonConventionService conventionService) : base(conventionService) { } + private readonly CodeUsingWriter _codeUsingWriter; + public CodePropertyWriter(PythonConventionService conventionService, string clientNamespaceName) : base(conventionService) + { + _codeUsingWriter = new(clientNamespaceName); + } public override void WriteCodeElement(CodeProperty codeElement, LanguageWriter writer) { var returnType = conventions.GetTypeString(codeElement.Type, codeElement, true, writer); @@ -21,6 +25,7 @@ public override void WriteCodeElement(CodeProperty codeElement, LanguageWriter w writer.WriteLine($"def {codeElement.Name.ToSnakeCase()}(self) -> {returnType}:"); writer.IncreaseIndent(); conventions.WriteShortDescription(codeElement.Documentation.Description, writer); + _codeUsingWriter.WriteDeferredImport(parentClass, codeElement.Type.Name, writer); conventions.AddRequestBuilderBody(parentClass, returnType, writer); writer.CloseBlock(string.Empty); break; diff --git a/src/Kiota.Builder/Writers/Python/CodeUsingWriter.cs b/src/Kiota.Builder/Writers/Python/CodeUsingWriter.cs new file mode 100644 index 0000000000..650bb75936 --- /dev/null +++ b/src/Kiota.Builder/Writers/Python/CodeUsingWriter.cs @@ -0,0 +1,135 @@ +using System; +using System.Linq; +using Kiota.Builder.CodeDOM; +using Kiota.Builder.Extensions; + + +namespace Kiota.Builder.Writers.Python; +public class CodeUsingWriter +{ + private readonly PythonRelativeImportManager _relativeImportManager; + public CodeUsingWriter(string clientNamespaceName) + { + _relativeImportManager = new PythonRelativeImportManager(clientNamespaceName, '.'); + } + /// + /// Writes external imports for a given code element. + /// + /// The element to write external usings + /// An instance of the language writer + /// void + public void WriteExternalImports(ClassDeclaration codeElement, LanguageWriter writer) + { + var externalImportSymbolsAndPaths = codeElement.Usings + .Where(static x => x.IsExternal) + .Select(static x => (x.Name, string.Empty, x.Declaration?.Name)) + .GroupBy(static x => x.Item3) + .OrderBy(static x => x.Key); + if (externalImportSymbolsAndPaths.Any()) + { + foreach (var codeUsing in externalImportSymbolsAndPaths) + if (!string.IsNullOrWhiteSpace(codeUsing.Key)) + { + if ("-".Equals(codeUsing.Key, StringComparison.OrdinalIgnoreCase)) + writer.WriteLine($"import {codeUsing.Select(x => GetAliasedSymbol(x.Item1, x.Item2)).Distinct(StringComparer.OrdinalIgnoreCase).Order(StringComparer.OrdinalIgnoreCase).Aggregate(static (x, y) => x + ", " + y)}"); + else + writer.WriteLine($"from {codeUsing.Key.ToSnakeCase()} import {codeUsing.Select(x => GetAliasedSymbol(x.Item1, x.Item2)).Distinct(StringComparer.OrdinalIgnoreCase).Order(StringComparer.OrdinalIgnoreCase).Aggregate(static (x, y) => x + ", " + y)}"); + } + writer.WriteLine(); + } + } + /// + /// Writes error mapping imports for a given code class. + /// + /// The CodeClass from which to write error mapping usings + /// An instance of the language writer + /// void + public void WriteInternalErrorMappingImports(CodeClass parentClass, LanguageWriter writer) + { + var parentNameSpace = parentClass.GetImmediateParentOfType(); + var internalErrorMappingImportSymbolsAndPaths = parentClass.Usings + .Where(static x => !x.IsExternal) + .Where(static x => x.Declaration?.TypeDefinition is CodeClass codeClass && codeClass.IsErrorDefinition) + .Select(x => _relativeImportManager.GetRelativeImportPathForUsing(x, parentNameSpace)) + .GroupBy(static x => x.Item3) + .Where(static x => !string.IsNullOrEmpty(x.Key)) + .OrderBy(static x => x.Key, StringComparer.OrdinalIgnoreCase); + WriteCodeUsings(internalErrorMappingImportSymbolsAndPaths, writer); + } + /// + /// Writes all internal imports for a given code class. + /// + /// The CodeClass from which to write internal usings + /// An instance of the language writer + /// void + public void WriteInternalImports(CodeClass parentClass, LanguageWriter writer) + { + var parentNameSpace = parentClass.GetImmediateParentOfType(); + var internalImportSymbolsAndPaths = parentClass.Usings + .Where(static x => !x.IsExternal) + .Select(x => _relativeImportManager.GetRelativeImportPathForUsing(x, parentNameSpace)) + .GroupBy(static x => x.Item3) + .Where(static x => !string.IsNullOrEmpty(x.Key)) + .OrderBy(static x => x.Key, StringComparer.OrdinalIgnoreCase); + WriteCodeUsings(internalImportSymbolsAndPaths, writer); + } + /// + /// Writes conditional internal imports for a given code element for type checking environments. + /// + /// The element to write internal usings from + /// An instance of the language writer + /// The code namespace of the code element + /// void + public void WriteConditionalInternalImports(ClassDeclaration codeElement, LanguageWriter writer, CodeNamespace parentNameSpace) + { + var internalImportSymbolsAndPaths = codeElement.Usings + .Where(static x => !x.IsExternal) + .Select(x => _relativeImportManager.GetRelativeImportPathForUsing(x, parentNameSpace)) + .GroupBy(static x => x.Item3) + .Where(static x => !string.IsNullOrEmpty(x.Key)) + .OrderBy(static x => x.Key, StringComparer.OrdinalIgnoreCase); + if (internalImportSymbolsAndPaths.Any()) + { + writer.WriteLine("if TYPE_CHECKING:"); + writer.IncreaseIndent(); + foreach (var codeUsing in internalImportSymbolsAndPaths) + writer.WriteLine($"from {codeUsing.Key.ToSnakeCase()} import {codeUsing.Select(x => GetAliasedSymbol(x.Item1, x.Item2)).Distinct(StringComparer.OrdinalIgnoreCase).Order(StringComparer.OrdinalIgnoreCase).Aggregate(static (x, y) => x + ", " + y)}"); + writer.DecreaseIndent(); + writer.WriteLine(); + + } + } + /// + /// Writes local imports for a given type. + /// + /// The parent CodeClass of the given type + /// The name of the given type. Must be a valid using declaration name in the parentClass + /// An instance of the language writer + /// void + public void WriteDeferredImport(CodeClass parentClass, string typeName, LanguageWriter writer) + { + var parentNamespace = parentClass.GetImmediateParentOfType(); + var internalImportSymbolsAndPaths = parentClass.Usings + .Where(static x => !x.IsExternal) + .Where(x => typeName.Equals(x.Declaration?.Name, StringComparison.OrdinalIgnoreCase)) + .Select(x => _relativeImportManager.GetRelativeImportPathForUsing(x, parentNamespace)) + .GroupBy(static x => x.Item3) + .Where(static x => !string.IsNullOrEmpty(x.Key)) + .OrderBy(static x => x.Key, StringComparer.OrdinalIgnoreCase); + WriteCodeUsings(internalImportSymbolsAndPaths, writer); + } + + private static void WriteCodeUsings(IOrderedEnumerable> importSymbolsAndPaths, LanguageWriter writer) + { + if (importSymbolsAndPaths.Any()) + { + foreach (var codeUsing in importSymbolsAndPaths) + writer.WriteLine($"from {codeUsing.Key.ToSnakeCase()} import {codeUsing.Select(x => GetAliasedSymbol(x.Item1, x.Item2)).Distinct(StringComparer.OrdinalIgnoreCase).Order(StringComparer.OrdinalIgnoreCase).Aggregate(static (x, y) => x + ", " + y)}"); + writer.WriteLine(); + } + } + private static string GetAliasedSymbol(string symbol, string alias) + { + return string.IsNullOrEmpty(alias) ? symbol : $"{symbol} as {alias}"; + } +} diff --git a/src/Kiota.Builder/Writers/Python/PythonConventionService.cs b/src/Kiota.Builder/Writers/Python/PythonConventionService.cs index b2499c56c4..50369d81aa 100644 --- a/src/Kiota.Builder/Writers/Python/PythonConventionService.cs +++ b/src/Kiota.Builder/Writers/Python/PythonConventionService.cs @@ -25,7 +25,7 @@ internal void AddRequestBuilderBody(CodeClass parentClass, string returnType, La var urlTemplateParams = string.IsNullOrEmpty(urlTemplateVarName) && parentClass.GetPropertyOfKind(CodePropertyKind.PathParameters) is CodeProperty pathParametersProperty ? $"self.{pathParametersProperty.Name.ToSnakeCase()}" : urlTemplateVarName; - var pathParametersSuffix = !(pathParameters?.Any() ?? false) ? string.Empty : $", {string.Join(", ", pathParameters.Select(x => $"{x.Name}"))}"; + var pathParametersSuffix = !(pathParameters?.Any() ?? false) ? string.Empty : $", {string.Join(", ", pathParameters.Select(x => $"{x.Name.ToSnakeCase()}"))}"; if (parentClass.GetPropertyOfKind(CodePropertyKind.RequestAdapter) is CodeProperty requestAdapterProp) writer.WriteLine($"return {returnType}(self.{requestAdapterProp.Name.ToSnakeCase()}, {urlTemplateParams}{pathParametersSuffix})"); } diff --git a/src/Kiota.Builder/Writers/Python/PythonRelativeImportManager.cs b/src/Kiota.Builder/Writers/Python/PythonRelativeImportManager.cs new file mode 100644 index 0000000000..2cda2dfa3b --- /dev/null +++ b/src/Kiota.Builder/Writers/Python/PythonRelativeImportManager.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +using Kiota.Builder.CodeDOM; +using Kiota.Builder.Extensions; + +namespace Kiota.Builder.Writers.Python; +public class PythonRelativeImportManager : RelativeImportManager +{ + public PythonRelativeImportManager(string namespacePrefix, char namespaceSeparator) : base(namespacePrefix, namespaceSeparator) + { + } + /// + /// Returns the relative import path for the given using and import context namespace. + /// + /// The using to import into the current namespace context + /// The current namespace + /// The import symbol, it's alias if any and the relative import path + public override (string, string, string) GetRelativeImportPathForUsing(CodeUsing codeUsing, CodeNamespace currentNamespace) + { + if (codeUsing?.IsExternal ?? true) + return (string.Empty, string.Empty, string.Empty);//it's an external import, add nothing + var (importSymbol, typeDef) = codeUsing.Declaration?.TypeDefinition is CodeElement td ? td switch + { + CodeFunction f => (f.Name.ToFirstCharacterLowerCase(), td), + _ => (td.Name.ToSnakeCase(), td), + } : (codeUsing.Name, null); + + if (typeDef == null) + return (importSymbol, codeUsing.Alias, "."); // it's relative to the folder, with no declaration (default failsafe) + var importPath = GetImportRelativePathFromNamespaces(currentNamespace, + typeDef.GetImmediateParentOfType()); + return (importSymbol, codeUsing.Alias, importPath); + } + protected new string GetImportRelativePathFromNamespaces(CodeNamespace currentNamespace, CodeNamespace importNamespace) + { + var result = currentNamespace.GetDifferential(importNamespace, prefix, separator); + return result.State switch + { + NamespaceDifferentialTrackerState.Same => ".", + NamespaceDifferentialTrackerState.Downwards => $".{GetRemainingImportPath(result.DownwardsSegments)}", + NamespaceDifferentialTrackerState.Upwards => GetUpwardsMoves(result.UpwardsMovesCount), + NamespaceDifferentialTrackerState.UpwardsAndThenDownwards => $"{GetUpwardsMoves(result.UpwardsMovesCount)}{GetRemainingImportPath(result.DownwardsSegments)}", + _ => throw new NotImplementedException(), + }; + } + protected static new string GetUpwardsMoves(int UpwardsMovesCount) => string.Join("", Enumerable.Repeat(".", UpwardsMovesCount)) + (UpwardsMovesCount > 0 ? "." : string.Empty); + protected static new string GetRemainingImportPath(IEnumerable remainingSegments) + { + if (remainingSegments.Any()) + return remainingSegments.Select(x => x.ToFirstCharacterLowerCase()).Aggregate((x, y) => $"{x}.{y}"); + return string.Empty; + } +} diff --git a/src/Kiota.Builder/Writers/Python/PythonWriter.cs b/src/Kiota.Builder/Writers/Python/PythonWriter.cs index b0ebfc2987..52d69e61de 100644 --- a/src/Kiota.Builder/Writers/Python/PythonWriter.cs +++ b/src/Kiota.Builder/Writers/Python/PythonWriter.cs @@ -7,11 +7,11 @@ public PythonWriter(string rootPath, string clientNamespaceName, bool usesBackin { PathSegmenter = new PythonPathSegmenter(rootPath, clientNamespaceName); var conventionService = new PythonConventionService(); - AddOrReplaceCodeElementWriter(new CodeClassDeclarationWriter(conventionService)); + AddOrReplaceCodeElementWriter(new CodeClassDeclarationWriter(conventionService, clientNamespaceName)); AddOrReplaceCodeElementWriter(new CodeBlockEndWriter()); AddOrReplaceCodeElementWriter(new CodeEnumWriter(conventionService)); - AddOrReplaceCodeElementWriter(new CodeMethodWriter(conventionService, usesBackingStore)); - AddOrReplaceCodeElementWriter(new CodePropertyWriter(conventionService)); + AddOrReplaceCodeElementWriter(new CodeMethodWriter(conventionService, clientNamespaceName, usesBackingStore)); + AddOrReplaceCodeElementWriter(new CodePropertyWriter(conventionService, clientNamespaceName)); AddOrReplaceCodeElementWriter(new CodeTypeWriter(conventionService)); AddOrReplaceCodeElementWriter(new CodeNameSpaceWriter(conventionService)); } diff --git a/tests/Kiota.Builder.Tests/CodeDOM/CodeElementComparerPythonTests.cs b/tests/Kiota.Builder.Tests/CodeDOM/CodeElementComparerPythonTests.cs new file mode 100644 index 0000000000..c09496381e --- /dev/null +++ b/tests/Kiota.Builder.Tests/CodeDOM/CodeElementComparerPythonTests.cs @@ -0,0 +1,89 @@ +using System; +using System.Collections.Generic; + +using Kiota.Builder.CodeDOM; + +using Xunit; + +namespace Kiota.Builder.Tests.CodeDOM; +public class CodeElementComparerPythonTests +{ + [Fact] + public void OrdersWithMethodWithinClass() + { + var root = CodeNamespace.InitRootNamespace(); + var comparer = new CodeElementOrderComparerPython(); + var codeClass = new CodeClass + { + Name = "Class" + }; + root.AddClass(codeClass); + var method = new CodeMethod + { + Name = "Method", + ReturnType = new CodeType + { + Name = "string" + } + }; + codeClass.AddMethod(method); + method.AddParameter(new CodeParameter + { + Name = "param", + Type = new CodeType + { + Name = "string" + } + }); + var dataSet = new List> { + new(null, null, 0), + new(null, new CodeClass(), -1), + new(new CodeClass(), null, 1), + new(new CodeUsing(), new CodeProperty() { + Name = "prop", + Type = new CodeType { + Name = "string" + } + }, -1100), + new(new CodeIndexer() { + ReturnType = new CodeType { + Name = "string" + }, + IndexType = new CodeType { + Name = "string" + } + }, new CodeProperty() { + Name = "prop", + Type = new CodeType { + Name = "string" + } + }, -1100), + new(method, new CodeProperty() { + Name = "prop", + Type = new CodeType { + Name = "string" + } + }, -899), + new(method, codeClass, -699), + new(new CodeMethod() { + Kind = CodeMethodKind.Constructor, + ReturnType = new CodeType + { + Name = "null", + } + }, method, -301), + new(new CodeMethod() { + Kind = CodeMethodKind.ClientConstructor, + ReturnType = new CodeType + { + Name = "null", + } + }, method, -301), + + }; + foreach (var dataEntry in dataSet) + { + Assert.Equal(dataEntry.Item3, comparer.Compare(dataEntry.Item1, dataEntry.Item2)); + } + } +} diff --git a/tests/Kiota.Builder.Tests/Writers/Python/CodeClassDeclarationWriterTests.cs b/tests/Kiota.Builder.Tests/Writers/Python/CodeClassDeclarationWriterTests.cs index b949009287..da4a13f080 100644 --- a/tests/Kiota.Builder.Tests/Writers/Python/CodeClassDeclarationWriterTests.cs +++ b/tests/Kiota.Builder.Tests/Writers/Python/CodeClassDeclarationWriterTests.cs @@ -13,6 +13,8 @@ public class CodeClassDeclarationWriterTests : IDisposable { private const string DefaultPath = "./"; private const string DefaultName = "name"; + + private const string ClientNamespaceName = "graph"; private readonly CodeNamespace root; private readonly CodeNamespace ns; private readonly StringWriter tw; @@ -23,7 +25,7 @@ public class CodeClassDeclarationWriterTests : IDisposable public CodeClassDeclarationWriterTests() { writer = LanguageWriter.GetLanguageWriter(GenerationLanguage.Python, DefaultPath, DefaultName); - codeElementWriter = new CodeClassDeclarationWriter(new PythonConventionService()); + codeElementWriter = new CodeClassDeclarationWriter(new PythonConventionService(), ClientNamespaceName); tw = new StringWriter(); writer.SetTextWriter(tw); root = CodeNamespace.InitRootNamespace(); @@ -42,7 +44,7 @@ public void Dispose() [Fact] public void Defensive() { - var codeClassDeclarationWriter = new CodeClassDeclarationWriter(new PythonConventionService()); + var codeClassDeclarationWriter = new CodeClassDeclarationWriter(new PythonConventionService(), ClientNamespaceName); Assert.Throws(() => codeClassDeclarationWriter.WriteCodeElement(null, writer)); var declaration = parentClass.StartBlock; Assert.Throws(() => codeClassDeclarationWriter.WriteCodeElement(declaration, null)); @@ -52,6 +54,7 @@ public void WritesSimpleDeclaration() { codeElementWriter.WriteCodeElement(parentClass.StartBlock, writer); var result = tw.ToString(); + Assert.DoesNotContain("@dataclass", result); Assert.Contains("class ParentClass()", result); } [Fact] @@ -68,18 +71,36 @@ public void WritesImplementation() }); codeElementWriter.WriteCodeElement(declaration, writer); var result = tw.ToString(); + Assert.DoesNotContain("()", result); Assert.Contains("(SecondInterface, SomeInterface):", result); } [Fact] public void WritesInheritance() { var declaration = parentClass.StartBlock; + var interfaceDef = new CodeInterface + { + Name = "someInterface", + }; + ns.AddInterface(interfaceDef); + var nUsing = new CodeUsing + { + Name = "graph", + Declaration = new() + { + Name = "someInterface", + TypeDefinition = interfaceDef, + } + }; + declaration.AddUsings(nUsing); declaration.Inherits = new() { Name = "someInterface" }; codeElementWriter.WriteCodeElement(declaration, writer); var result = tw.ToString(); + Assert.Contains("if TYPE_CHECKING:", result); + Assert.Contains("from . import some_interface", result); Assert.Contains("(some_interface.SomeInterface):", result); } [Fact] @@ -129,7 +150,7 @@ public void WritesExternalImportsWithoutPath() Assert.Contains("import Objects", result); } [Fact] - public void WritesInternalImportsSubNamespace() + public void WritesConditionalInternalImportsSubNamespace() { var declaration = parentClass.StartBlock; var subNS = ns.AddNamespace($"{ns.Name}.messages"); @@ -150,11 +171,12 @@ public void WritesInternalImportsSubNamespace() declaration.AddUsings(nUsing); codeElementWriter.WriteCodeElement(declaration, writer); var result = tw.ToString(); - Assert.Contains("message = lazy_import('graphtests.models.messages.message')", result); + Assert.Contains("if TYPE_CHECKING:", result); + Assert.Contains("from .messages import message", result); } [Fact] - public void WritesInternalImportsSameNamespace() + public void WritesConditionalInternalImportsSameNamespace() { var declaration = parentClass.StartBlock; var messageClassDef = new CodeClass @@ -174,7 +196,8 @@ public void WritesInternalImportsSameNamespace() declaration.AddUsings(nUsing); codeElementWriter.WriteCodeElement(declaration, writer); var result = tw.ToString(); - Assert.Contains("message = lazy_import('graphtests.models.message')", result); + Assert.Contains("if TYPE_CHECKING:", result); + Assert.Contains("from . import message", result); } [Fact] public void WritesInternalImportsNoTypeDef() @@ -191,6 +214,6 @@ public void WritesInternalImportsNoTypeDef() declaration.AddUsings(nUsing); codeElementWriter.WriteCodeElement(declaration, writer); var result = tw.ToString(); - Assert.DoesNotContain("message = lazy_import('graphtests.models.message')", result); + Assert.DoesNotContain("from . import message", result); } } diff --git a/tests/Kiota.Builder.Tests/Writers/Python/CodeMethodWriterTests.cs b/tests/Kiota.Builder.Tests/Writers/Python/CodeMethodWriterTests.cs index 416fbf99c7..87f193c452 100644 --- a/tests/Kiota.Builder.Tests/Writers/Python/CodeMethodWriterTests.cs +++ b/tests/Kiota.Builder.Tests/Writers/Python/CodeMethodWriterTests.cs @@ -17,7 +17,9 @@ public class CodeMethodWriterTests : IDisposable private readonly LanguageWriter writer; private readonly CodeMethod method; private readonly CodeClass parentClass; + private readonly CodeClass childClass; private readonly CodeNamespace root; + private const string ClientNamespaceName = "graph"; private const string MethodName = "method_name"; private const string ReturnTypeName = "Somecustomtype"; private const string MethodDescription = "some description"; @@ -34,6 +36,26 @@ public CodeMethodWriterTests() Name = "parentClass" }; root.AddClass(parentClass); + childClass = new CodeClass + { + Name = "childClass" + }; + root.AddClass(childClass); + var returnTypeClassDef = new CodeClass + { + Name = ReturnTypeName, + }; + root.AddClass(returnTypeClassDef); + var nUsing = new CodeUsing + { + Name = returnTypeClassDef.Name, + Declaration = new() + { + Name = returnTypeClassDef.Name, + TypeDefinition = returnTypeClassDef, + } + }; + parentClass.StartBlock.AddUsings(nUsing); method = new CodeMethod { Name = MethodName, @@ -196,6 +218,20 @@ private void AddInheritanceClass() Name = "someParentClass" }; } + + private void AddCodeUsings() + { + var nUsing = new CodeUsing + { + Name = childClass.Name, + Declaration = new() + { + Name = childClass.Name, + TypeDefinition = childClass, + } + }; + parentClass.StartBlock.AddUsings(nUsing); + } private void AddRequestBodyParameters(bool useComplexTypeForBody = false) { var stringType = new CodeType @@ -280,26 +316,59 @@ public void WritesRequestExecutorBody() var error4XX = root.AddClass(new CodeClass { Name = "Error4XX", + IsErrorDefinition = true + }).First(); var error5XX = root.AddClass(new CodeClass { Name = "Error5XX", + IsErrorDefinition = true }).First(); var error401 = root.AddClass(new CodeClass { Name = "Error401", + IsErrorDefinition = true }).First(); + parentClass.StartBlock.AddUsings(new() + { + Name = error401.Name, + Declaration = new() + { + Name = error401.Name, + TypeDefinition = error401, + } + }, + new() + { + Name = error5XX.Name, + Declaration = new() + { + Name = error5XX.Name, + TypeDefinition = error5XX, + } + }, + new() + { + Name = error4XX.Name, + Declaration = new() + { + Name = error4XX.Name, + TypeDefinition = error4XX, + } + }); method.AddErrorMapping("4XX", new CodeType { Name = "Error4XX", TypeDefinition = error4XX }); method.AddErrorMapping("5XX", new CodeType { Name = "Error5XX", TypeDefinition = error5XX }); - method.AddErrorMapping("403", new CodeType { Name = "Error403", TypeDefinition = error401 }); + method.AddErrorMapping("401", new CodeType { Name = "Error401", TypeDefinition = error401 }); AddRequestBodyParameters(); writer.Write(method); var result = tw.ToString(); Assert.Contains("request_info", result); + Assert.Contains("from . import error401, error4_x_x, error5_x_x", result); Assert.Contains("error_mapping: Dict[str, ParsableFactory] =", result); Assert.Contains("\"4XX\": error4_x_x.Error4XX", result); Assert.Contains("\"5XX\": error5_x_x.Error5XX", result); - Assert.Contains("\"403\": error403.Error403", result); + Assert.Contains("\"401\": error401.Error401", result); + Assert.Contains("from . import somecustomtype", result); Assert.Contains("send_async", result); Assert.Contains("raise Exception", result); } @@ -323,6 +392,7 @@ public void WritesRequestExecutorBodyForCollections() AddRequestBodyParameters(); writer.Write(method); var result = tw.ToString(); + Assert.Contains("from . import somecustomtype", result); Assert.Contains("send_collection_async", result); } [Fact] @@ -381,6 +451,7 @@ public void WritesInheritedDeSerializerBody() AddInheritanceClass(); writer.Write(method); var result = tw.ToString(); + Assert.Contains("from . import somecustomtype", result); Assert.Contains("super_fields = super()", result); Assert.Contains("fields.update(super_fields)", result); Assert.Contains("return fields", result); @@ -393,6 +464,8 @@ public void WritesDeSerializerBody() AddSerializationProperties(); writer.Write(method); var result = tw.ToString(); + Assert.Contains("from . import somecustomtype", result); + Assert.Contains("fields: Dict[str, Callable[[Any], None]] =", result); Assert.Contains("get_str_value", result); Assert.Contains("get_int_value", result); Assert.Contains("get_float_value", result); @@ -489,7 +562,7 @@ public void WritesMethodSyncDescription() [Fact] public void Defensive() { - var codeMethodWriter = new CodeMethodWriter(new PythonConventionService(), false); + var codeMethodWriter = new CodeMethodWriter(new PythonConventionService(), ClientNamespaceName, false); Assert.Throws(() => codeMethodWriter.WriteCodeElement(null, writer)); Assert.Throws(() => codeMethodWriter.WriteCodeElement(method, null)); var originalParent = method.Parent; @@ -538,6 +611,21 @@ public void DoesNotAddAsyncInformationOnSyncMethods() public void WritesFactoryMethods() { method.Kind = CodeMethodKind.Factory; + method.AddParameter(new CodeParameter + { + Name = "parseNode", + Kind = CodeParameterKind.ParseNode, + Type = new CodeType + { + Name = "ParseNode", + TypeDefinition = new CodeClass + { + Name = "ParseNode", + }, + IsExternal = true, + }, + Optional = false, + }); writer.Write(method); var result = tw.ToString(); Assert.Contains("@staticmethod", result); @@ -545,6 +633,161 @@ public void WritesFactoryMethods() } [Fact] + public void WritesModelFactoryBody() + { + parentClass.Kind = CodeClassKind.Model; + childClass.Kind = CodeClassKind.Model; + childClass.StartBlock.Inherits = new CodeType + { + Name = "parentClass", + TypeDefinition = parentClass, + }; + method.Kind = CodeMethodKind.Factory; + method.ReturnType = new CodeType + { + Name = "parentClass", + TypeDefinition = parentClass, + }; + method.IsStatic = true; + parentClass.DiscriminatorInformation.AddDiscriminatorMapping("ns.childclass", new CodeType + { + Name = "childClass", + TypeDefinition = childClass, + }); + parentClass.DiscriminatorInformation.DiscriminatorPropertyName = "@odata.type"; + AddCodeUsings(); + method.AddParameter(new CodeParameter + { + Name = "parseNode", + Kind = CodeParameterKind.ParseNode, + Type = new CodeType + { + Name = "ParseNode", + TypeDefinition = new CodeClass + { + Name = "ParseNode", + }, + IsExternal = true, + }, + Optional = false, + }); + writer.Write(method); + var result = tw.ToString(); + Assert.Contains("mapping_value_node = parse_node.get_child_node(\"@odata.type\")", result); + Assert.Contains("if mapping_value_node:", result); + Assert.Contains("mapping_value = mapping_value_node.get_str_value()", result); + Assert.Contains("if mapping_value == \"ns.childclass\"", result); + Assert.Contains("from . import child_class", result); + Assert.Contains("return child_class.ChildClass()", result); + Assert.Contains("return ParentClass()", result); + } + [Fact] + public void DoesntWriteFactoryConditionalsOnMissingParameter() + { + parentClass.Kind = CodeClassKind.Model; + childClass.Kind = CodeClassKind.Model; + childClass.StartBlock.Inherits = new CodeType + { + Name = "parentClass", + TypeDefinition = parentClass, + }; + method.Kind = CodeMethodKind.Factory; + method.ReturnType = new CodeType + { + Name = "parentClass", + TypeDefinition = parentClass, + }; + method.IsStatic = true; + parentClass.DiscriminatorInformation.AddDiscriminatorMapping("ns.childclass", new CodeType + { + Name = "childClass", + TypeDefinition = childClass, + }); + parentClass.DiscriminatorInformation.DiscriminatorPropertyName = "@odata.type"; + Assert.Throws(() => writer.Write(method)); + } + [Fact] + public void DoesntWriteFactoryConditionalsOnEmptyPropertyName() + { + parentClass.Kind = CodeClassKind.Model; + childClass.Kind = CodeClassKind.Model; + childClass.StartBlock.Inherits = new CodeType + { + Name = "parentClass", + TypeDefinition = parentClass, + }; + method.Kind = CodeMethodKind.Factory; + method.ReturnType = new CodeType + { + Name = "parentClass", + TypeDefinition = parentClass, + }; + method.IsStatic = true; + parentClass.DiscriminatorInformation.AddDiscriminatorMapping("ns.childclass", new CodeType + { + Name = "childClass", + TypeDefinition = childClass, + }); + parentClass.DiscriminatorInformation.DiscriminatorPropertyName = string.Empty; + method.AddParameter(new CodeParameter + { + Name = "parseNode", + Kind = CodeParameterKind.ParseNode, + Type = new CodeType + { + Name = "ParseNode", + TypeDefinition = new CodeClass + { + Name = "ParseNode", + }, + IsExternal = true, + }, + Optional = false, + }); + writer.Write(method); + var result = tw.ToString(); + Assert.DoesNotContain("mapping_value_node = parse_node.get_child_node(\"@odata.type\")", result); + Assert.DoesNotContain("if mapping_value_node:", result); + Assert.DoesNotContain("mapping_value = mapping_value_node.get_str_value", result); + Assert.DoesNotContain("if mapping_value == \"ns.childclass\"", result); + Assert.Contains("return ParentClass()", result); + } + [Fact] + public void DoesntWriteFactorySwitchOnEmptyMappings() + { + parentClass.Kind = CodeClassKind.Model; + method.Kind = CodeMethodKind.Factory; + method.ReturnType = new CodeType + { + Name = "parentClass", + TypeDefinition = parentClass, + }; + method.IsStatic = true; + parentClass.DiscriminatorInformation.DiscriminatorPropertyName = "@odata.type"; + method.AddParameter(new CodeParameter + { + Name = "parseNode", + Kind = CodeParameterKind.ParseNode, + Type = new CodeType + { + Name = "ParseNode", + TypeDefinition = new CodeClass + { + Name = "ParseNode", + }, + IsExternal = true, + }, + Optional = false, + }); + writer.Write(method); + var result = tw.ToString(); + Assert.DoesNotContain("mapping_value_node = parse_node.get_child_node(\"@odata.type\")", result); + Assert.DoesNotContain("if mapping_value_node:", result); + Assert.DoesNotContain("mapping_value = mapping_value_node.get_str_value", result); + Assert.DoesNotContain("if mapping_value == \"ns.childclass\"", result); + Assert.Contains("return ParentClass()", result); + } + [Fact] public void WritesPublicMethodByDefault() { writer.Write(method); @@ -578,6 +821,7 @@ public void WritesIndexer() Name = "string", } }; + writer.Write(method); var result = tw.ToString(); Assert.Contains("self.request_adapter", result); @@ -601,6 +845,7 @@ public void WritesPathParameterRequestBuilder() }); writer.Write(method); var result = tw.ToString(); + Assert.Contains("from . import somecustomtype", result); Assert.Contains("self.request_adapter", result); Assert.Contains("self.path_parameters", result); Assert.Contains("path_param", result); diff --git a/tests/Kiota.Builder.Tests/Writers/Python/CodePropertyWriterTests.cs b/tests/Kiota.Builder.Tests/Writers/Python/CodePropertyWriterTests.cs index 640a84c57d..97f697ccfb 100644 --- a/tests/Kiota.Builder.Tests/Writers/Python/CodePropertyWriterTests.cs +++ b/tests/Kiota.Builder.Tests/Writers/Python/CodePropertyWriterTests.cs @@ -15,6 +15,7 @@ public class CodePropertyWriterTests : IDisposable private readonly LanguageWriter writer; private readonly CodeProperty property; private readonly CodeClass parentClass; + private readonly CodeNamespace ns; private const string PropertyName = "propertyName"; private const string TypeName = "Somecustomtype"; public CodePropertyWriterTests() @@ -23,11 +24,12 @@ public CodePropertyWriterTests() tw = new StringWriter(); writer.SetTextWriter(tw); var root = CodeNamespace.InitRootNamespace(); + ns = root.AddNamespace("graphtests.models"); parentClass = new CodeClass { Name = "parentClass" }; - root.AddClass(parentClass); + ns.AddClass(parentClass); property = new CodeProperty { Name = PropertyName, @@ -36,6 +38,22 @@ public CodePropertyWriterTests() Name = TypeName } }; + var subNS = ns.AddNamespace($"{ns.Name}.somecustomtype"); + var somecustomtypeClassDef = new CodeClass + { + Name = "Somecustomtype", + }; + subNS.AddClass(somecustomtypeClassDef); + var nUsing = new CodeUsing + { + Name = somecustomtypeClassDef.Name, + Declaration = new() + { + Name = somecustomtypeClassDef.Name, + TypeDefinition = somecustomtypeClassDef, + } + }; + parentClass.StartBlock.AddUsings(nUsing); parentClass.AddProperty(property, new() { Name = "pathParameters", @@ -67,8 +85,9 @@ public void WritesRequestBuilder() writer.Write(property); var result = tw.ToString(); Assert.Contains("@property", result); - Assert.Contains("def property_name(", result); + Assert.Contains("def property_name(self) -> somecustomtype.Somecustomtype:", result); Assert.Contains("This is a request builder", result); + Assert.Contains("from .somecustomtype import somecustomtype", result); Assert.Contains($"return {TypeName.ToLower()}.{TypeName}(", result); Assert.Contains("self.request_adapter", result); Assert.Contains("self.path_parameters", result); diff --git a/tests/Kiota.Builder.Tests/Writers/Python/CodeUsingWriterTests.cs b/tests/Kiota.Builder.Tests/Writers/Python/CodeUsingWriterTests.cs new file mode 100644 index 0000000000..6e42efbe03 --- /dev/null +++ b/tests/Kiota.Builder.Tests/Writers/Python/CodeUsingWriterTests.cs @@ -0,0 +1,70 @@ +using System.IO; +using System.Linq; +using Kiota.Builder.CodeDOM; +using Kiota.Builder.Writers; +using Kiota.Builder.Writers.Python; +using Xunit; + +namespace Kiota.Builder.Tests.Writers.Python; + +public class CodeUsingWriterTests +{ + private const string DefaultPath = "./"; + private const string DefaultName = "name"; + private readonly LanguageWriter writer; + private readonly StringWriter tw; + private readonly CodeNamespace root; + public CodeUsingWriterTests() + { + writer = LanguageWriter.GetLanguageWriter(GenerationLanguage.TypeScript, DefaultPath, DefaultName); + tw = new StringWriter(); + writer.SetTextWriter(tw); + root = CodeNamespace.InitRootNamespace(); + } + [Fact] + public void WritesAliasedSymbol() + { + var usingWriter = new CodeUsingWriter("foo"); + var codeClass = root.AddClass(new CodeClass + { + Name = "bar", + }).First(); + var us = new CodeUsing + { + Name = "bar", + Alias = "baz", + Declaration = new CodeType + { + Name = "bar", + TypeDefinition = codeClass, + }, + }; + codeClass.AddUsing(us); + usingWriter.WriteInternalImports(codeClass, writer); + var result = tw.ToString(); + Assert.Contains("from . import bar as baz", result); + } + [Fact] + public void DoesntAliasRegularSymbols() + { + var usingWriter = new CodeUsingWriter("foo"); + var codeClass = root.AddClass(new CodeClass + { + Name = "bar", + + }).First(); + var us = new CodeUsing + { + Name = "bar", + Declaration = new CodeType + { + Name = "bar", + TypeDefinition = codeClass, + }, + }; + codeClass.AddUsing(us); + usingWriter.WriteInternalImports(codeClass, writer); + var result = tw.ToString(); + Assert.Contains("from . import bar", result); + } +}