Skip to content

Commit 2d864c2

Browse files
authored
Merge pull request #83 from jhonabreul/feature-snake-case-names-binding
PEP8 compliant names binding
2 parents a967d46 + b93cab7 commit 2d864c2

File tree

10 files changed

+791
-26
lines changed

10 files changed

+791
-26
lines changed

src/embed_tests/ClassManagerTests.cs

Lines changed: 579 additions & 0 deletions
Large diffs are not rendered by default.

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/perf_tests/Python.PerformanceTests.csproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
1414
</PackageReference>
1515
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.*" />
16-
<PackageReference Include="quantconnect.pythonnet" Version="2.0.29" GeneratePathProperty="true">
16+
<PackageReference Include="quantconnect.pythonnet" Version="2.0.30" GeneratePathProperty="true">
1717
<IncludeAssets>compile</IncludeAssets>
1818
</PackageReference>
1919
</ItemGroup>
@@ -25,7 +25,7 @@
2525
</Target>
2626

2727
<Target Name="CopyBaseline" AfterTargets="Build">
28-
<Copy SourceFiles="$(NuGetPackageRoot)quantconnect.pythonnet\2.0.29\lib\net5.0\Python.Runtime.dll" DestinationFolder="$(OutDir)baseline" />
28+
<Copy SourceFiles="$(NuGetPackageRoot)quantconnect.pythonnet\2.0.30\lib\net5.0\Python.Runtime.dll" DestinationFolder="$(OutDir)baseline" />
2929
</Target>
3030

3131
<Target Name="CopyNewBuild" AfterTargets="Build">

src/runtime/ClassManager.cs

Lines changed: 75 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -336,18 +336,21 @@ internal static bool ShouldBindEvent(EventInfo ei)
336336
private static ClassInfo GetClassInfo(Type type, ClassBase impl)
337337
{
338338
var ci = new ClassInfo();
339-
var methods = new Dictionary<string, List<MethodBase>>();
339+
var methods = new Dictionary<string, MethodOverloads>();
340340
MethodInfo meth;
341341
ExtensionType ob;
342342
string name;
343343
Type tp;
344344
int i, n;
345345

346-
MemberInfo[] info = type.GetMembers(BindingFlags);
346+
MemberInfo[] info = type.GetMembers(BindingFlags | BindingFlags.FlattenHierarchy);
347347
var local = new HashSet<string>();
348348
var items = new List<MemberInfo>();
349349
MemberInfo m;
350350

351+
var snakeCasedAttributes = new HashSet<string>();
352+
var originalMemberNames = info.Select(mi => mi.Name).ToHashSet();
353+
351354
// Loop through once to find out which names are declared
352355
for (i = 0; i < info.Length; i++)
353356
{
@@ -430,6 +433,28 @@ private static ClassInfo GetClassInfo(Type type, ClassBase impl)
430433
}
431434
}
432435

436+
void CheckForSnakeCasedAttribute(string name)
437+
{
438+
if (snakeCasedAttributes.Remove(name))
439+
{
440+
// 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
441+
methods.Remove(name);
442+
}
443+
}
444+
445+
void AddMember(string name, string snakeCasedName, PyObject obj)
446+
{
447+
CheckForSnakeCasedAttribute(name);
448+
449+
ci.members[name] = obj;
450+
451+
if (!originalMemberNames.Contains(snakeCasedName))
452+
{
453+
ci.members[snakeCasedName] = obj;
454+
snakeCasedAttributes.Add(snakeCasedName);
455+
}
456+
}
457+
433458
for (i = 0; i < items.Count; i++)
434459
{
435460
var mi = (MemberInfo)items[i];
@@ -448,11 +473,26 @@ private static ClassInfo GetClassInfo(Type type, ClassBase impl)
448473
if (name == "__init__" && !impl.HasCustomNew())
449474
continue;
450475

476+
CheckForSnakeCasedAttribute(name);
451477
if (!methods.TryGetValue(name, out var methodList))
452478
{
453-
methodList = methods[name] = new List<MethodBase>();
479+
methodList = methods[name] = new MethodOverloads(true);
454480
}
455481
methodList.Add(meth);
482+
483+
if (!OperatorMethod.IsOperatorMethod(meth))
484+
{
485+
var snakeCasedMethodName = name.ToSnakeCase();
486+
if (snakeCasedMethodName != name && !originalMemberNames.Contains(snakeCasedMethodName))
487+
{
488+
if (!methods.TryGetValue(snakeCasedMethodName, out methodList))
489+
{
490+
methodList = methods[snakeCasedMethodName] = new MethodOverloads(false);
491+
}
492+
methodList.Add(meth);
493+
snakeCasedAttributes.Add(snakeCasedMethodName);
494+
}
495+
}
456496
continue;
457497

458498
case MemberTypes.Constructor when !impl.HasCustomNew():
@@ -465,7 +505,7 @@ private static ClassInfo GetClassInfo(Type type, ClassBase impl)
465505
name = "__init__";
466506
if (!methods.TryGetValue(name, out methodList))
467507
{
468-
methodList = methods[name] = new List<MethodBase>();
508+
methodList = methods[name] = new MethodOverloads(true);
469509
}
470510
methodList.Add(ctor);
471511
continue;
@@ -493,7 +533,7 @@ private static ClassInfo GetClassInfo(Type type, ClassBase impl)
493533
}
494534

