Skip to content

Commit 283a52f

Browse files
committed
feat: bind snake case name methods along with original method .net to python
1 parent a967d46 commit 283a52f

File tree

4 files changed

+134
-4
lines changed

4 files changed

+134
-4
lines changed

src/embed_tests/ClassManagerTests.cs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,39 @@ public void NestedClassDerivingFromParent()
2424
var f = new NestedTestContainer().ToPython();
2525
f.GetAttr(nameof(NestedTestContainer.Bar));
2626
}
27+
28+
#region Snake case naming tests
29+
30+
public class SnakeCaseNamesTesClass
31+
{
32+
// Purposely long method name to test snake case conversion
33+
public int AddNumbersAndGetHalf(int a, int b)
34+
{
35+
return (a + b) / 2;
36+
}
37+
38+
public static int AddNumbersAndGetHalf_Static(int a, int b)
39+
{
40+
return (a + b) / 2;
41+
}
42+
}
43+
44+
[TestCase("AddNumbersAndGetHalf", "add_numbers_and_get_half")]
45+
[TestCase("AddNumbersAndGetHalf_Static", "add_numbers_and_get_half_static")]
46+
public void BindsSnakeCaseClassMethods(string originalMethodName, string snakeCaseMethodName)
47+
{
48+
using var obj = new SnakeCaseNamesTesClass().ToPython();
49+
using var a = 10.ToPython();
50+
using var b = 20.ToPython();
51+
52+
var camelCaseResult = obj.InvokeMethod(originalMethodName, a, b).As<int>();
53+
var snakeCaseResult = obj.InvokeMethod(snakeCaseMethodName, a, b).As<int>();
54+
55+
Assert.AreEqual(15, camelCaseResult);
56+
Assert.AreEqual(camelCaseResult, snakeCaseResult);
57+
}
58+
59+
#endregion
2760
}
2861

2962
public class NestedTestParent

src/embed_tests/TestUtil.cs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
using NUnit.Framework;
2+
3+
using Python.Runtime;
4+
5+
namespace Python.EmbeddingTest
6+
{
7+
[TestFixture]
8+
public class TestUtil
9+
{
10+
[TestCase("TestCamelCaseString", "test_camel_case_string")]
11+
[TestCase("testCamelCaseString", "test_camel_case_string")]
12+
[TestCase("TestCamelCaseString123 ", "test_camel_case_string123")]
13+
[TestCase("_testCamelCaseString123", "_test_camel_case_string123")]
14+
[TestCase("TestCCS", "test_ccs")]
15+
[TestCase("testCCS", "test_ccs")]
16+
[TestCase("CCSTest", "ccs_test")]
17+
[TestCase("test_CamelCaseString", "test_camel_case_string")]
18+
public void ConvertsNameToSnakeCase(string name, string expected)
19+
{
20+
Assert.AreEqual(expected, name.ToSnakeCase());
21+
}
22+
}
23+
}

src/runtime/ClassManager.cs

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -448,11 +448,21 @@ private static ClassInfo GetClassInfo(Type type, ClassBase impl)
448448
if (name == "__init__" && !impl.HasCustomNew())
449449
continue;
450450

451-
if (!methods.TryGetValue(name, out var methodList))
451+
List<MethodBase> methodList;
452+
var names = new List<string> { name };
453+
if (!meth.IsSpecialName && !OperatorMethod.IsOperatorMethod(meth))
452454
{
453-
methodList = methods[name] = new List<MethodBase>();
455+
names.Add(name.ToSnakeCase());
456+
}
457+
foreach (var currentName in names.Distinct())
458+
{
459+
if (!methods.TryGetValue(currentName, out methodList))
460+
{
461+
methodList = methods[currentName] = new List<MethodBase>();
462+
}
463+
methodList.Add(meth);
454464
}
455-
methodList.Add(meth);
465+
456466
continue;
457467

458468
case MemberTypes.Constructor when !impl.HasCustomNew():

src/runtime/Util/Util.cs

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
using System;
22
using System.Collections.Generic;
33
using System.Diagnostics;
4-
using System.Diagnostics.Contracts;
4+
using System.Globalization;
55
using System.IO;
66
using System.Runtime.CompilerServices;
77
using System.Runtime.InteropServices;
8+
using System.Text;
89

910
namespace Python.Runtime
1011
{
@@ -158,5 +159,68 @@ public static IEnumerable<T> WhereNotNull<T>(this IEnumerable<T?> source)
158159
if (item is not null) yield return item;
159160
}
160161
}
162+
163+
/// <summary>
164+
/// Converts the specified name to snake case.
165+
/// </summary>
166+
/// <remarks>
167+
/// Reference: https://github.com/efcore/EFCore.NamingConventions/blob/main/EFCore.NamingConventions/Internal/SnakeCaseNameRewriter.cs
168+
/// </remarks>
169+
public static string ToSnakeCase(this string name)
170+
{
171+
var builder = new StringBuilder(name.Length + Math.Min(2, name.Length / 5));
172+
var previousCategory = default(UnicodeCategory?);
173+
174+
for (var currentIndex = 0; currentIndex < name.Length; currentIndex++)
175+
{
176+
var currentChar = name[currentIndex];
177+
if (currentChar == '_')
178+
{
179+
builder.Append('_');
180+
previousCategory = null;
181+
continue;
182+
}
183+
184+
var currentCategory = char.GetUnicodeCategory(currentChar);
185+
switch (currentCategory)
186+
{
187+
case UnicodeCategory.UppercaseLetter:
188+
case UnicodeCategory.TitlecaseLetter:
189+
if (previousCategory == UnicodeCategory.SpaceSeparator ||
190+
previousCategory == UnicodeCategory.LowercaseLetter ||
191+
previousCategory != UnicodeCategory.DecimalDigitNumber &&
192+
previousCategory != null &&
193+
currentIndex > 0 &&
194+
currentIndex + 1 < name.Length &&
195+
char.IsLower(name[currentIndex + 1]))
196+
{
197+
builder.Append('_');
198+
}
199+
200+
currentChar = char.ToLower(currentChar, CultureInfo.InvariantCulture);
201+
break;
202+
203+
case UnicodeCategory.LowercaseLetter:
204+
case UnicodeCategory.DecimalDigitNumber:
205+
if (previousCategory == UnicodeCategory.SpaceSeparator)
206+
{
207+
builder.Append('_');
208+
}
209+
break;
210+
211+
default:
212+
if (previousCategory != null)
213+
{
214+
previousCategory = UnicodeCategory.SpaceSeparator;
215+
}
216+
continue;
217+
}
218+
219+
builder.Append(currentChar);
220+
previousCategory = currentCategory;
221+
}
222+
223+
return builder.ToString();
224+
}
161225
}
162226
}

0 commit comments

Comments
 (0)