diff --git a/src/embed_tests/ClassManagerTests.cs b/src/embed_tests/ClassManagerTests.cs index 72025a28b..000d6db1d 100644 --- a/src/embed_tests/ClassManagerTests.cs +++ b/src/embed_tests/ClassManagerTests.cs @@ -1,3 +1,7 @@ +using System; +using System.Collections.Generic; +using System.Linq; + using NUnit.Framework; using Python.Runtime; @@ -24,6 +28,581 @@ public void NestedClassDerivingFromParent() var f = new NestedTestContainer().ToPython(); f.GetAttr(nameof(NestedTestContainer.Bar)); } + + #region Snake case naming tests + + public enum SnakeCaseEnum + { + EnumValue1, + EnumValue2, + EnumValue3 + } + + public class SnakeCaseNamesTesClass + { + // 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 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 SnakeCaseEnum EnumValue = SnakeCaseEnum.EnumValue2; + + 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) + { + return (a + b) / 2; + } + + 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 = default) + { + // Join all parameters into a single string separated by "-" + 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")] + public void BindsSnakeCaseClassMethods(string originalMethodName, string snakeCaseMethodName) + { + using var obj = new SnakeCaseNamesTesClass().ToPython(); + using var a = 10.ToPython(); + using var b = 20.ToPython(); + + var originalMethodResult = obj.InvokeMethod(originalMethodName, a, b).As(); + var snakeCaseMethodResult = obj.InvokeMethod(snakeCaseMethodName, a, b).As(); + + Assert.AreEqual(15, originalMethodResult); + Assert.AreEqual(originalMethodResult, snakeCaseMethodResult); + } + + [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")] + 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"") + +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); + } + } + + [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"") + +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); + } + } + + [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 + } + } + + 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 } + }; + 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, 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, 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, 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, + string expectedResult) + { + 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(expectedResult, 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()); + } + } + } + + 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 } 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/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/ClassManager.cs b/src/runtime/ClassManager.cs index ffe11ec18..4ddb641a2 100644 --- a/src/runtime/ClassManager.cs +++ b/src/runtime/ClassManager.cs @@ -336,18 +336,21 @@ 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; 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,11 +473,26 @@ 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 List(); + methodList = methods[name] = new MethodOverloads(true); } methodList.Add(meth); + + if (!OperatorMethod.IsOperatorMethod(meth)) + { + var snakeCasedMethodName = name.ToSnakeCase(); + if (snakeCasedMethodName != name && !originalMemberNames.Contains(snakeCasedMethodName)) + { + if (!methods.TryGetValue(snakeCasedMethodName, out methodList)) + { + methodList = methods[snakeCasedMethodName] = new MethodOverloads(false); + } + methodList.Add(meth); + snakeCasedAttributes.Add(snakeCasedMethodName); + } + } continue; case MemberTypes.Constructor when !impl.HasCustomNew(): @@ -465,7 +505,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; @@ -493,7 +533,7 @@ private static ClassInfo GetClassInfo(Type type, ClassBase impl) } ob = new PropertyObject(pi); - ci.members[pi.Name] = ob.AllocObject(); + AddMember(pi.Name, pi.Name.ToSnakeCase(), ob.AllocObject()); continue; case MemberTypes.Field: @@ -503,7 +543,14 @@ private static ClassInfo GetClassInfo(Type type, ClassBase impl) continue; } ob = new FieldObject(fi); - ci.members[mi.Name] = ob.AllocObject(); + + var pepName = fi.Name.ToSnakeCase(); + if (fi.IsLiteral) + { + pepName = pepName.ToUpper(); + } + + AddMember(fi.Name, pepName, ob.AllocObject()); continue; case MemberTypes.Event: @@ -515,7 +562,7 @@ private static ClassInfo GetClassInfo(Type type, ClassBase impl) ob = ei.AddMethod.IsStatic ? new EventBinding(ei) : new EventObject(ei); - ci.members[ei.Name] = ob.AllocObject(); + AddMember(ei.Name, ei.Name.ToSnakeCase(), ob.AllocObject()); continue; case MemberTypes.NestedType: @@ -527,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; @@ -536,9 +584,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)) { @@ -590,6 +638,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..7d53b89e3 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) { @@ -436,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() @@ -447,9 +453,10 @@ internal Binding Bind(BorrowedReference inst, BorrowedReference args, BorrowedRe // Relevant method variables var mi = methodInformation.MethodBase; var pi = methodInformation.ParameterInfo; + // 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 bool isOperator = OperatorMethod.IsOperatorMethod(mi); // Binary operator methods will have 2 CLR args but only one Python arg @@ -479,6 +486,7 @@ internal Binding Bind(BorrowedReference inst, BorrowedReference args, BorrowedRe pyArgCount, kwArgDict, pi, + paramNames, out bool paramsArray, out ArrayList defaultArgList)) { @@ -497,8 +505,8 @@ 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); - if(tempPyObject != null) + bool hasNamedParam = kwArgDict == null ? false : kwArgDict.TryGetValue(paramNames[paramIndex], out tempPyObject); + if (tempPyObject != null) { op = tempPyObject; } @@ -762,10 +770,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[] parameterNames, out bool paramsArray, out ArrayList defaultArgList) { @@ -788,7 +802,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 => parameterInfo.Any(pi => x.Key == pi.Name))) + if (!kwargDict.All(x => parameterNames.Any(paramName => x.Key == paramName))) { return false; } @@ -808,7 +822,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(parameterNames[v])) { // we have a keyword argument for this parameter, // no need to check for a default parameter, but put a null @@ -973,14 +987,30 @@ 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[] ParameterNames { get { return _parametersNames.Value; } } + public MethodInformation(MethodBase methodBase, ParameterInfo[] parameterInfo) + : this(methodBase, parameterInfo, true) + { + } + + public MethodInformation(MethodBase methodBase, ParameterInfo[] parameterInfo, bool isOriginal) { MethodBase = methodBase; ParameterInfo = parameterInfo; + IsOriginal = isOriginal; + + _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/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 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; 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) diff --git a/src/runtime/Util/Util.cs b/src/runtime/Util/Util.cs index 89f5bdf4c..6aa398c91 100644 --- a/src/runtime/Util/Util.cs +++ b/src/runtime/Util/Util.cs @@ -1,14 +1,15 @@ 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 { - 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"; @@ -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(); + } } }