From 283a52f616875b91f6ae15e8b515acdf21df79c7 Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Fri, 5 Apr 2024 15:40:31 -0400 Subject: [PATCH 01/13] feat: bind snake case name methods along with original method .net to python --- src/embed_tests/ClassManagerTests.cs | 33 ++++++++++++++ src/embed_tests/TestUtil.cs | 23 ++++++++++ src/runtime/ClassManager.cs | 16 +++++-- src/runtime/Util/Util.cs | 66 +++++++++++++++++++++++++++- 4 files changed, 134 insertions(+), 4 deletions(-) create mode 100644 src/embed_tests/TestUtil.cs diff --git a/src/embed_tests/ClassManagerTests.cs b/src/embed_tests/ClassManagerTests.cs index 72025a28b..ee910c7c1 100644 --- a/src/embed_tests/ClassManagerTests.cs +++ b/src/embed_tests/ClassManagerTests.cs @@ -24,6 +24,39 @@ public void NestedClassDerivingFromParent() var f = new NestedTestContainer().ToPython(); f.GetAttr(nameof(NestedTestContainer.Bar)); } + + #region Snake case naming tests + + public class SnakeCaseNamesTesClass + { + // Purposely long method name to test snake case conversion + public int AddNumbersAndGetHalf(int a, int b) + { + return (a + b) / 2; + } + + public static int AddNumbersAndGetHalf_Static(int a, int b) + { + return (a + b) / 2; + } + } + + [TestCase("AddNumbersAndGetHalf", "add_numbers_and_get_half")] + [TestCase("AddNumbersAndGetHalf_Static", "add_numbers_and_get_half_static")] + public void BindsSnakeCaseClassMethods(string originalMethodName, string snakeCaseMethodName) + { + using var obj = new SnakeCaseNamesTesClass().ToPython(); + using var a = 10.ToPython(); + using var b = 20.ToPython(); + + var camelCaseResult = obj.InvokeMethod(originalMethodName, a, b).As(); + var snakeCaseResult = obj.InvokeMethod(snakeCaseMethodName, a, b).As(); + + Assert.AreEqual(15, camelCaseResult); + Assert.AreEqual(camelCaseResult, snakeCaseResult); + } + + #endregion } public class NestedTestParent diff --git a/src/embed_tests/TestUtil.cs b/src/embed_tests/TestUtil.cs new file mode 100644 index 000000000..0b0c5a84a --- /dev/null +++ b/src/embed_tests/TestUtil.cs @@ -0,0 +1,23 @@ +using NUnit.Framework; + +using Python.Runtime; + +namespace Python.EmbeddingTest +{ + [TestFixture] + public class TestUtil + { + [TestCase("TestCamelCaseString", "test_camel_case_string")] + [TestCase("testCamelCaseString", "test_camel_case_string")] + [TestCase("TestCamelCaseString123 ", "test_camel_case_string123")] + [TestCase("_testCamelCaseString123", "_test_camel_case_string123")] + [TestCase("TestCCS", "test_ccs")] + [TestCase("testCCS", "test_ccs")] + [TestCase("CCSTest", "ccs_test")] + [TestCase("test_CamelCaseString", "test_camel_case_string")] + public void ConvertsNameToSnakeCase(string name, string expected) + { + Assert.AreEqual(expected, name.ToSnakeCase()); + } + } +} diff --git a/src/runtime/ClassManager.cs b/src/runtime/ClassManager.cs index ffe11ec18..8dee3a590 100644 --- a/src/runtime/ClassManager.cs +++ b/src/runtime/ClassManager.cs @@ -448,11 +448,21 @@ private static ClassInfo GetClassInfo(Type type, ClassBase impl) if (name == "__init__" && !impl.HasCustomNew()) continue; - if (!methods.TryGetValue(name, out var methodList)) + List methodList; + var names = new List { name }; + if (!meth.IsSpecialName && !OperatorMethod.IsOperatorMethod(meth)) { - methodList = methods[name] = new List(); + names.Add(name.ToSnakeCase()); + } + foreach (var currentName in names.Distinct()) + { + if (!methods.TryGetValue(currentName, out methodList)) + { + methodList = methods[currentName] = new List(); + } + methodList.Add(meth); } - methodList.Add(meth); + continue; case MemberTypes.Constructor when !impl.HasCustomNew(): diff --git a/src/runtime/Util/Util.cs b/src/runtime/Util/Util.cs index 89f5bdf4c..2ef75ac55 100644 --- a/src/runtime/Util/Util.cs +++ b/src/runtime/Util/Util.cs @@ -1,10 +1,11 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using System.Diagnostics.Contracts; +using System.Globalization; using System.IO; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using System.Text; namespace Python.Runtime { @@ -158,5 +159,68 @@ public static IEnumerable WhereNotNull(this IEnumerable source) if (item is not null) yield return item; } } + + /// + /// Converts the specified name to snake case. + /// + /// + /// Reference: https://github.com/efcore/EFCore.NamingConventions/blob/main/EFCore.NamingConventions/Internal/SnakeCaseNameRewriter.cs + /// + public static string ToSnakeCase(this string name) + { + var builder = new StringBuilder(name.Length + Math.Min(2, name.Length / 5)); + var previousCategory = default(UnicodeCategory?); + + for (var currentIndex = 0; currentIndex < name.Length; currentIndex++) + { + var currentChar = name[currentIndex]; + if (currentChar == '_') + { + builder.Append('_'); + previousCategory = null; + continue; + } + + var currentCategory = char.GetUnicodeCategory(currentChar); + switch (currentCategory) + { + case UnicodeCategory.UppercaseLetter: + case UnicodeCategory.TitlecaseLetter: + if (previousCategory == UnicodeCategory.SpaceSeparator || + previousCategory == UnicodeCategory.LowercaseLetter || + previousCategory != UnicodeCategory.DecimalDigitNumber && + previousCategory != null && + currentIndex > 0 && + currentIndex + 1 < name.Length && + char.IsLower(name[currentIndex + 1])) + { + builder.Append('_'); + } + + currentChar = char.ToLower(currentChar, CultureInfo.InvariantCulture); + break; + + case UnicodeCategory.LowercaseLetter: + case UnicodeCategory.DecimalDigitNumber: + if (previousCategory == UnicodeCategory.SpaceSeparator) + { + builder.Append('_'); + } + break; + + default: + if (previousCategory != null) + { + previousCategory = UnicodeCategory.SpaceSeparator; + } + continue; + } + + builder.Append(currentChar); + previousCategory = currentCategory; + } + + return builder.ToString(); + } } } From 07285dd9d5348cff05256f36631fb31151411eb5 Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Fri, 5 Apr 2024 16:28:57 -0400 Subject: [PATCH 02/13] feat: bind snake case name fields along with original method .net to python --- src/embed_tests/ClassManagerTests.cs | 99 ++++++++++++++++++++++++++-- src/runtime/ClassManager.cs | 17 ++--- 2 files changed, 103 insertions(+), 13 deletions(-) diff --git a/src/embed_tests/ClassManagerTests.cs b/src/embed_tests/ClassManagerTests.cs index ee910c7c1..0f07620ff 100644 --- a/src/embed_tests/ClassManagerTests.cs +++ b/src/embed_tests/ClassManagerTests.cs @@ -1,3 +1,5 @@ +using System; + using NUnit.Framework; using Python.Runtime; @@ -29,7 +31,16 @@ public void NestedClassDerivingFromParent() public class SnakeCaseNamesTesClass { - // Purposely long method name to test snake case conversion + // Purposely long names to test snake case conversion + + public string PublicStringField = "public_string_field"; + public const string PublicConstStringField = "public_const_string_field"; + public readonly string PublicReadonlyStringField = "public_readonly_string_field"; + public static string PublicStaticStringField = "public_static_string_field"; + public static readonly string PublicStaticReadonlyStringField = "public_static_readonly_string_field"; + + public static string SettablePublicStaticStringField = "settable_public_static_string_field"; + public int AddNumbersAndGetHalf(int a, int b) { return (a + b) / 2; @@ -49,11 +60,89 @@ public void BindsSnakeCaseClassMethods(string originalMethodName, string snakeCa using var a = 10.ToPython(); using var b = 20.ToPython(); - var camelCaseResult = obj.InvokeMethod(originalMethodName, a, b).As(); - var snakeCaseResult = obj.InvokeMethod(snakeCaseMethodName, a, b).As(); + var originalMethodResult = obj.InvokeMethod(originalMethodName, a, b).As(); + var snakeCaseMethodResult = obj.InvokeMethod(snakeCaseMethodName, a, b).As(); - Assert.AreEqual(15, camelCaseResult); - Assert.AreEqual(camelCaseResult, snakeCaseResult); + Assert.AreEqual(15, originalMethodResult); + Assert.AreEqual(originalMethodResult, snakeCaseMethodResult); + } + + [TestCase("PublicStringField", "public_string_field")] + [TestCase("PublicConstStringField", "public_const_string_field")] + [TestCase("PublicReadonlyStringField", "public_readonly_string_field")] + [TestCase("PublicStaticStringField", "public_static_string_field")] + [TestCase("PublicStaticReadonlyStringField", "public_static_readonly_string_field")] + public void BindsSnakeCaseClassFields(string originalFieldName, string snakeCaseFieldName) + { + using var obj = new SnakeCaseNamesTesClass().ToPython(); + + var expectedValue = originalFieldName switch + { + "PublicStringField" => "public_string_field", + "PublicConstStringField" => "public_const_string_field", + "PublicReadonlyStringField" => "public_readonly_string_field", + "PublicStaticStringField" => "public_static_string_field", + "PublicStaticReadonlyStringField" => "public_static_readonly_string_field", + _ => throw new ArgumentException("Invalid field name") + }; + + var originalFieldValue = obj.GetAttr(originalFieldName).As(); + var snakeCaseFieldValue = obj.GetAttr(snakeCaseFieldName).As(); + + Assert.AreEqual(expectedValue, originalFieldValue); + Assert.AreEqual(expectedValue, snakeCaseFieldValue); + } + + [Test] + public void CanSetFieldUsingSnakeCaseName() + { + var obj = new SnakeCaseNamesTesClass(); + using var pyObj = obj.ToPython(); + + // Try with the original field name + var newValue1 = "new value 1"; + using var pyNewValue1 = newValue1.ToPython(); + pyObj.SetAttr("PublicStringField", pyNewValue1); + Assert.AreEqual(newValue1, obj.PublicStringField); + + // Try with the snake case field name + var newValue2 = "new value 2"; + using var pyNewValue2 = newValue2.ToPython(); + pyObj.SetAttr("public_string_field", pyNewValue2); + Assert.AreEqual(newValue2, obj.PublicStringField); + } + + [Test] + public void CanSetStaticFieldUsingSnakeCaseName() + { + using (Py.GIL()) + { + var module = PyModule.FromString("module", $@" +from clr import AddReference +AddReference(""Python.EmbeddingTest"") +AddReference(""System"") + +from Python.EmbeddingTest import * + +def SetCamelCaseStaticProperty(value): + ClassManagerTests.SnakeCaseNamesTesClass.PublicStaticStringField = value + +def SetSnakeCaseStaticProperty(value): + ClassManagerTests.SnakeCaseNamesTesClass.public_static_string_field = value + "); + + // Try with the original field name + var newValue1 = "new value 1"; + using var pyNewValue1 = newValue1.ToPython(); + module.InvokeMethod("SetCamelCaseStaticProperty", pyNewValue1); + Assert.AreEqual(newValue1, SnakeCaseNamesTesClass.PublicStaticStringField); + + // Try with the snake case field name + var newValue2 = "new value 2"; + using var pyNewValue2 = newValue2.ToPython(); + module.InvokeMethod("SetSnakeCaseStaticProperty", pyNewValue2); + Assert.AreEqual(newValue2, SnakeCaseNamesTesClass.PublicStaticStringField); + } } #endregion diff --git a/src/runtime/ClassManager.cs b/src/runtime/ClassManager.cs index 8dee3a590..272e4e324 100644 --- a/src/runtime/ClassManager.cs +++ b/src/runtime/ClassManager.cs @@ -448,21 +448,21 @@ private static ClassInfo GetClassInfo(Type type, ClassBase impl) if (name == "__init__" && !impl.HasCustomNew()) continue; - List methodList; - var names = new List { name }; - if (!meth.IsSpecialName && !OperatorMethod.IsOperatorMethod(meth)) + if (!methods.TryGetValue(name, out var methodList)) { - names.Add(name.ToSnakeCase()); + methodList = methods[name] = new List(); } - foreach (var currentName in names.Distinct()) + methodList.Add(meth); + + if (!meth.IsSpecialName && !OperatorMethod.IsOperatorMethod(meth)) { - if (!methods.TryGetValue(currentName, out methodList)) + name = name.ToSnakeCase(); + if (!methods.TryGetValue(name, out methodList)) { - methodList = methods[currentName] = new List(); + methodList = methods[name] = new List(); } methodList.Add(meth); } - continue; case MemberTypes.Constructor when !impl.HasCustomNew(): @@ -514,6 +514,7 @@ private static ClassInfo GetClassInfo(Type type, ClassBase impl) } ob = new FieldObject(fi); ci.members[mi.Name] = ob.AllocObject(); + ci.members[mi.Name.ToSnakeCase()] = ob.AllocObject(); continue; case MemberTypes.Event: From c04c79fbebe11f94a2bcfb75cee3e05c54b06796 Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Fri, 5 Apr 2024 17:44:25 -0400 Subject: [PATCH 03/13] feat: bind snake case name properties along with original method .net to python --- src/embed_tests/ClassManagerTests.cs | 75 ++++++++++++++++++++++++++++ src/runtime/ClassManager.cs | 2 + 2 files changed, 77 insertions(+) diff --git a/src/embed_tests/ClassManagerTests.cs b/src/embed_tests/ClassManagerTests.cs index 0f07620ff..da5205bd6 100644 --- a/src/embed_tests/ClassManagerTests.cs +++ b/src/embed_tests/ClassManagerTests.cs @@ -41,6 +41,10 @@ public class SnakeCaseNamesTesClass public static string SettablePublicStaticStringField = "settable_public_static_string_field"; + public string PublicStringProperty { get; set; } = "public_string_property"; + public static string PublicStaticStringProperty { get; set; } = "public_static_string_property"; + + public int AddNumbersAndGetHalf(int a, int b) { return (a + b) / 2; @@ -145,6 +149,77 @@ def SetSnakeCaseStaticProperty(value): } } + [TestCase("PublicStringProperty", "public_string_property")] + [TestCase("PublicStaticStringProperty", "public_static_string_property")] + public void BindsSnakeCaseClassProperties(string originalPropertyName, string snakeCasePropertyName) + { + using var obj = new SnakeCaseNamesTesClass().ToPython(); + var expectedValue = originalPropertyName switch + { + "PublicStringProperty" => "public_string_property", + "PublicStaticStringProperty" => "public_static_string_property", + _ => throw new ArgumentException("Invalid property name") + }; + + var originalPropertyValue = obj.GetAttr(originalPropertyName).As(); + var snakeCasePropertyValue = obj.GetAttr(snakeCasePropertyName).As(); + + Assert.AreEqual(expectedValue, originalPropertyValue); + Assert.AreEqual(expectedValue, snakeCasePropertyValue); + } + + [Test] + public void CanSetPropertyUsingSnakeCaseName() + { + var obj = new SnakeCaseNamesTesClass(); + using var pyObj = obj.ToPython(); + + // Try with the original property name + var newValue1 = "new value 1"; + using var pyNewValue1 = newValue1.ToPython(); + pyObj.SetAttr("PublicStringProperty", pyNewValue1); + Assert.AreEqual(newValue1, obj.PublicStringProperty); + + // Try with the snake case property name + var newValue2 = "new value 2"; + using var pyNewValue2 = newValue2.ToPython(); + pyObj.SetAttr("public_string_property", pyNewValue2); + Assert.AreEqual(newValue2, obj.PublicStringProperty); + } + + [Test] + public void CanSetStaticPropertyUsingSnakeCaseName() + { + using (Py.GIL()) + { + var module = PyModule.FromString("module", $@" +from clr import AddReference +AddReference(""Python.EmbeddingTest"") +AddReference(""System"") + +from Python.EmbeddingTest import * + +def SetCamelCaseStaticProperty(value): + ClassManagerTests.SnakeCaseNamesTesClass.PublicStaticStringProperty = value + +def SetSnakeCaseStaticProperty(value): + ClassManagerTests.SnakeCaseNamesTesClass.public_static_string_property = value + "); + + // Try with the original property name + var newValue1 = "new value 1"; + using var pyNewValue1 = newValue1.ToPython(); + module.InvokeMethod("SetCamelCaseStaticProperty", pyNewValue1); + Assert.AreEqual(newValue1, SnakeCaseNamesTesClass.PublicStaticStringProperty); + + // Try with the snake case property name + var newValue2 = "new value 2"; + using var pyNewValue2 = newValue2.ToPython(); + module.InvokeMethod("SetSnakeCaseStaticProperty", pyNewValue2); + Assert.AreEqual(newValue2, SnakeCaseNamesTesClass.PublicStaticStringProperty); + } + } + #endregion } diff --git a/src/runtime/ClassManager.cs b/src/runtime/ClassManager.cs index 272e4e324..db6344fb6 100644 --- a/src/runtime/ClassManager.cs +++ b/src/runtime/ClassManager.cs @@ -504,6 +504,7 @@ private static ClassInfo GetClassInfo(Type type, ClassBase impl) ob = new PropertyObject(pi); ci.members[pi.Name] = ob.AllocObject(); + ci.members[pi.Name.ToSnakeCase()] = ob.AllocObject(); continue; case MemberTypes.Field: @@ -514,6 +515,7 @@ private static ClassInfo GetClassInfo(Type type, ClassBase impl) } ob = new FieldObject(fi); ci.members[mi.Name] = ob.AllocObject(); + // TODO: Upper-case constants? ci.members[mi.Name.ToSnakeCase()] = ob.AllocObject(); continue; From 5ddc78c8ae0f42379fb5979c88e499441073889f Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Fri, 5 Apr 2024 17:59:40 -0400 Subject: [PATCH 04/13] feat: bind snake case name events along with original method .net to python --- src/embed_tests/ClassManagerTests.cs | 82 +++++++++++++++++++++++++++- src/runtime/ClassManager.cs | 1 + 2 files changed, 81 insertions(+), 2 deletions(-) diff --git a/src/embed_tests/ClassManagerTests.cs b/src/embed_tests/ClassManagerTests.cs index da5205bd6..f765d44fb 100644 --- a/src/embed_tests/ClassManagerTests.cs +++ b/src/embed_tests/ClassManagerTests.cs @@ -44,6 +44,18 @@ public class SnakeCaseNamesTesClass public string PublicStringProperty { get; set; } = "public_string_property"; public static string PublicStaticStringProperty { get; set; } = "public_static_string_property"; + public event EventHandler PublicStringEvent; + public static event EventHandler PublicStaticStringEvent; + + public void InvokePublicStringEvent(string value) + { + PublicStringEvent?.Invoke(this, value); + } + + public static void InvokePublicStaticStringEvent(string value) + { + PublicStaticStringEvent?.Invoke(null, value); + } public int AddNumbersAndGetHalf(int a, int b) { @@ -124,7 +136,6 @@ public void CanSetStaticFieldUsingSnakeCaseName() var module = PyModule.FromString("module", $@" from clr import AddReference AddReference(""Python.EmbeddingTest"") -AddReference(""System"") from Python.EmbeddingTest import * @@ -195,7 +206,6 @@ public void CanSetStaticPropertyUsingSnakeCaseName() var module = PyModule.FromString("module", $@" from clr import AddReference AddReference(""Python.EmbeddingTest"") -AddReference(""System"") from Python.EmbeddingTest import * @@ -220,6 +230,74 @@ def SetSnakeCaseStaticProperty(value): } } + [TestCase("PublicStringEvent")] + [TestCase("public_string_event")] + public void BindsSnakeCaseEvents(string eventName) + { + var obj = new SnakeCaseNamesTesClass(); + using var pyObj = obj.ToPython(); + + var value = ""; + var eventHandler = new EventHandler((sender, arg) => { value = arg; }); + + // Try with the original event name + using (Py.GIL()) + { + var module = PyModule.FromString("module", $@" +def AddEventHandler(obj, handler): + obj.{eventName} += handler + +def RemoveEventHandler(obj, handler): + obj.{eventName} -= handler + "); + + using var pyEventHandler = eventHandler.ToPython(); + + module.InvokeMethod("AddEventHandler", pyObj, pyEventHandler); + obj.InvokePublicStringEvent("new value 1"); + Assert.AreEqual("new value 1", value); + + module.InvokeMethod("RemoveEventHandler", pyObj, pyEventHandler); + obj.InvokePublicStringEvent("new value 2"); + Assert.AreEqual("new value 1", value); // Should not have changed + } + } + + [TestCase("PublicStaticStringEvent")] + [TestCase("public_static_string_event")] + public void BindsSnakeCaseStaticEvents(string eventName) + { + var value = ""; + var eventHandler = new EventHandler((sender, arg) => { value = arg; }); + + // Try with the original event name + using (Py.GIL()) + { + var module = PyModule.FromString("module", $@" +from clr import AddReference +AddReference(""Python.EmbeddingTest"") + +from Python.EmbeddingTest import * + +def AddEventHandler(handler): + ClassManagerTests.SnakeCaseNamesTesClass.{eventName} += handler + +def RemoveEventHandler(handler): + ClassManagerTests.SnakeCaseNamesTesClass.{eventName} -= handler + "); + + using var pyEventHandler = eventHandler.ToPython(); + + module.InvokeMethod("AddEventHandler", pyEventHandler); + SnakeCaseNamesTesClass.InvokePublicStaticStringEvent("new value 1"); + Assert.AreEqual("new value 1", value); + + module.InvokeMethod("RemoveEventHandler", pyEventHandler); + SnakeCaseNamesTesClass.InvokePublicStaticStringEvent("new value 2"); + Assert.AreEqual("new value 1", value); // Should not have changed + } + } + #endregion } diff --git a/src/runtime/ClassManager.cs b/src/runtime/ClassManager.cs index db6344fb6..b6febfe3a 100644 --- a/src/runtime/ClassManager.cs +++ b/src/runtime/ClassManager.cs @@ -529,6 +529,7 @@ private static ClassInfo GetClassInfo(Type type, ClassBase impl) ? new EventBinding(ei) : new EventObject(ei); ci.members[ei.Name] = ob.AllocObject(); + ci.members[ei.Name.ToSnakeCase()] = ob.AllocObject(); continue; case MemberTypes.NestedType: From 6757e1fd24ad09013f3e286f73d43fcbe41d0f11 Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Mon, 8 Apr 2024 12:34:55 -0400 Subject: [PATCH 05/13] feat: bind snake case name methods named parameters along with original method .net to python --- src/embed_tests/ClassManagerTests.cs | 102 +++++++++++++++++++++++++++ src/runtime/ClassManager.cs | 30 ++++++-- src/runtime/MethodBinder.cs | 35 +++++++-- src/runtime/Types/MethodObject.cs | 5 +- 4 files changed, 159 insertions(+), 13 deletions(-) diff --git a/src/embed_tests/ClassManagerTests.cs b/src/embed_tests/ClassManagerTests.cs index f765d44fb..2c3bcf246 100644 --- a/src/embed_tests/ClassManagerTests.cs +++ b/src/embed_tests/ClassManagerTests.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using System.Linq; using NUnit.Framework; @@ -66,6 +68,20 @@ public static int AddNumbersAndGetHalf_Static(int a, int b) { return (a + b) / 2; } + + public string JoinToString(string thisIsAStringParameter, + char thisIsACharParameter, + int thisIsAnIntParameter, + float thisIsAFloatParameter, + double thisIsADoubleParameter, + decimal thisIsADecimalParameter, + bool thisIsABoolParameter, + DateTime thisIsADateTimeParameter) + { + // Join all parameters into a single string separated by "-" + return string.Join("-", thisIsAStringParameter, thisIsACharParameter, thisIsAnIntParameter, thisIsAFloatParameter, + thisIsADoubleParameter, thisIsADecimalParameter, thisIsABoolParameter, string.Format("{0:MMddyyyy}", thisIsADateTimeParameter)); + } } [TestCase("AddNumbersAndGetHalf", "add_numbers_and_get_half")] @@ -298,6 +314,92 @@ def RemoveEventHandler(handler): } } + private static IEnumerable SnakeCasedNamedArgsTestCases + { + get + { + var stringParam = "string"; + var charParam = 'c'; + var intParam = 1; + var floatParam = 2.0f; + var doubleParam = 3.0; + var decimalParam = 4.0m; + var boolParam = true; + var dateTimeParam = new DateTime(2013, 01, 05); + + // 1. All kwargs: + + // 1.1. Original method name: + var args = Array.Empty(); + var namedArgs = new Dictionary() + { + { "thisIsAStringParameter", stringParam }, + { "thisIsACharParameter", charParam }, + { "thisIsAnIntParameter", intParam }, + { "thisIsAFloatParameter", floatParam }, + { "thisIsADoubleParameter", doubleParam }, + { "thisIsADecimalParameter", decimalParam }, + { "thisIsABoolParameter", boolParam }, + { "thisIsADateTimeParameter", dateTimeParam } + }; + yield return new TestCaseData("JoinToString", args, namedArgs); + + // 1.2. Snake-cased method name: + namedArgs = new Dictionary() + { + { "this_is_a_string_parameter", stringParam }, + { "this_is_a_char_parameter", charParam }, + { "this_is_an_int_parameter", intParam }, + { "this_is_a_float_parameter", floatParam }, + { "this_is_a_double_parameter", doubleParam }, + { "this_is_a_decimal_parameter", decimalParam }, + { "this_is_a_bool_parameter", boolParam }, + { "this_is_a_date_time_parameter", dateTimeParam } + }; + yield return new TestCaseData("join_to_string", args, namedArgs); + + // 2. Some args and some kwargs: + + // 2.1. Original method name: + args = new object[] { stringParam, charParam, intParam, floatParam }; + namedArgs = new Dictionary() + { + { "thisIsADoubleParameter", doubleParam }, + { "thisIsADecimalParameter", decimalParam }, + { "thisIsABoolParameter", boolParam }, + { "thisIsADateTimeParameter", dateTimeParam } + }; + yield return new TestCaseData("JoinToString", args, namedArgs); + + // 2.2. Snake-cased method name: + namedArgs = new Dictionary() + { + { "this_is_a_double_parameter", doubleParam }, + { "this_is_a_decimal_parameter", decimalParam }, + { "this_is_a_bool_parameter", boolParam }, + { "this_is_a_date_time_parameter", dateTimeParam } + }; + yield return new TestCaseData("join_to_string", args, namedArgs); + } + } + + [TestCaseSource(nameof(SnakeCasedNamedArgsTestCases))] + public void CanCallSnakeCasedMethodWithSnakeCasedNamedArguments(string methodName, object[] args, Dictionary namedArgs) + { + using var obj = new SnakeCaseNamesTesClass().ToPython(); + + var pyArgs = args.Select(a => a.ToPython()).ToArray(); + using var pyNamedArgs = new PyDict(); + foreach (var (key, value) in namedArgs) + { + pyNamedArgs[key] = value.ToPython(); + } + + var result = obj.InvokeMethod(methodName, pyArgs, pyNamedArgs).As(); + + Assert.AreEqual("string-c-1-2-3-4.0-True-01052013", result); + } + #endregion } diff --git a/src/runtime/ClassManager.cs b/src/runtime/ClassManager.cs index b6febfe3a..bcb8d89b1 100644 --- a/src/runtime/ClassManager.cs +++ b/src/runtime/ClassManager.cs @@ -336,7 +336,7 @@ internal static bool ShouldBindEvent(EventInfo ei) private static ClassInfo GetClassInfo(Type type, ClassBase impl) { var ci = new ClassInfo(); - var methods = new Dictionary>(); + var methods = new Dictionary(); MethodInfo meth; ExtensionType ob; string name; @@ -450,7 +450,7 @@ private static ClassInfo GetClassInfo(Type type, ClassBase impl) if (!methods.TryGetValue(name, out var methodList)) { - methodList = methods[name] = new List(); + methodList = methods[name] = new MethodOverloads(true); } methodList.Add(meth); @@ -459,7 +459,7 @@ private static ClassInfo GetClassInfo(Type type, ClassBase impl) name = name.ToSnakeCase(); if (!methods.TryGetValue(name, out methodList)) { - methodList = methods[name] = new List(); + methodList = methods[name] = new MethodOverloads(false); } methodList.Add(meth); } @@ -475,7 +475,7 @@ private static ClassInfo GetClassInfo(Type type, ClassBase impl) name = "__init__"; if (!methods.TryGetValue(name, out methodList)) { - methodList = methods[name] = new List(); + methodList = methods[name] = new MethodOverloads(true); } methodList.Add(ctor); continue; @@ -550,9 +550,9 @@ private static ClassInfo GetClassInfo(Type type, ClassBase impl) foreach (var iter in methods) { name = iter.Key; - var mlist = iter.Value.ToArray(); + var mlist = iter.Value.Methods.ToArray(); - ob = new MethodObject(type, name, mlist); + ob = new MethodObject(type, name, mlist, isOriginal: iter.Value.IsOriginal); ci.members[name] = ob.AllocObject(); if (mlist.Any(OperatorMethod.IsOperatorMethod)) { @@ -604,6 +604,24 @@ internal ClassInfo() indexer = null; } } + + private class MethodOverloads + { + public List Methods { get; } + + public bool IsOriginal { get; } + + public MethodOverloads(bool original = true) + { + Methods = new List(); + IsOriginal = original; + } + + public void Add(MethodBase method) + { + Methods.Add(method); + } + } } } diff --git a/src/runtime/MethodBinder.cs b/src/runtime/MethodBinder.cs index 352073170..db6239523 100644 --- a/src/runtime/MethodBinder.cs +++ b/src/runtime/MethodBinder.cs @@ -40,10 +40,15 @@ public int Count } internal void AddMethod(MethodBase m) + { + AddMethod(m, true); + } + + internal void AddMethod(MethodBase m, bool isOriginal) { // we added a new method so we have to re sort the method list init = false; - list.Add(new MethodInformation(m, m.GetParameters())); + list.Add(new MethodInformation(m, m.GetParameters(), isOriginal)); } /// @@ -118,7 +123,7 @@ internal static MethodInfo[] MatchParameters(MethodBase[] mi, Type[] tp) return result.ToArray(); } - // Given a generic method and the argsTypes previously matched with it, + // Given a generic method and the argsTypes previously matched with it, // generate the matching method internal static MethodInfo ResolveGenericMethod(MethodInfo method, Object[] args) { @@ -474,11 +479,15 @@ internal Binding Bind(BorrowedReference inst, BorrowedReference args, BorrowedRe // Must be done after IsOperator section int clrArgCount = pi.Length; + var parametersSnakeCasedNames = kwArgDict == null || methodInformation.IsOriginal + ? null + : pi.Select(p => p.Name.ToSnakeCase()).ToArray(); if (CheckMethodArgumentsMatch(clrArgCount, pyArgCount, kwArgDict, pi, + parametersSnakeCasedNames, out bool paramsArray, out ArrayList defaultArgList)) { @@ -497,7 +506,12 @@ internal Binding Bind(BorrowedReference inst, BorrowedReference args, BorrowedRe object arg; // Python -> Clr argument // Check our KWargs for this parameter - bool hasNamedParam = kwArgDict == null ? false : kwArgDict.TryGetValue(parameter.Name, out tempPyObject); + var hasNamedParam = false; + if (kwArgDict != null) + { + var paramName = methodInformation.IsOriginal ? parameter.Name : parametersSnakeCasedNames[paramIndex]; + hasNamedParam = kwArgDict.TryGetValue(paramName, out tempPyObject); + } if(tempPyObject != null) { op = tempPyObject; @@ -766,6 +780,7 @@ private bool CheckMethodArgumentsMatch(int clrArgCount, int pyArgCount, Dictionary kwargDict, ParameterInfo[] parameterInfo, + string[] parametersSnakeCasedNames, out bool paramsArray, out ArrayList defaultArgList) { @@ -788,7 +803,9 @@ private bool CheckMethodArgumentsMatch(int clrArgCount, { // If the method doesn't have all of these kw args, it is not a match // Otherwise just continue on to see if it is a match - if (!kwargDict.All(x => parameterInfo.Any(pi => x.Key == pi.Name))) + if (!kwargDict.All(x => parametersSnakeCasedNames == null + ? parameterInfo.Any(pi => x.Key == pi.Name) + : parametersSnakeCasedNames.Any(paramName => x.Key == paramName))) { return false; } @@ -808,7 +825,7 @@ private bool CheckMethodArgumentsMatch(int clrArgCount, defaultArgList = new ArrayList(); for (var v = pyArgCount; v < clrArgCount && match; v++) { - if (kwargDict != null && kwargDict.ContainsKey(parameterInfo[v].Name)) + if (kwargDict != null && kwargDict.ContainsKey(parametersSnakeCasedNames == null ? parameterInfo[v].Name : parametersSnakeCasedNames[v])) { // we have a keyword argument for this parameter, // no need to check for a default parameter, but put a null @@ -977,10 +994,18 @@ internal class MethodInformation public ParameterInfo[] ParameterInfo { get; } + public bool IsOriginal { get; } + public MethodInformation(MethodBase methodBase, ParameterInfo[] parameterInfo) + : this(methodBase, parameterInfo, true) + { + } + + public MethodInformation(MethodBase methodBase, ParameterInfo[] parameterInfo, bool isOriginal) { MethodBase = methodBase; ParameterInfo = parameterInfo; + IsOriginal = isOriginal; } public override string ToString() diff --git a/src/runtime/Types/MethodObject.cs b/src/runtime/Types/MethodObject.cs index 36504482c..32c832a88 100644 --- a/src/runtime/Types/MethodObject.cs +++ b/src/runtime/Types/MethodObject.cs @@ -28,7 +28,8 @@ internal class MethodObject : ExtensionType internal PyString? doc; internal MaybeType type; - public MethodObject(MaybeType type, string name, MethodBase[] info, bool allow_threads = MethodBinder.DefaultAllowThreads) + public MethodObject(MaybeType type, string name, MethodBase[] info, bool allow_threads = MethodBinder.DefaultAllowThreads, + bool isOriginal = true) { this.type = type; this.name = name; @@ -37,7 +38,7 @@ public MethodObject(MaybeType type, string name, MethodBase[] info, bool allow_t foreach (MethodBase item in info) { this.infoList.Add(item); - binder.AddMethod(item); + binder.AddMethod(item, isOriginal); if (item.IsStatic) { this.is_static = true; From 6a5e57508d303ff1629373ab3be173f1e005d972 Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Mon, 8 Apr 2024 15:18:25 -0400 Subject: [PATCH 06/13] feat: bind constants as upper-case snake-case additional: add enums unit tests --- src/embed_tests/ClassManagerTests.cs | 69 ++++++++++++++++++++++++++-- src/runtime/ClassManager.cs | 9 +++- src/runtime/MethodBinder.cs | 40 +++++++++------- 3 files changed, 96 insertions(+), 22 deletions(-) diff --git a/src/embed_tests/ClassManagerTests.cs b/src/embed_tests/ClassManagerTests.cs index 2c3bcf246..9df8fe821 100644 --- a/src/embed_tests/ClassManagerTests.cs +++ b/src/embed_tests/ClassManagerTests.cs @@ -31,6 +31,13 @@ public void NestedClassDerivingFromParent() #region Snake case naming tests + public enum SnakeCaseEnum + { + EnumValue1, + EnumValue2, + EnumValue3 + } + public class SnakeCaseNamesTesClass { // Purposely long names to test snake case conversion @@ -49,6 +56,8 @@ public class SnakeCaseNamesTesClass public event EventHandler PublicStringEvent; public static event EventHandler PublicStaticStringEvent; + public SnakeCaseEnum EnumValue = SnakeCaseEnum.EnumValue2; + public void InvokePublicStringEvent(string value) { PublicStringEvent?.Invoke(this, value); @@ -100,10 +109,11 @@ public void BindsSnakeCaseClassMethods(string originalMethodName, string snakeCa } [TestCase("PublicStringField", "public_string_field")] - [TestCase("PublicConstStringField", "public_const_string_field")] - [TestCase("PublicReadonlyStringField", "public_readonly_string_field")] [TestCase("PublicStaticStringField", "public_static_string_field")] - [TestCase("PublicStaticReadonlyStringField", "public_static_readonly_string_field")] + // Constants + [TestCase("PublicConstStringField", "PUBLIC_CONST_STRING_FIELD")] + [TestCase("PublicReadonlyStringField", "PUBLIC_READONLY_STRING_FIELD")] + [TestCase("PublicStaticReadonlyStringField", "PUBLIC_STATIC_READONLY_STRING_FIELD")] public void BindsSnakeCaseClassFields(string originalFieldName, string snakeCaseFieldName) { using var obj = new SnakeCaseNamesTesClass().ToPython(); @@ -400,6 +410,59 @@ public void CanCallSnakeCasedMethodWithSnakeCasedNamedArguments(string methodNam Assert.AreEqual("string-c-1-2-3-4.0-True-01052013", result); } + [Test] + public void BindsEnumValuesWithPEPStyleNaming([Values] bool useSnakeCased) + { + using (Py.GIL()) + { + var module = PyModule.FromString("module", $@" +from clr import AddReference +AddReference(""Python.EmbeddingTest"") + +from Python.EmbeddingTest import * + +def SetEnumValue1(obj): + obj.EnumValue = ClassManagerTests.SnakeCaseEnum.EnumValue1 + +def SetEnumValue2(obj): + obj.EnumValue = ClassManagerTests.SnakeCaseEnum.EnumValue2 + +def SetEnumValue3(obj): + obj.EnumValue = ClassManagerTests.SnakeCaseEnum.EnumValue3 + +def SetEnumValue1SnakeCase(obj): + obj.enum_value = ClassManagerTests.SnakeCaseEnum.ENUM_VALUE1 + +def SetEnumValue2SnakeCase(obj): + obj.enum_value = ClassManagerTests.SnakeCaseEnum.ENUM_VALUE2 + +def SetEnumValue3SnakeCase(obj): + obj.enum_value = ClassManagerTests.SnakeCaseEnum.ENUM_VALUE3 + "); + + using var obj = new SnakeCaseNamesTesClass().ToPython(); + + if (useSnakeCased) + { + module.InvokeMethod("SetEnumValue1SnakeCase", obj); + Assert.AreEqual(SnakeCaseEnum.EnumValue1, obj.GetAttr("enum_value").As()); + module.InvokeMethod("SetEnumValue2SnakeCase", obj); + Assert.AreEqual(SnakeCaseEnum.EnumValue2, obj.GetAttr("enum_value").As()); + module.InvokeMethod("SetEnumValue3SnakeCase", obj); + Assert.AreEqual(SnakeCaseEnum.EnumValue3, obj.GetAttr("enum_value").As()); + } + else + { + module.InvokeMethod("SetEnumValue1", obj); + Assert.AreEqual(SnakeCaseEnum.EnumValue1, obj.GetAttr("EnumValue").As()); + module.InvokeMethod("SetEnumValue2", obj); + Assert.AreEqual(SnakeCaseEnum.EnumValue2, obj.GetAttr("EnumValue").As()); + module.InvokeMethod("SetEnumValue3", obj); + Assert.AreEqual(SnakeCaseEnum.EnumValue3, obj.GetAttr("EnumValue").As()); + } + } + } + #endregion } diff --git a/src/runtime/ClassManager.cs b/src/runtime/ClassManager.cs index bcb8d89b1..7058b1692 100644 --- a/src/runtime/ClassManager.cs +++ b/src/runtime/ClassManager.cs @@ -515,8 +515,13 @@ private static ClassInfo GetClassInfo(Type type, ClassBase impl) } ob = new FieldObject(fi); ci.members[mi.Name] = ob.AllocObject(); - // TODO: Upper-case constants? - ci.members[mi.Name.ToSnakeCase()] = ob.AllocObject(); + + var pepName = fi.Name.ToSnakeCase(); + if (fi.IsLiteral || fi.IsInitOnly) + { + pepName = pepName.ToUpper(); + } + ci.members[pepName] = ob.AllocObject(); continue; case MemberTypes.Event: diff --git a/src/runtime/MethodBinder.cs b/src/runtime/MethodBinder.cs index db6239523..b36d21224 100644 --- a/src/runtime/MethodBinder.cs +++ b/src/runtime/MethodBinder.cs @@ -452,9 +452,9 @@ internal Binding Bind(BorrowedReference inst, BorrowedReference args, BorrowedRe // Relevant method variables var mi = methodInformation.MethodBase; var pi = methodInformation.ParameterInfo; + var paramNames = methodInformation.ParametersNames; int pyArgCount = (int)Runtime.PyTuple_Size(args); - // Special case for operators bool isOperator = OperatorMethod.IsOperatorMethod(mi); // Binary operator methods will have 2 CLR args but only one Python arg @@ -479,15 +479,12 @@ internal Binding Bind(BorrowedReference inst, BorrowedReference args, BorrowedRe // Must be done after IsOperator section int clrArgCount = pi.Length; - var parametersSnakeCasedNames = kwArgDict == null || methodInformation.IsOriginal - ? null - : pi.Select(p => p.Name.ToSnakeCase()).ToArray(); if (CheckMethodArgumentsMatch(clrArgCount, pyArgCount, kwArgDict, pi, - parametersSnakeCasedNames, + paramNames, out bool paramsArray, out ArrayList defaultArgList)) { @@ -506,13 +503,8 @@ internal Binding Bind(BorrowedReference inst, BorrowedReference args, BorrowedRe object arg; // Python -> Clr argument // Check our KWargs for this parameter - var hasNamedParam = false; - if (kwArgDict != null) - { - var paramName = methodInformation.IsOriginal ? parameter.Name : parametersSnakeCasedNames[paramIndex]; - hasNamedParam = kwArgDict.TryGetValue(paramName, out tempPyObject); - } - if(tempPyObject != null) + bool hasNamedParam = kwArgDict == null ? false : kwArgDict.TryGetValue(paramNames[paramIndex], out tempPyObject); + if (tempPyObject != null) { op = tempPyObject; } @@ -776,11 +768,16 @@ static BorrowedReference HandleParamsArray(BorrowedReference args, int arrayStar /// This helper method will perform an initial check to determine if we found a matching /// method based on its parameters count and type /// + /// + /// We required both the parameters info and the parameters names to perform this check. + /// The CLR method parameters info is required to match the parameters count and type. + /// The names are required to perform an accurate match, since the method can be the snake-cased version. + /// private bool CheckMethodArgumentsMatch(int clrArgCount, int pyArgCount, Dictionary kwargDict, ParameterInfo[] parameterInfo, - string[] parametersSnakeCasedNames, + string[] parameterNames, out bool paramsArray, out ArrayList defaultArgList) { @@ -803,9 +800,7 @@ private bool CheckMethodArgumentsMatch(int clrArgCount, { // If the method doesn't have all of these kw args, it is not a match // Otherwise just continue on to see if it is a match - if (!kwargDict.All(x => parametersSnakeCasedNames == null - ? parameterInfo.Any(pi => x.Key == pi.Name) - : parametersSnakeCasedNames.Any(paramName => x.Key == paramName))) + if (!kwargDict.All(x => parameterNames.Any(paramName => x.Key == paramName))) { return false; } @@ -825,7 +820,7 @@ private bool CheckMethodArgumentsMatch(int clrArgCount, defaultArgList = new ArrayList(); for (var v = pyArgCount; v < clrArgCount && match; v++) { - if (kwargDict != null && kwargDict.ContainsKey(parametersSnakeCasedNames == null ? parameterInfo[v].Name : parametersSnakeCasedNames[v])) + if (kwargDict != null && kwargDict.ContainsKey(parameterNames[v])) { // we have a keyword argument for this parameter, // no need to check for a default parameter, but put a null @@ -996,6 +991,8 @@ internal class MethodInformation public bool IsOriginal { get; } + public string[] ParametersNames { get; } + public MethodInformation(MethodBase methodBase, ParameterInfo[] parameterInfo) : this(methodBase, parameterInfo, true) { @@ -1006,6 +1003,15 @@ public MethodInformation(MethodBase methodBase, ParameterInfo[] parameterInfo, b MethodBase = methodBase; ParameterInfo = parameterInfo; IsOriginal = isOriginal; + + if (isOriginal) + { + ParametersNames = ParameterInfo.Select(pi => pi.Name).ToArray(); + } + else + { + ParametersNames = ParameterInfo.Select(pi => pi.Name.ToSnakeCase()).ToArray(); + } } public override string ToString() From 4fbf8910aca1d8211bff2f738c0889a0a2873fc4 Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Wed, 10 Apr 2024 08:52:22 -0400 Subject: [PATCH 07/13] feat: sabe parameter names along with method information in method binder --- src/runtime/MethodBinder.cs | 15 ++++++--------- src/runtime/Util/Util.cs | 2 +- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/src/runtime/MethodBinder.cs b/src/runtime/MethodBinder.cs index b36d21224..38cb0f603 100644 --- a/src/runtime/MethodBinder.cs +++ b/src/runtime/MethodBinder.cs @@ -985,13 +985,15 @@ internal virtual NewReference Invoke(BorrowedReference inst, BorrowedReference a [Serializable] internal class MethodInformation { + private Lazy _parametersNames; + public MethodBase MethodBase { get; } public ParameterInfo[] ParameterInfo { get; } public bool IsOriginal { get; } - public string[] ParametersNames { get; } + public string[] ParametersNames { get { return _parametersNames.Value; } } public MethodInformation(MethodBase methodBase, ParameterInfo[] parameterInfo) : this(methodBase, parameterInfo, true) @@ -1004,14 +1006,9 @@ public MethodInformation(MethodBase methodBase, ParameterInfo[] parameterInfo, b ParameterInfo = parameterInfo; IsOriginal = isOriginal; - if (isOriginal) - { - ParametersNames = ParameterInfo.Select(pi => pi.Name).ToArray(); - } - else - { - ParametersNames = ParameterInfo.Select(pi => pi.Name.ToSnakeCase()).ToArray(); - } + _parametersNames = new Lazy(() => IsOriginal + ? ParameterInfo.Select(pi => pi.Name).ToArray() + : ParameterInfo.Select(pi => pi.Name.ToSnakeCase()).ToArray()); } public override string ToString() diff --git a/src/runtime/Util/Util.cs b/src/runtime/Util/Util.cs index 2ef75ac55..6aa398c91 100644 --- a/src/runtime/Util/Util.cs +++ b/src/runtime/Util/Util.cs @@ -9,7 +9,7 @@ namespace Python.Runtime { - internal static class Util + public static class Util { internal const string UnstableApiMessage = "This API is unstable, and might be changed or removed in the next minor release"; From ac0102b09b80e95e2ff373f389f540d4437e9cb6 Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Wed, 10 Apr 2024 09:29:33 -0400 Subject: [PATCH 08/13] Bump version to 2.0.30 --- src/perf_tests/Python.PerformanceTests.csproj | 4 ++-- src/runtime/Properties/AssemblyInfo.cs | 4 ++-- src/runtime/Python.Runtime.csproj | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/perf_tests/Python.PerformanceTests.csproj b/src/perf_tests/Python.PerformanceTests.csproj index b9533b460..2809f2b35 100644 --- a/src/perf_tests/Python.PerformanceTests.csproj +++ b/src/perf_tests/Python.PerformanceTests.csproj @@ -13,7 +13,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + compile @@ -25,7 +25,7 @@ - + diff --git a/src/runtime/Properties/AssemblyInfo.cs b/src/runtime/Properties/AssemblyInfo.cs index 896f2ba0e..e4fe802f6 100644 --- a/src/runtime/Properties/AssemblyInfo.cs +++ b/src/runtime/Properties/AssemblyInfo.cs @@ -4,5 +4,5 @@ [assembly: InternalsVisibleTo("Python.EmbeddingTest, PublicKey=00240000048000009400000006020000002400005253413100040000110000005ffd8f49fb44ab0641b3fd8d55e749f716e6dd901032295db641eb98ee46063cbe0d4a1d121ef0bc2af95f8a7438d7a80a3531316e6b75c2dae92fb05a99f03bf7e0c03980e1c3cfb74ba690aca2f3339ef329313bcc5dccced125a4ffdc4531dcef914602cd5878dc5fbb4d4c73ddfbc133f840231343e013762884d6143189")] [assembly: InternalsVisibleTo("Python.Test, PublicKey=00240000048000009400000006020000002400005253413100040000110000005ffd8f49fb44ab0641b3fd8d55e749f716e6dd901032295db641eb98ee46063cbe0d4a1d121ef0bc2af95f8a7438d7a80a3531316e6b75c2dae92fb05a99f03bf7e0c03980e1c3cfb74ba690aca2f3339ef329313bcc5dccced125a4ffdc4531dcef914602cd5878dc5fbb4d4c73ddfbc133f840231343e013762884d6143189")] -[assembly: AssemblyVersion("2.0.29")] -[assembly: AssemblyFileVersion("2.0.29")] +[assembly: AssemblyVersion("2.0.30")] +[assembly: AssemblyFileVersion("2.0.30")] diff --git a/src/runtime/Python.Runtime.csproj b/src/runtime/Python.Runtime.csproj index 6704bd978..bbb9613a6 100644 --- a/src/runtime/Python.Runtime.csproj +++ b/src/runtime/Python.Runtime.csproj @@ -5,7 +5,7 @@ Python.Runtime Python.Runtime QuantConnect.pythonnet - 2.0.29 + 2.0.30 false LICENSE https://github.com/pythonnet/pythonnet From 71f1d353e02a1bdfe72a329dc9a5f05d9d2cc949 Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Wed, 10 Apr 2024 10:45:55 -0400 Subject: [PATCH 09/13] feat: not uppercasing readonly fields --- src/embed_tests/ClassManagerTests.cs | 4 ++-- src/runtime/ClassManager.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/embed_tests/ClassManagerTests.cs b/src/embed_tests/ClassManagerTests.cs index 9df8fe821..7ba56b59c 100644 --- a/src/embed_tests/ClassManagerTests.cs +++ b/src/embed_tests/ClassManagerTests.cs @@ -110,10 +110,10 @@ public void BindsSnakeCaseClassMethods(string originalMethodName, string snakeCa [TestCase("PublicStringField", "public_string_field")] [TestCase("PublicStaticStringField", "public_static_string_field")] + [TestCase("PublicReadonlyStringField", "public_readonly_string_field")] + [TestCase("PublicStaticReadonlyStringField", "public_static_readonly_string_field")] // Constants [TestCase("PublicConstStringField", "PUBLIC_CONST_STRING_FIELD")] - [TestCase("PublicReadonlyStringField", "PUBLIC_READONLY_STRING_FIELD")] - [TestCase("PublicStaticReadonlyStringField", "PUBLIC_STATIC_READONLY_STRING_FIELD")] public void BindsSnakeCaseClassFields(string originalFieldName, string snakeCaseFieldName) { using var obj = new SnakeCaseNamesTesClass().ToPython(); diff --git a/src/runtime/ClassManager.cs b/src/runtime/ClassManager.cs index 7058b1692..60d0ce467 100644 --- a/src/runtime/ClassManager.cs +++ b/src/runtime/ClassManager.cs @@ -517,7 +517,7 @@ private static ClassInfo GetClassInfo(Type type, ClassBase impl) ci.members[mi.Name] = ob.AllocObject(); var pepName = fi.Name.ToSnakeCase(); - if (fi.IsLiteral || fi.IsInitOnly) + if (fi.IsLiteral) { pepName = pepName.ToUpper(); } From ebaa532b2f8bc1f4acb8d2bbf985ef9af9f08035 Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Wed, 10 Apr 2024 11:35:44 -0400 Subject: [PATCH 10/13] Avoid duplicating method bindings --- src/runtime/ClassManager.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/runtime/ClassManager.cs b/src/runtime/ClassManager.cs index 60d0ce467..f431a8d63 100644 --- a/src/runtime/ClassManager.cs +++ b/src/runtime/ClassManager.cs @@ -456,13 +456,16 @@ private static ClassInfo GetClassInfo(Type type, ClassBase impl) if (!meth.IsSpecialName && !OperatorMethod.IsOperatorMethod(meth)) { - name = name.ToSnakeCase(); - if (!methods.TryGetValue(name, out methodList)) + var snakeCasedName = name.ToSnakeCase(); + if (snakeCasedName != name) { - methodList = methods[name] = new MethodOverloads(false); + if (!methods.TryGetValue(snakeCasedName, out methodList)) + { + methodList = methods[snakeCasedName] = new MethodOverloads(false); } methodList.Add(meth); } + } continue; case MemberTypes.Constructor when !impl.HasCustomNew(): From c57d283928e11dcf81c07e10c03ab9b8c7692e2f Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Wed, 10 Apr 2024 12:11:11 -0400 Subject: [PATCH 11/13] Expand unit tests --- src/embed_tests/ClassManagerTests.cs | 106 ++++++++++++++++----------- 1 file changed, 65 insertions(+), 41 deletions(-) diff --git a/src/embed_tests/ClassManagerTests.cs b/src/embed_tests/ClassManagerTests.cs index 7ba56b59c..9675a0a7c 100644 --- a/src/embed_tests/ClassManagerTests.cs +++ b/src/embed_tests/ClassManagerTests.cs @@ -83,13 +83,13 @@ public string JoinToString(string thisIsAStringParameter, int thisIsAnIntParameter, float thisIsAFloatParameter, double thisIsADoubleParameter, - decimal thisIsADecimalParameter, + decimal? thisIsADecimalParameter, bool thisIsABoolParameter, - DateTime thisIsADateTimeParameter) + DateTime thisIsADateTimeParameter = default) { // Join all parameters into a single string separated by "-" return string.Join("-", thisIsAStringParameter, thisIsACharParameter, thisIsAnIntParameter, thisIsAFloatParameter, - thisIsADoubleParameter, thisIsADecimalParameter, thisIsABoolParameter, string.Format("{0:MMddyyyy}", thisIsADateTimeParameter)); + thisIsADoubleParameter, thisIsADecimalParameter ?? 123.456m, thisIsABoolParameter, string.Format("{0:MMddyyyy}", thisIsADateTimeParameter)); } } @@ -342,59 +342,83 @@ private static IEnumerable SnakeCasedNamedArgsTestCases // 1.1. Original method name: var args = Array.Empty(); var namedArgs = new Dictionary() - { - { "thisIsAStringParameter", stringParam }, - { "thisIsACharParameter", charParam }, - { "thisIsAnIntParameter", intParam }, - { "thisIsAFloatParameter", floatParam }, - { "thisIsADoubleParameter", doubleParam }, - { "thisIsADecimalParameter", decimalParam }, - { "thisIsABoolParameter", boolParam }, - { "thisIsADateTimeParameter", dateTimeParam } - }; - yield return new TestCaseData("JoinToString", args, namedArgs); + { + { "thisIsAStringParameter", stringParam }, + { "thisIsACharParameter", charParam }, + { "thisIsAnIntParameter", intParam }, + { "thisIsAFloatParameter", floatParam }, + { "thisIsADoubleParameter", doubleParam }, + { "thisIsADecimalParameter", decimalParam }, + { "thisIsABoolParameter", boolParam }, + { "thisIsADateTimeParameter", dateTimeParam } + }; + var expectedResult = "string-c-1-2-3-4.0-True-01052013"; + yield return new TestCaseData("JoinToString", args, namedArgs, expectedResult); // 1.2. Snake-cased method name: namedArgs = new Dictionary() - { - { "this_is_a_string_parameter", stringParam }, - { "this_is_a_char_parameter", charParam }, - { "this_is_an_int_parameter", intParam }, - { "this_is_a_float_parameter", floatParam }, - { "this_is_a_double_parameter", doubleParam }, - { "this_is_a_decimal_parameter", decimalParam }, - { "this_is_a_bool_parameter", boolParam }, - { "this_is_a_date_time_parameter", dateTimeParam } - }; - yield return new TestCaseData("join_to_string", args, namedArgs); + { + { "this_is_a_string_parameter", stringParam }, + { "this_is_a_char_parameter", charParam }, + { "this_is_an_int_parameter", intParam }, + { "this_is_a_float_parameter", floatParam }, + { "this_is_a_double_parameter", doubleParam }, + { "this_is_a_decimal_parameter", decimalParam }, + { "this_is_a_bool_parameter", boolParam }, + { "this_is_a_date_time_parameter", dateTimeParam } + }; + yield return new TestCaseData("join_to_string", args, namedArgs, expectedResult); // 2. Some args and some kwargs: // 2.1. Original method name: args = new object[] { stringParam, charParam, intParam, floatParam }; namedArgs = new Dictionary() - { - { "thisIsADoubleParameter", doubleParam }, - { "thisIsADecimalParameter", decimalParam }, - { "thisIsABoolParameter", boolParam }, - { "thisIsADateTimeParameter", dateTimeParam } - }; - yield return new TestCaseData("JoinToString", args, namedArgs); + { + { "thisIsADoubleParameter", doubleParam }, + { "thisIsADecimalParameter", decimalParam }, + { "thisIsABoolParameter", boolParam }, + { "thisIsADateTimeParameter", dateTimeParam } + }; + yield return new TestCaseData("JoinToString", args, namedArgs, expectedResult); // 2.2. Snake-cased method name: namedArgs = new Dictionary() - { - { "this_is_a_double_parameter", doubleParam }, - { "this_is_a_decimal_parameter", decimalParam }, - { "this_is_a_bool_parameter", boolParam }, - { "this_is_a_date_time_parameter", dateTimeParam } - }; - yield return new TestCaseData("join_to_string", args, namedArgs); + { + { "this_is_a_double_parameter", doubleParam }, + { "this_is_a_decimal_parameter", decimalParam }, + { "this_is_a_bool_parameter", boolParam }, + { "this_is_a_date_time_parameter", dateTimeParam } + }; + yield return new TestCaseData("join_to_string", args, namedArgs, expectedResult); + + // 3. Nullable args: + namedArgs = new Dictionary() + { + { "thisIsADoubleParameter", doubleParam }, + { "thisIsADecimalParameter", null }, + { "thisIsABoolParameter", boolParam }, + { "thisIsADateTimeParameter", dateTimeParam } + }; + expectedResult = "string-c-1-2-3-123.456-True-01052013"; + yield return new TestCaseData("JoinToString", args, namedArgs, expectedResult); + + // 4. Parameters with default values: + namedArgs = new Dictionary() + { + { "this_is_a_double_parameter", doubleParam }, + { "this_is_a_decimal_parameter", decimalParam }, + { "this_is_a_bool_parameter", boolParam }, + // Purposefully omitting the DateTime parameter so the default value is used + }; + expectedResult = "string-c-1-2-3-4.0-True-01010001"; + yield return new TestCaseData("join_to_string", args, namedArgs, expectedResult); } } [TestCaseSource(nameof(SnakeCasedNamedArgsTestCases))] - public void CanCallSnakeCasedMethodWithSnakeCasedNamedArguments(string methodName, object[] args, Dictionary namedArgs) + public void CanCallSnakeCasedMethodWithSnakeCasedNamedArguments(string methodName, object[] args, Dictionary namedArgs, + string expectedResult) { using var obj = new SnakeCaseNamesTesClass().ToPython(); @@ -407,7 +431,7 @@ public void CanCallSnakeCasedMethodWithSnakeCasedNamedArguments(string methodNam var result = obj.InvokeMethod(methodName, pyArgs, pyNamedArgs).As(); - Assert.AreEqual("string-c-1-2-3-4.0-True-01052013", result); + Assert.AreEqual(expectedResult, result); } [Test] From 10e721bf5148b15f4c647cc90b1130ba5a1ced07 Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Thu, 11 Apr 2024 10:32:51 -0400 Subject: [PATCH 12/13] Address peer review --- src/runtime/ClassManager.cs | 21 ++++++++++++--------- src/runtime/MethodBinder.cs | 6 ++++-- src/runtime/Types/OperatorMethod.cs | 4 +++- 3 files changed, 19 insertions(+), 12 deletions(-) diff --git a/src/runtime/ClassManager.cs b/src/runtime/ClassManager.cs index f431a8d63..edc2dd443 100644 --- a/src/runtime/ClassManager.cs +++ b/src/runtime/ClassManager.cs @@ -454,17 +454,17 @@ private static ClassInfo GetClassInfo(Type type, ClassBase impl) } methodList.Add(meth); - if (!meth.IsSpecialName && !OperatorMethod.IsOperatorMethod(meth)) + if (!OperatorMethod.IsOperatorMethod(meth)) { var snakeCasedName = name.ToSnakeCase(); if (snakeCasedName != name) { if (!methods.TryGetValue(snakeCasedName, out methodList)) - { + { methodList = methods[snakeCasedName] = new MethodOverloads(false); + } + methodList.Add(meth); } - methodList.Add(meth); - } } continue; @@ -506,8 +506,9 @@ private static ClassInfo GetClassInfo(Type type, ClassBase impl) } ob = new PropertyObject(pi); + var allocatedOb = ob.AllocObject(); ci.members[pi.Name] = ob.AllocObject(); - ci.members[pi.Name.ToSnakeCase()] = ob.AllocObject(); + ci.members[pi.Name.ToSnakeCase()] = allocatedOb; continue; case MemberTypes.Field: @@ -517,14 +518,15 @@ private static ClassInfo GetClassInfo(Type type, ClassBase impl) continue; } ob = new FieldObject(fi); - ci.members[mi.Name] = ob.AllocObject(); + allocatedOb = ob.AllocObject(); + ci.members[mi.Name] = allocatedOb; var pepName = fi.Name.ToSnakeCase(); if (fi.IsLiteral) { pepName = pepName.ToUpper(); } - ci.members[pepName] = ob.AllocObject(); + ci.members[pepName] = allocatedOb; continue; case MemberTypes.Event: @@ -536,8 +538,9 @@ private static ClassInfo GetClassInfo(Type type, ClassBase impl) ob = ei.AddMethod.IsStatic ? new EventBinding(ei) : new EventObject(ei); - ci.members[ei.Name] = ob.AllocObject(); - ci.members[ei.Name.ToSnakeCase()] = ob.AllocObject(); + allocatedOb = ob.AllocObject(); + ci.members[ei.Name] = allocatedOb; + ci.members[ei.Name.ToSnakeCase()] = allocatedOb; continue; case MemberTypes.NestedType: diff --git a/src/runtime/MethodBinder.cs b/src/runtime/MethodBinder.cs index 38cb0f603..7d53b89e3 100644 --- a/src/runtime/MethodBinder.cs +++ b/src/runtime/MethodBinder.cs @@ -441,6 +441,7 @@ internal Binding Bind(BorrowedReference inst, BorrowedReference args, BorrowedRe kwArgDict[keyStr!] = new PyObject(value); } } + var hasNamedArgs = kwArgDict != null && kwArgDict.Count > 0; // Fetch our methods we are going to attempt to match and bind too. var methods = info == null ? GetMethods() @@ -452,7 +453,8 @@ internal Binding Bind(BorrowedReference inst, BorrowedReference args, BorrowedRe // Relevant method variables var mi = methodInformation.MethodBase; var pi = methodInformation.ParameterInfo; - var paramNames = methodInformation.ParametersNames; + // Avoid accessing the parameter names property unless necessary + var paramNames = hasNamedArgs ? methodInformation.ParameterNames : Array.Empty(); int pyArgCount = (int)Runtime.PyTuple_Size(args); // Special case for operators @@ -993,7 +995,7 @@ internal class MethodInformation public bool IsOriginal { get; } - public string[] ParametersNames { get { return _parametersNames.Value; } } + public string[] ParameterNames { get { return _parametersNames.Value; } } public MethodInformation(MethodBase methodBase, ParameterInfo[] parameterInfo) : this(methodBase, parameterInfo, true) diff --git a/src/runtime/Types/OperatorMethod.cs b/src/runtime/Types/OperatorMethod.cs index abe6ded1a..7d21b0649 100644 --- a/src/runtime/Types/OperatorMethod.cs +++ b/src/runtime/Types/OperatorMethod.cs @@ -27,6 +27,7 @@ public SlotDefinition(string methodName, int typeOffset) public int TypeOffset { get; } } + private static HashSet _operatorNames; private static PyObject? _opType; static OperatorMethod() @@ -63,6 +64,7 @@ static OperatorMethod() ["op_LessThan"] = "__lt__", ["op_GreaterThan"] = "__gt__", }; + _operatorNames = new HashSet(OpMethodMap.Keys.Concat(ComparisonOpMap.Keys)); } public static void Initialize() @@ -85,7 +87,7 @@ public static bool IsOperatorMethod(MethodBase method) { return false; } - return OpMethodMap.ContainsKey(method.Name) || ComparisonOpMap.ContainsKey(method.Name); + return _operatorNames.Contains(method.Name); } public static bool IsComparisonOp(MethodBase method) From b93cab7a3fd62d3f253a2fac41aaa4328cbf85f8 Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Thu, 11 Apr 2024 14:01:45 -0400 Subject: [PATCH 13/13] Fix binding already defined in c# snake case member --- src/embed_tests/ClassManagerTests.cs | 117 ++++++++++++++++++++++++++- src/runtime/ClassManager.cs | 51 ++++++++---- 2 files changed, 153 insertions(+), 15 deletions(-) diff --git a/src/embed_tests/ClassManagerTests.cs b/src/embed_tests/ClassManagerTests.cs index 9675a0a7c..000d6db1d 100644 --- a/src/embed_tests/ClassManagerTests.cs +++ b/src/embed_tests/ClassManagerTests.cs @@ -91,7 +91,7 @@ public string JoinToString(string thisIsAStringParameter, return string.Join("-", thisIsAStringParameter, thisIsACharParameter, thisIsAnIntParameter, thisIsAFloatParameter, thisIsADoubleParameter, thisIsADecimalParameter ?? 123.456m, thisIsABoolParameter, string.Format("{0:MMddyyyy}", thisIsADateTimeParameter)); } - } + } [TestCase("AddNumbersAndGetHalf", "add_numbers_and_get_half")] [TestCase("AddNumbersAndGetHalf_Static", "add_numbers_and_get_half_static")] @@ -487,6 +487,121 @@ def SetEnumValue3SnakeCase(obj): } } + private class AlreadyDefinedSnakeCaseMemberTestBaseClass + { + public virtual int SomeIntProperty { get; set; } = 123; + + public int some_int_property { get; set; } = 321; + + public virtual int AnotherIntProperty { get; set; } = 456; + + public int another_int_property() + { + return 654; + } + } + + [Test] + public void DoesntBindSnakeCasedMemberIfAlreadyOriginallyDefinedAsProperty() + { + var obj = new AlreadyDefinedSnakeCaseMemberTestBaseClass(); + using var pyObj = obj.ToPython(); + + Assert.AreEqual(123, pyObj.GetAttr("SomeIntProperty").As()); + Assert.AreEqual(321, pyObj.GetAttr("some_int_property").As()); + } + + [Test] + public void DoesntBindSnakeCasedMemberIfAlreadyOriginallyDefinedAsMethod() + { + var obj = new AlreadyDefinedSnakeCaseMemberTestBaseClass(); + using var pyObj = obj.ToPython(); + + Assert.AreEqual(456, pyObj.GetAttr("AnotherIntProperty").As()); + + using var method = pyObj.GetAttr("another_int_property"); + Assert.IsTrue(method.IsCallable()); + Assert.AreEqual(654, method.Invoke().As()); + } + + private class AlreadyDefinedSnakeCaseMemberTestDerivedClass : AlreadyDefinedSnakeCaseMemberTestBaseClass + { + public int SomeIntProperty { get; set; } = 111; + + public int AnotherIntProperty { get; set; } = 222; + } + + [Test] + public void DoesntBindSnakeCasedMemberIfAlreadyOriginallyDefinedAsPropertyInBaseClass() + { + var obj = new AlreadyDefinedSnakeCaseMemberTestDerivedClass(); + using var pyObj = obj.ToPython(); + + Assert.AreEqual(111, pyObj.GetAttr("SomeIntProperty").As()); + Assert.AreEqual(321, pyObj.GetAttr("some_int_property").As()); + } + + [Test] + public void DoesntBindSnakeCasedMemberIfAlreadyOriginallyDefinedAsMethodInBaseClass() + { + var obj = new AlreadyDefinedSnakeCaseMemberTestDerivedClass(); + using var pyObj = obj.ToPython(); + + Assert.AreEqual(222, pyObj.GetAttr("AnotherIntProperty").As()); + + using var method = pyObj.GetAttr("another_int_property"); + Assert.IsTrue(method.IsCallable()); + Assert.AreEqual(654, method.Invoke().As()); + } + + private abstract class AlreadyDefinedSnakeCaseMemberTestBaseAbstractClass + { + public abstract int AbstractProperty { get; } + + public virtual int SomeIntProperty { get; set; } = 123; + + public int some_int_property { get; set; } = 321; + + public virtual int AnotherIntProperty { get; set; } = 456; + + public int another_int_property() + { + return 654; + } + } + + private class AlreadyDefinedSnakeCaseMemberTestDerivedFromAbstractClass : AlreadyDefinedSnakeCaseMemberTestBaseAbstractClass + { + public override int AbstractProperty => 0; + + public int SomeIntProperty { get; set; } = 333; + + public int AnotherIntProperty { get; set; } = 444; + } + + [Test] + public void DoesntBindSnakeCasedMemberIfAlreadyOriginallyDefinedAsPropertyInBaseAbstractClass() + { + var obj = new AlreadyDefinedSnakeCaseMemberTestDerivedFromAbstractClass(); + using var pyObj = obj.ToPython(); + + Assert.AreEqual(333, pyObj.GetAttr("SomeIntProperty").As()); + Assert.AreEqual(321, pyObj.GetAttr("some_int_property").As()); + } + + [Test] + public void DoesntBindSnakeCasedMemberIfAlreadyOriginallyDefinedAsMethodInBaseAbstractClass() + { + var obj = new AlreadyDefinedSnakeCaseMemberTestDerivedFromAbstractClass(); + using var pyObj = obj.ToPython(); + + Assert.AreEqual(444, pyObj.GetAttr("AnotherIntProperty").As()); + + using var method = pyObj.GetAttr("another_int_property"); + Assert.IsTrue(method.IsCallable()); + Assert.AreEqual(654, method.Invoke().As()); + } + #endregion } diff --git a/src/runtime/ClassManager.cs b/src/runtime/ClassManager.cs index edc2dd443..4ddb641a2 100644 --- a/src/runtime/ClassManager.cs +++ b/src/runtime/ClassManager.cs @@ -343,11 +343,14 @@ private static ClassInfo GetClassInfo(Type type, ClassBase impl) Type tp; int i, n; - MemberInfo[] info = type.GetMembers(BindingFlags); + MemberInfo[] info = type.GetMembers(BindingFlags | BindingFlags.FlattenHierarchy); var local = new HashSet(); var items = new List(); MemberInfo m; + var snakeCasedAttributes = new HashSet(); + var originalMemberNames = info.Select(mi => mi.Name).ToHashSet(); + // Loop through once to find out which names are declared for (i = 0; i < info.Length; i++) { @@ -430,6 +433,28 @@ private static ClassInfo GetClassInfo(Type type, ClassBase impl) } } + void CheckForSnakeCasedAttribute(string name) + { + if (snakeCasedAttributes.Remove(name)) + { + // If the snake cased attribute is a method, we remove it from the list of methods so that it is not added to the class + methods.Remove(name); + } + } + + void AddMember(string name, string snakeCasedName, PyObject obj) + { + CheckForSnakeCasedAttribute(name); + + ci.members[name] = obj; + + if (!originalMemberNames.Contains(snakeCasedName)) + { + ci.members[snakeCasedName] = obj; + snakeCasedAttributes.Add(snakeCasedName); + } + } + for (i = 0; i < items.Count; i++) { var mi = (MemberInfo)items[i]; @@ -448,6 +473,7 @@ private static ClassInfo GetClassInfo(Type type, ClassBase impl) if (name == "__init__" && !impl.HasCustomNew()) continue; + CheckForSnakeCasedAttribute(name); if (!methods.TryGetValue(name, out var methodList)) { methodList = methods[name] = new MethodOverloads(true); @@ -456,14 +482,15 @@ private static ClassInfo GetClassInfo(Type type, ClassBase impl) if (!OperatorMethod.IsOperatorMethod(meth)) { - var snakeCasedName = name.ToSnakeCase(); - if (snakeCasedName != name) + var snakeCasedMethodName = name.ToSnakeCase(); + if (snakeCasedMethodName != name && !originalMemberNames.Contains(snakeCasedMethodName)) { - if (!methods.TryGetValue(snakeCasedName, out methodList)) + if (!methods.TryGetValue(snakeCasedMethodName, out methodList)) { - methodList = methods[snakeCasedName] = new MethodOverloads(false); + methodList = methods[snakeCasedMethodName] = new MethodOverloads(false); } methodList.Add(meth); + snakeCasedAttributes.Add(snakeCasedMethodName); } } continue; @@ -506,9 +533,7 @@ private static ClassInfo GetClassInfo(Type type, ClassBase impl) } ob = new PropertyObject(pi); - var allocatedOb = ob.AllocObject(); - ci.members[pi.Name] = ob.AllocObject(); - ci.members[pi.Name.ToSnakeCase()] = allocatedOb; + AddMember(pi.Name, pi.Name.ToSnakeCase(), ob.AllocObject()); continue; case MemberTypes.Field: @@ -518,15 +543,14 @@ private static ClassInfo GetClassInfo(Type type, ClassBase impl) continue; } ob = new FieldObject(fi); - allocatedOb = ob.AllocObject(); - ci.members[mi.Name] = allocatedOb; var pepName = fi.Name.ToSnakeCase(); if (fi.IsLiteral) { pepName = pepName.ToUpper(); } - ci.members[pepName] = allocatedOb; + + AddMember(fi.Name, pepName, ob.AllocObject()); continue; case MemberTypes.Event: @@ -538,9 +562,7 @@ private static ClassInfo GetClassInfo(Type type, ClassBase impl) ob = ei.AddMethod.IsStatic ? new EventBinding(ei) : new EventObject(ei); - allocatedOb = ob.AllocObject(); - ci.members[ei.Name] = allocatedOb; - ci.members[ei.Name.ToSnakeCase()] = allocatedOb; + AddMember(ei.Name, ei.Name.ToSnakeCase(), ob.AllocObject()); continue; case MemberTypes.NestedType: @@ -552,6 +574,7 @@ private static ClassInfo GetClassInfo(Type type, ClassBase impl) } // Note the given instance might be uninitialized var pyType = GetClass(tp); + CheckForSnakeCasedAttribute(mi.Name); // make a copy, that could be disposed later ci.members[mi.Name] = new ReflectedClrType(pyType); continue;