495535
ob = new PropertyObject(pi);
496-
ci.members[pi.Name] = ob.AllocObject();
536+
AddMember(pi.Name, pi.Name.ToSnakeCase(), ob.AllocObject());
497537
continue;
498538

499539
case MemberTypes.Field:
@@ -503,7 +543,14 @@ private static ClassInfo GetClassInfo(Type type, ClassBase impl)
503543
continue;
504544
}
505545
ob = new FieldObject(fi);
506-
ci.members[mi.Name] = ob.AllocObject();
546+
547+
var pepName = fi.Name.ToSnakeCase();
548+
if (fi.IsLiteral)
549+
{
550+
pepName = pepName.ToUpper();
551+
}
552+
553+
AddMember(fi.Name, pepName, ob.AllocObject());
507554
continue;
508555

509556
case MemberTypes.Event:
@@ -515,7 +562,7 @@ private static ClassInfo GetClassInfo(Type type, ClassBase impl)
515562
ob = ei.AddMethod.IsStatic
516563
? new EventBinding(ei)
517564
: new EventObject(ei);
518-
ci.members[ei.Name] = ob.AllocObject();
565+
AddMember(ei.Name, ei.Name.ToSnakeCase(), ob.AllocObject());
519566
continue;
520567

521568
case MemberTypes.NestedType:
@@ -527,6 +574,7 @@ private static ClassInfo GetClassInfo(Type type, ClassBase impl)
527574
}
528575
// Note the given instance might be uninitialized
529576
var pyType = GetClass(tp);
577+
CheckForSnakeCasedAttribute(mi.Name);
530578
// make a copy, that could be disposed later
531579
ci.members[mi.Name] = new ReflectedClrType(pyType);
532580
continue;
@@ -536,9 +584,9 @@ private static ClassInfo GetClassInfo(Type type, ClassBase impl)
536584
foreach (var iter in methods)
537585
{
538586
name = iter.Key;
539-
var mlist = iter.Value.ToArray();
587+
var mlist = iter.Value.Methods.ToArray();
540588

541-
ob = new MethodObject(type, name, mlist);
589+
ob = new MethodObject(type, name, mlist, isOriginal: iter.Value.IsOriginal);
542590
ci.members[name] = ob.AllocObject();
543591
if (mlist.Any(OperatorMethod.IsOperatorMethod))
544592
{
@@ -590,6 +638,24 @@ internal ClassInfo()
590638
indexer = null;
591639
}
592640
}
641+
642+
private class MethodOverloads
643+
{
644+
public List<MethodBase> Methods { get; }
645+
646+
public bool IsOriginal { get; }
647+
648+
public MethodOverloads(bool original = true)
649+
{
650+
Methods = new List<MethodBase>();
651+
IsOriginal = original;
652+
}
653+
654+
public void Add(MethodBase method)
655+
{
656+
Methods.Add(method);
657+
}
658+
}
593659
}
594660

595661
}

src/runtime/MethodBinder.cs

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,15 @@ public int Count
4040
}
4141

4242
internal void AddMethod(MethodBase m)
43+
{
44+
AddMethod(m, true);
45+
}
46+
47+
internal void AddMethod(MethodBase m, bool isOriginal)
4348
{
4449
// we added a new method so we have to re sort the method list
4550
init = false;
46-
list.Add(new MethodInformation(m, m.GetParameters()));
51+
list.Add(new MethodInformation(m, m.GetParameters(), isOriginal));
4752
}
4853

