diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c9f7440cb..3df046673f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added +- Added backing store support for Python. [#2858](https://github.com/microsoft/kiota/issues/2858) ### Changed diff --git a/src/Kiota.Builder/Refiners/PythonRefiner.cs b/src/Kiota.Builder/Refiners/PythonRefiner.cs index 3c69114d7a..fc1c0c9529 100644 --- a/src/Kiota.Builder/Refiners/PythonRefiner.cs +++ b/src/Kiota.Builder/Refiners/PythonRefiner.cs @@ -38,7 +38,7 @@ public override Task Refine(CodeNamespace generatedCode, CancellationToken cance RemoveCancellationParameter(generatedCode); CorrectCoreType(generatedCode, CorrectMethodType, CorrectPropertyType, CorrectImplements); cancellationToken.ThrowIfCancellationRequested(); - CorrectCoreTypesForBackingStore(generatedCode, "BackingStoreFactorySingleton.__instance.create_backing_store()"); + CorrectCoreTypesForBackingStore(generatedCode, "field(default_factory=BackingStoreFactorySingleton(backing_store_factory=None).backing_store_factory.create_backing_store, repr=False)"); AddPropertiesAndMethodTypesImports(generatedCode, true, true, true, codeTypeFilter); AddParsableImplementsForModelClasses(generatedCode, "Parsable"); cancellationToken.ThrowIfCancellationRequested(); @@ -82,7 +82,7 @@ public override Task Refine(CodeNamespace generatedCode, CancellationToken cance CodePropertyKind.AdditionalData, }, static (_, s) => s.ToSnakeCase(), - _configuration.UsesBackingStore, + false, false, string.Empty, string.Empty); @@ -153,7 +153,7 @@ public override Task Refine(CodeNamespace generatedCode, CancellationToken cance method.Parameters.Any(y => y.IsOfKind(CodeParameterKind.BackingStore)), $"{AbstractionsPackageName}.store", "BackingStoreFactory", "BackingStoreFactorySingleton"), new (static x => x is CodeProperty prop && prop.IsOfKind(CodePropertyKind.BackingStore), - $"{AbstractionsPackageName}.store", "BackingStore", "BackedModel", "BackingStoreFactorySingleton" ), + $"{AbstractionsPackageName}.store", "BackedModel", "BackingStore", "BackingStoreFactorySingleton" ), new (static x => x is CodeClass @class && (@class.IsOfKind(CodeClassKind.Model) || x.Parent is CodeClass), "dataclasses", "dataclass, field"), new (static x => x is CodeClass { OriginalComposedType: CodeIntersectionType intersectionType } && intersectionType.Types.Any(static y => !y.IsExternal) && intersectionType.DiscriminatorInformation.HasBasicDiscriminatorInformation, $"{AbstractionsPackageName}.serialization", "ParseNodeHelper"), diff --git a/src/Kiota.Builder/Writers/LanguageWriter.cs b/src/Kiota.Builder/Writers/LanguageWriter.cs index c66c49e931..3319c11052 100644 --- a/src/Kiota.Builder/Writers/LanguageWriter.cs +++ b/src/Kiota.Builder/Writers/LanguageWriter.cs @@ -179,7 +179,7 @@ public static LanguageWriter GetLanguageWriter(GenerationLanguage language, stri GenerationLanguage.TypeScript => new TypeScriptWriter(outputPath, clientNamespaceName, usesBackingStore), GenerationLanguage.Ruby => new RubyWriter(outputPath, clientNamespaceName), GenerationLanguage.PHP => new PhpWriter(outputPath, clientNamespaceName, usesBackingStore), - GenerationLanguage.Python => new PythonWriter(outputPath, clientNamespaceName), + GenerationLanguage.Python => new PythonWriter(outputPath, clientNamespaceName, usesBackingStore), GenerationLanguage.Go => new GoWriter(outputPath, clientNamespaceName), GenerationLanguage.Shell => new ShellWriter(outputPath, clientNamespaceName), GenerationLanguage.Swift => new SwiftWriter(outputPath, clientNamespaceName), diff --git a/src/Kiota.Builder/Writers/Python/CodeMethodWriter.cs b/src/Kiota.Builder/Writers/Python/CodeMethodWriter.cs index 20641cf615..99b47e6485 100644 --- a/src/Kiota.Builder/Writers/Python/CodeMethodWriter.cs +++ b/src/Kiota.Builder/Writers/Python/CodeMethodWriter.cs @@ -248,7 +248,7 @@ private static void WriteApiConstructorBody(CodeClass parentClass, CodeMethod me writer.WriteLine($"self.{pathParametersProperty.Name.ToSnakeCase()}[\"base_url\"] = self.{requestAdapterPropertyName}.base_url"); } if (backingStoreParameter != null) - writer.WriteLine($"self.{requestAdapterPropertyName}.enable_backing_store({backingStoreParameter.Name})"); + writer.WriteLine($"self.{requestAdapterPropertyName}.enable_backing_store({backingStoreParameter.Name.ToSnakeCase()})"); } private static void WriteQueryParametersMapper(CodeMethod codeElement, CodeClass parentClass, LanguageWriter writer) { @@ -338,15 +338,6 @@ private void WriteConstructorBody(CodeClass parentClass, CodeMethod currentMetho WriteDirectAccessProperties(parentClass, writer); WriteSetterAccessProperties(parentClass, writer); WriteSetterAccessPropertiesWithoutDefaults(parentClass, writer); - if (currentMethod.Parameters.OfKind(CodeParameterKind.PathParameters) is CodeParameter pathParametersParam) - conventions.AddParametersAssignment(writer, - pathParametersParam.Type.AllTypes.OfType().FirstOrDefault(), - pathParametersParam.Name.ToFirstCharacterLowerCase(), - currentMethod.Parameters - .Where(x => x.IsOfKind(CodeParameterKind.Path)) - .Select(x => (x.Type, x.SerializationName, x.Name.ToFirstCharacterLowerCase())) - .ToArray()); - AssignPropertyFromParameter(parentClass, currentMethod, CodeParameterKind.PathParameters, CodePropertyKind.PathParameters, writer, conventions.TempDictionaryVarName); } if (parentClass.IsOfKind(CodeClassKind.Model)) @@ -362,15 +353,20 @@ private void WriteDirectAccessProperties(CodeClass parentClass, LanguageWriter w .ThenBy(static x => x.Name)) { var returnType = conventions.GetTypeString(propWithDefault.Type, propWithDefault, true, writer); + var defaultValue = propWithDefault.DefaultValue; + if (propWithDefault.Type is CodeType propertyType && propertyType.TypeDefinition is CodeEnum enumDefinition) + { + defaultValue = $"{enumDefinition.Name.ToFirstCharacterUpperCase()}({defaultValue})"; + } conventions.WriteInLineDescription(propWithDefault.Documentation.Description, writer); if (parentClass.IsOfKind(CodeClassKind.Model)) { - writer.WriteLine($"{propWithDefault.Name.ToSnakeCase()}: {(propWithDefault.Type.IsNullable ? "Optional[" : string.Empty)}{returnType}{(propWithDefault.Type.IsNullable ? "]" : string.Empty)} = {propWithDefault.DefaultValue}"); + writer.WriteLine($"{propWithDefault.Name.ToSnakeCase()}: {(propWithDefault.Type.IsNullable ? "Optional[" : string.Empty)}{returnType}{(propWithDefault.Type.IsNullable ? "]" : string.Empty)} = {defaultValue}"); writer.WriteLine(); } else { - writer.WriteLine($"self.{conventions.GetAccessModifier(propWithDefault.Access)}{propWithDefault.NamePrefix}{propWithDefault.Name.ToSnakeCase()}: {(propWithDefault.Type.IsNullable ? "Optional[" : string.Empty)}{returnType}{(propWithDefault.Type.IsNullable ? "]" : string.Empty)} = {propWithDefault.DefaultValue}"); + writer.WriteLine($"self.{conventions.GetAccessModifier(propWithDefault.Access)}{propWithDefault.NamePrefix}{propWithDefault.Name.ToSnakeCase()}: {(propWithDefault.Type.IsNullable ? "Optional[" : string.Empty)}{returnType}{(propWithDefault.Type.IsNullable ? "]" : string.Empty)} = {defaultValue}"); writer.WriteLine(); } } @@ -384,10 +380,20 @@ private void WriteSetterAccessProperties(CodeClass parentClass, LanguageWriter w .OrderByDescending(static x => x.Kind) .ThenBy(static x => x.Name)) { + var defaultValue = propWithDefault.DefaultValue; + if (propWithDefault.Type is CodeType propertyType && propertyType.TypeDefinition is CodeEnum enumDefinition) + { + defaultValue = $"{enumDefinition.Name.ToFirstCharacterUpperCase()}({defaultValue})"; + } + var returnType = conventions.GetTypeString(propWithDefault.Type, propWithDefault, true, writer); + conventions.WriteInLineDescription(propWithDefault.Documentation.Description, writer); + var setterString = $"{propWithDefault.Name.ToSnakeCase()}: {(propWithDefault.Type.IsNullable ? "Optional[" : string.Empty)}{returnType}{(propWithDefault.Type.IsNullable ? "]" : string.Empty)} = {defaultValue}"; if (parentClass.IsOfKind(CodeClassKind.Model)) - writer.WriteLine($"{propWithDefault.Name.ToSnakeCase()} = {propWithDefault.DefaultValue}"); + { + writer.WriteLine($"{setterString}"); + } else - writer.WriteLine($"self.{propWithDefault.Name.ToSnakeCase()} = {propWithDefault.DefaultValue}"); + writer.WriteLine($"self.{setterString}"); } } private void WriteSetterAccessPropertiesWithoutDefaults(CodeClass parentClass, LanguageWriter writer) @@ -405,16 +411,6 @@ private void WriteSetterAccessPropertiesWithoutDefaults(CodeClass parentClass, L writer.WriteLine($"self.{conventions.GetAccessModifier(propWithoutDefault.Access)}{propWithoutDefault.NamePrefix}{propWithoutDefault.Name.ToSnakeCase()}: {(propWithoutDefault.Type.IsNullable ? "Optional[" : string.Empty)}{returnType}{(propWithoutDefault.Type.IsNullable ? "]" : string.Empty)} = None"); } } - private static void AssignPropertyFromParameter(CodeClass parentClass, CodeMethod currentMethod, CodeParameterKind parameterKind, CodePropertyKind propertyKind, LanguageWriter writer, string? variableName = default) - { - if (parentClass.GetPropertyOfKind(propertyKind) is CodeProperty property) - { - if (!string.IsNullOrEmpty(variableName)) - writer.WriteLine($"self.{property.Name.ToSnakeCase()} = {variableName.ToSnakeCase()}"); - else if (currentMethod.Parameters.OfKind(parameterKind) is CodeParameter parameter) - writer.WriteLine($"self.{property.Name.ToSnakeCase()} = {parameter.Name.ToSnakeCase()}"); - } - } private static void WriteSetterBody(CodeMethod codeElement, LanguageWriter writer, CodeClass parentClass) { if (!parentClass.IsOfKind(CodeClassKind.Model)) diff --git a/tests/Kiota.Builder.Tests/Writers/Python/CodeMethodWriterTests.cs b/tests/Kiota.Builder.Tests/Writers/Python/CodeMethodWriterTests.cs index 3b5836dff9..dc7773fb7e 100644 --- a/tests/Kiota.Builder.Tests/Writers/Python/CodeMethodWriterTests.cs +++ b/tests/Kiota.Builder.Tests/Writers/Python/CodeMethodWriterTests.cs @@ -3,6 +3,7 @@ using System.Linq; using Kiota.Builder.CodeDOM; +using Kiota.Builder.Extensions; using Kiota.Builder.Writers; using Kiota.Builder.Writers.Python; @@ -739,6 +740,7 @@ public void WritesUnionDeSerializerBody() var result = tw.ToString(); Assert.DoesNotContain("super_fields = super()", result); Assert.DoesNotContain("return fields", result); + Assert.DoesNotContain("elif", result); Assert.Contains("if self.complex_type1_value:", result); Assert.Contains("return self.complex_type1_value.get_field_deserializers()", result); Assert.Contains("return {}", result); @@ -763,6 +765,7 @@ public void WritesIntersectionDeSerializerBody() Assert.DoesNotContain("super_fields = super()", result); Assert.DoesNotContain("return fields", result); Assert.Contains("from .complex_type1 import ComplexType1", result); + Assert.DoesNotContain("elif", result); Assert.Contains("if self.complex_type1_value or self.complex_type3_value", result); Assert.Contains("return ParseNodeHelper.merge_deserializers_for_intersection_wrapper(self.complex_type1_value, self.complex_type3_value)", result); Assert.Contains("return {}", result); @@ -1454,6 +1457,34 @@ public void WritesConstructor() setup(); method.Kind = CodeMethodKind.Constructor; method.IsAsync = false; + var propName = "prop_without_default_value"; + parentClass.Kind = CodeClassKind.Custom; + parentClass.AddProperty(new CodeProperty + { + Name = propName, + Kind = CodePropertyKind.Custom, + Documentation = new() + { + Description = "This property has a description", + }, + Type = new CodeType + { + Name = "string" + } + }); + writer.Write(method); + var result = tw.ToString(); + Assert.Contains("def __init__(self,)", result); + Assert.Contains("This property has a description", result); + Assert.Contains($"self.{propName}: Optional[str] = None", result); + Assert.DoesNotContain("get_path_parameters(", result); + } + [Fact] + public void WritesConstructorForReqestBuilder() + { + setup(true); + method.Kind = CodeMethodKind.Constructor; + method.IsAsync = false; var defaultValue = "someVal"; var propName = "prop_with_default_value"; parentClass.Kind = CodeClassKind.RequestBuilder; @@ -1471,7 +1502,89 @@ public void WritesConstructor() Name = "string" } }); - AddRequestProperties(); + writer.Write(method); + var result = tw.ToString(); + Assert.Contains("def __init__(self,)", result); + Assert.DoesNotContain("This property has a description", result); + Assert.DoesNotContain($"self.{propName}: Optional[str] = {defaultValue}", result); + Assert.DoesNotContain("get_path_parameters(", result); + Assert.Contains("super().__init__()", result); + } + [Fact] + public void WritesConstructorForRequestBuilderWithRequestAdapter() + { + setup(true); + method.Kind = CodeMethodKind.Constructor; + method.IsAsync = false; + var defaultValue = "someVal"; + var propName = "prop_with_default_value"; + parentClass.Kind = CodeClassKind.RequestBuilder; + parentClass.AddProperty(new CodeProperty + { + Name = propName, + DefaultValue = defaultValue, + Kind = CodePropertyKind.UrlTemplate, + Documentation = new() + { + Description = "This property has a description", + }, + Type = new CodeType + { + Name = "string" + } + }); + // AddRequestProperties(); + method.AddParameter(new CodeParameter + { + Name = "requestAdapter", + Kind = CodeParameterKind.RequestAdapter, + Type = new CodeType + { + Name = "requestAdapter" + }, + }); + writer.Write(method); + var result = tw.ToString(); + Assert.DoesNotContain("super().__init__(self)", result); + Assert.Contains("def __init__(self,request_adapter: Optional[RequestAdapter] = None)", result); + Assert.DoesNotContain("This property has a description", result); + Assert.DoesNotContain($"self.{propName}: Optional[str] = {defaultValue}", result); + Assert.DoesNotContain("get_path_parameters(", result); + Assert.Contains("super().__init__(request_adapter, someVal, None)", result); + } + [Fact] + public void WritesConstructorForRequestBuilderWithRequestAdapterAndPathParameters() + { + setup(true); + method.Kind = CodeMethodKind.Constructor; + method.IsAsync = false; + var defaultValue = "someVal"; + var propName = "prop_with_default_value"; + parentClass.Kind = CodeClassKind.RequestBuilder; + parentClass.AddProperty(new CodeProperty + { + Name = propName, + DefaultValue = defaultValue, + Kind = CodePropertyKind.UrlTemplate, + Documentation = new() + { + Description = "This property has a description", + }, + Type = new CodeType + { + Name = "string" + } + }); + // AddRequestProperties(); + method.AddParameter(new CodeParameter + { + Name = "requestAdapter", + Kind = CodeParameterKind.RequestAdapter, + Type = new CodeType + { + Name = "requestAdapter" + }, + }); method.AddParameter(new CodeParameter { Name = "pathParameters", @@ -1480,14 +1593,16 @@ public void WritesConstructor() { Name = "Union[Dict[str, Any], str]", IsNullable = true, - } + }, }); writer.Write(method); var result = tw.ToString(); Assert.DoesNotContain("super().__init__(self)", result); + Assert.Contains("def __init__(self,request_adapter: Optional[RequestAdapter] = None, path_parameters: Optional[Union[Dict[str, Any], str]] = None)", result); Assert.DoesNotContain("This property has a description", result); Assert.DoesNotContain($"self.{propName}: Optional[str] = {defaultValue}", result); Assert.DoesNotContain("get_path_parameters(", result); + Assert.Contains("super().__init__(request_adapter, someVal, path_parameters)", result); } [Fact] public void DoesntWriteConstructorForModelClasses() @@ -1522,6 +1637,56 @@ public void DoesntWriteConstructorForModelClasses() Assert.Contains($"some_property: Optional[str] = None", result); } [Fact] + public void WritesModelClasses() + { + setup(); + method.AddAccessedProperty(); + method.Kind = CodeMethodKind.Constructor; + parentClass.Kind = CodeClassKind.Model; + writer.Write(method); + var result = tw.ToString(); + Assert.DoesNotContain("def __init__()", result); + Assert.DoesNotContain("super().__init__()", result); + Assert.Contains($"some_property: Optional[str] = None", result); + } + [Fact] + public void WritesModelClassesWithDefaultEnumValue() + { + setup(); + method.AddAccessedProperty(); + method.Kind = CodeMethodKind.Constructor; + var defaultValue = "1024x1024"; + var propName = "size"; + var codeEnum = new CodeEnum + { + Name = "pictureSize" + }; + parentClass.Kind = CodeClassKind.Model; + parentClass.AddProperty(new CodeProperty + { + Name = propName, + DefaultValue = defaultValue, + Kind = CodePropertyKind.Custom, + Documentation = new() + { + Description = "This property has a description", + }, + Type = new CodeType + { + Name = codeEnum.Name, + TypeDefinition = codeEnum + + } + }); + writer.Write(method); + var result = tw.ToString(); + Assert.DoesNotContain("def __init__()", result); + Assert.DoesNotContain("super().__init__()", result); + Assert.Contains("has a description", result); + Assert.Contains($"{propName}: Optional[{codeEnum.Name.ToFirstCharacterUpperCase()}] = {codeEnum.Name.ToFirstCharacterUpperCase()}({defaultValue})", result); + Assert.Contains($"some_property: Optional[str] = None", result); + } + [Fact] public void DoesNotWriteConstructorWithDefaultFromComposedType() { setup(); @@ -1663,25 +1828,51 @@ public void WritesApiConstructor() Assert.Contains("self.path_parameters[\"base_url\"] = self.core.base_url", result); } [Fact] + public void WritesBackedModelConstructor() + { + setup(); + parentClass.Kind = CodeClassKind.Model; + method.Kind = CodeMethodKind.Constructor; + parentClass.AddProperty(new CodeProperty + { + Name = "backing_store", + Kind = CodePropertyKind.BackingStore, + Access = AccessModifier.Public, + DefaultValue = "field(default_factory=BackingStoreFactorySingleton(backing_store_factory=None).backing_store_factory.create_backing_store, repr=False)", + Type = new CodeType + { + Name = "BackingStore", + IsExternal = true, + IsNullable = false, + } + }); + var tempWriter = LanguageWriter.GetLanguageWriter(GenerationLanguage.Python, DefaultPath, DefaultName); + tempWriter.SetTextWriter(tw); + tempWriter.Write(method); + var result = tw.ToString(); + Assert.Contains("backing_store: BackingStore = field(default_factory=BackingStoreFactorySingleton(backing_store_factory=None).backing_store_factory.create_backing_store, repr=False)", result); + } + [Fact] public void WritesApiConstructorWithBackingStore() { setup(); + parentClass.Kind = CodeClassKind.Model; method.Kind = CodeMethodKind.ClientConstructor; - var coreProp = parentClass.AddProperty(new CodeProperty + var requestAdapterProp = parentClass.AddProperty(new CodeProperty { - Name = "core", + Name = "request_adapter", Kind = CodePropertyKind.RequestAdapter, Type = new CodeType { - Name = "HttpCore", + Name = "RequestAdapter", IsExternal = true, } }).First(); method.AddParameter(new CodeParameter { - Name = "core", + Name = "request_adapter", Kind = CodeParameterKind.RequestAdapter, - Type = coreProp.Type, + Type = requestAdapterProp.Type, }); var backingStoreParam = new CodeParameter { @@ -1689,8 +1880,9 @@ public void WritesApiConstructorWithBackingStore() Kind = CodeParameterKind.BackingStore, Type = new CodeType { - Name = "BackingStore", + Name = "BackingStoreFactory", IsExternal = true, + IsNullable = true, } }; method.AddParameter(backingStoreParam); @@ -1698,7 +1890,8 @@ public void WritesApiConstructorWithBackingStore() tempWriter.SetTextWriter(tw); tempWriter.Write(method); var result = tw.ToString(); - Assert.Contains("enable_backing_store", result); + Assert.Contains("backing_store: Optional[BackingStoreFactory] = None)", result); + Assert.Contains("self.request_adapter.enable_backing_store(backing_store)", result); } [Fact] public void WritesNameMapperMethod()