4954
/// <summary>
@@ -118,7 +123,7 @@ internal static MethodInfo[] MatchParameters(MethodBase[] mi, Type[] tp)
118123
return result.ToArray();
119124
}
120125

121-
// Given a generic method and the argsTypes previously matched with it,
126+
// Given a generic method and the argsTypes previously matched with it,
122127
// generate the matching method
123128
internal static MethodInfo ResolveGenericMethod(MethodInfo method, Object[] args)
124129
{
@@ -436,6 +441,7 @@ internal Binding Bind(BorrowedReference inst, BorrowedReference args, BorrowedRe
436441
kwArgDict[keyStr!] = new PyObject(value);
437442
}
438443
}
444+
var hasNamedArgs = kwArgDict != null && kwArgDict.Count > 0;
439445

440446
// Fetch our methods we are going to attempt to match and bind too.
441447
var methods = info == null ? GetMethods()
@@ -447,9 +453,10 @@ internal Binding Bind(BorrowedReference inst, BorrowedReference args, BorrowedRe
447453
// Relevant method variables
448454
var mi = methodInformation.MethodBase;
449455
var pi = methodInformation.ParameterInfo;
456+
// Avoid accessing the parameter names property unless necessary
457+
var paramNames = hasNamedArgs ? methodInformation.ParameterNames : Array.Empty<string>();
450458
int pyArgCount = (int)Runtime.PyTuple_Size(args);
451459

452-
453460
// Special case for operators
454461
bool isOperator = OperatorMethod.IsOperatorMethod(mi);
455462
// 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
479486
pyArgCount,
480487
kwArgDict,
481488
pi,
489+
paramNames,
482490
out bool paramsArray,
483491
out ArrayList defaultArgList))
484492
{
@@ -497,8 +505,8 @@ internal Binding Bind(BorrowedReference inst, BorrowedReference args, BorrowedRe
497505
object arg; // Python -> Clr argument
498506

499507
// Check our KWargs for this parameter
500-
bool hasNamedParam = kwArgDict == null ? false : kwArgDict.TryGetValue(parameter.Name, out tempPyObject);
501-
if(tempPyObject != null)
508+
bool hasNamedParam = kwArgDict == null ? false : kwArgDict.TryGetValue(paramNames[paramIndex], out tempPyObject);
509+
if (tempPyObject != null)
502510
{
503511
op = tempPyObject;
504512
}
@@ -762,10 +770,16 @@ static BorrowedReference HandleParamsArray(BorrowedReference args, int arrayStar
762770
/// This helper method will perform an initial check to determine if we found a matching
763771
/// method based on its parameters count and type <see cref="Bind(IntPtr,IntPtr,IntPtr,MethodBase,MethodInfo[])"/>
764772
/// </summary>
773+
/// <remarks>
774+
/// We required both the parameters info and the parameters names to perform this check.
775+
/// The CLR method parameters info is required to match the parameters count and type.
776+
/// The names are required to perform an accurate match, since the method can be the snake-cased version.
777+
/// </remarks>
765778
private bool CheckMethodArgumentsMatch(int clrArgCount,
766779
int pyArgCount,
767780
Dictionary<string, PyObject> kwargDict,
768781
ParameterInfo[] parameterInfo,
782+
string[] parameterNames,
769783
out bool paramsArray,
770784
out ArrayList defaultArgList)
771785
{
@@ -788,7 +802,7 @@ private bool CheckMethodArgumentsMatch(int clrArgCount,
788802
{
789803
// If the method doesn't have all of these kw args, it is not a match
790804
// Otherwise just continue on to see if it is a match
791-
if (!kwargDict.All(x => parameterInfo.Any(pi => x.Key == pi.Name)))
805+
if (!kwargDict.All(x => parameterNames.Any(paramName => x.Key == paramName)))
792806
{
793807
return false;
794808
}
@@ -808,7 +822,7 @@ private bool CheckMethodArgumentsMatch(int clrArgCount,
808822
defaultArgList = new ArrayList();
809823
for (var v = pyArgCount; v < clrArgCount && match; v++)
810824
{
811-
if (kwargDict != null && kwargDict.ContainsKey(parameterInfo[v].Name))
825+
if (kwargDict != null && kwargDict.ContainsKey(parameterNames[v]))
812826
{
813827
// we have a keyword argument for this parameter,
814828
// no need to check for a default parameter, but put a null
@@ -973,14 +987,30 @@ internal virtual NewReference Invoke(BorrowedReference inst, BorrowedReference a
973987
[Serializable]
974988
internal class MethodInformation
975989
{
990+
private Lazy<string[]> _parametersNames;
991+
976992
public MethodBase MethodBase { get; }
977993

978994
public ParameterInfo[] ParameterInfo { get; }
979995

996+
public bool IsOriginal { get; }
997+
998+
public string[] ParameterNames { get { return _parametersNames.Value; } }
999+
9801000
public MethodInformation(MethodBase methodBase, ParameterInfo[] parameterInfo)
1001+
: this(methodBase, parameterInfo, true)
1002+
{
1003+
}
1004+
1005+
public MethodInformation(MethodBase methodBase, ParameterInfo[] parameterInfo, bool isOriginal)
9811006
{
9821007
MethodBase = methodBase;
9831008
ParameterInfo = parameterInfo;
1009+
IsOriginal = isOriginal;
1010+
1011+
_parametersNames = new Lazy<string[]>(() => IsOriginal
1012+
? ParameterInfo.Select(pi => pi.Name).ToArray()
1013+
: ParameterInfo.Select(pi => pi.Name.ToSnakeCase()).ToArray());
9841014
}
9851015

9861016
public override string ToString()

src/runtime/Properties/AssemblyInfo.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,5 @@
44
[assembly: InternalsVisibleTo("Python.EmbeddingTest, PublicKey=00240000048000009400000006020000002400005253413100040000110000005ffd8f49fb44ab0641b3fd8d55e749f716e6dd901032295db641eb98ee46063cbe0d4a1d121ef0bc2af95f8a7438d7a80a3531316e6b75c2dae92fb05a99f03bf7e0c03980e1c3cfb74ba690aca2f3339ef329313bcc5dccced125a4ffdc4531dcef914602cd5878dc5fbb4d4c73ddfbc133f840231343e013762884d6143189")]
55
[assembly: InternalsVisibleTo("Python.Test, PublicKey=00240000048000009400000006020000002400005253413100040000110000005ffd8f49fb44ab0641b3fd8d55e749f716e6dd901032295db641eb98ee46063cbe0d4a1d121ef0bc2af95f8a7438d7a80a3531316e6b75c2dae92fb05a99f03bf7e0c03980e1c3cfb74ba690aca2f3339ef329313bcc5dccced125a4ffdc4531dcef914602cd5878dc5fbb4d4c73ddfbc133f840231343e013762884d6143189")]
66

7-
[assembly: AssemblyVersion("2.0.29")]
8-
[assembly: AssemblyFileVersion("2.0.29")]
7+
[assembly: AssemblyVersion("2.0.30")]
8+
[assembly: AssemblyFileVersion("2.0.30")]

src/runtime/Python.Runtime.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
<RootNamespace>Python.Runtime</RootNamespace>
66
<AssemblyName>Python.Runtime</AssemblyName>
77
<PackageId>QuantConnect.pythonnet</PackageId>
8-
<Version>2.0.29</Version>
8+
<Version>2.0.30</Version>
99
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
1010
<PackageLicenseFile>LICENSE</PackageLicenseFile>
1111
<RepositoryUrl>https://github.com/pythonnet/pythonnet</RepositoryUrl>

src/runtime/Types/MethodObject.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ internal class MethodObject : ExtensionType
2828
internal PyString? doc;
2929
internal MaybeType type;
3030

31-
public MethodObject(MaybeType type, string name, MethodBase[] info, bool allow_threads = MethodBinder.DefaultAllowThreads)
31+
public MethodObject(MaybeType type, string name, MethodBase[] info, bool allow_threads = MethodBinder.DefaultAllowThreads,
32+
bool isOriginal = true)
3233
{
3334
this.type = type;
3435
this.name = name;
@@ -37,7 +38,7 @@ public MethodObject(MaybeType type, string name, MethodBase[] info, bool allow_t
3738
foreach (MethodBase item in info)
3839
{
3940
this.infoList.Add(item);
40-
binder.AddMethod(item);
41+
binder.AddMethod(item, isOriginal);
4142
if (item.IsStatic)
4243
{
4344
this.is_static = true;

0 commit comments

Comments
 (0)