Skip to content

Commit eb4089c

Browse files
committed
Add support for C# dynamic object properties access
1 parent 0e43fd4 commit eb4089c

File tree

5 files changed

+285
-2
lines changed

5 files changed

+285
-2
lines changed

src/embed_tests/TestPropertyAccess.cs

Lines changed: 186 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
using System.Collections.Generic;
33
using System.Diagnostics;
44
using System.Dynamic;
5-
using System.Linq.Expressions;
5+
using System.Globalization;
6+
using System.Reflection;
67

78
using NUnit.Framework;
89

@@ -926,6 +927,190 @@ def SetValue(self):
926927
}
927928
}
928929

930+
private static TestCaseData[] DynamicPropertiesGetterTestCases() => new[]
931+
{
932+
new TestCaseData(true),
933+
new TestCaseData(10),
934+
new TestCaseData(10.1),
935+
new TestCaseData(10.2m),
936+
new TestCaseData("Some string"),
937+
new TestCaseData(new DateTime(2023, 6, 22)),
938+
new TestCaseData(new List<int> { 1, 2, 3, 4, 5 }),
939+
new TestCaseData(new Dictionary<string, int> { { "first", 1 }, { "second", 2 }, { "third", 3 } }),
940+
new TestCaseData(new Fixture()),
941+
};
942+
943+
[TestCaseSource(nameof(DynamicPropertiesGetterTestCases))]
944+
public void TestGetPublicDynamicObjectPropertyWorks(object property)
945+
{
946+
dynamic model = PyModule.FromString("module", @"
947+
from clr import AddReference
948+
AddReference(""Python.EmbeddingTest"")
949+
AddReference(""System"")
950+
951+
from Python.EmbeddingTest import *
952+
953+
class TestGetPublicDynamicObjectPropertyWorks:
954+
def GetValue(self, fixture):
955+
return fixture.SomeProperty
956+
").GetAttr("TestGetPublicDynamicObjectPropertyWorks").Invoke();
957+
958+
dynamic fixture = new DynamicFixture();
959+
fixture.SomeProperty = property;
960+
961+
using (Py.GIL())
962+
{
963+
Assert.AreEqual(property, (model.GetValue(fixture) as PyObject).AsManagedObject(property.GetType()));
964+
}
965+
}
966+
967+
[Test]
968+
public void TestGetNonExistingPublicDynamicObjectPropertyThrows()
969+
{
970+
dynamic model = PyModule.FromString("module", @"
971+
from clr import AddReference
972+
AddReference(""Python.EmbeddingTest"")
973+
AddReference(""System"")
974+
975+
from Python.EmbeddingTest import *
976+
977+
class TestGetNonExistingPublicDynamicObjectPropertyThrows:
978+
def GetValue(self, fixture):
979+
try:
980+
prop = fixture.AnotherProperty
981+
except AttributeError as e:
982+
return e
983+
984+
return None
985+
").GetAttr("TestGetNonExistingPublicDynamicObjectPropertyThrows").Invoke();
986+
987+
dynamic fixture = new DynamicFixture();
988+
fixture.SomeProperty = "Some property";
989+
990+
using (Py.GIL())
991+
{
992+
var result = model.GetValue(fixture) as PyObject;
993+
Assert.IsFalse(result.IsNone());
994+
Assert.AreEqual(result.PyType, Exceptions.AttributeError);
995+
Assert.AreEqual("'Python.EmbeddingTest.TestPropertyAccess+DynamicFixture' object has no attribute 'AnotherProperty'",
996+
result.ToString());
997+
}
998+
}
999+
1000+
public class DynamicFixture : DynamicObject
1001+
{
1002+
private Dictionary<string, object> _properties = new Dictionary<string, object>();
1003+
1004+
public override bool TryGetMember(GetMemberBinder binder, out object result)
1005+
{
1006+
return _properties.TryGetValue(binder.Name, out result);
1007+
}
1008+
1009+
public override bool TrySetMember(SetMemberBinder binder, object value)
1010+
{
1011+
_properties[binder.Name] = value;
1012+
return true;
1013+
}
1014+
1015+
public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result)
1016+
{
1017+
try
1018+
{
1019+
result = _properties.GetType().InvokeMember(binder.Name, BindingFlags.InvokeMethod, null, _properties, args,
1020+
CultureInfo.InvariantCulture);
1021+
return true;
1022+
}
1023+
catch
1024+
{
1025+
result = null;
1026+
return false;
1027+
}
1028+
}
1029+
}
1030+
1031+
public class TestPerson : IComparable, IComparable<TestPerson>
1032+
{
1033+
public int Id { get; private set; }
1034+
public string Name { get; private set; }
1035+
1036+
public TestPerson(int id, string name)
1037+
{
1038+
Id = id;
1039+
Name = name;
1040+
}
1041+
1042+
public int CompareTo(object obj)
1043+
{
1044+
return CompareTo(obj as TestPerson);
1045+
}
1046+
1047+
public int CompareTo(TestPerson other)
1048+
{
1049+
if (ReferenceEquals(this, other)) return 0;
1050+
if (other == null) return 1;
1051+
if (Id < other.Id) return -1;
1052+
if (Id > other.Id) return 1;
1053+
return 0;
1054+
}
1055+
1056+
public override bool Equals(object obj)
1057+
{
1058+
return Equals(obj as TestPerson);
1059+
}
1060+
1061+
public bool Equals(TestPerson other)
1062+
{
1063+
return CompareTo(other) == 0;
1064+
}
1065+
}
1066+
1067+
private static TestCaseData[] DynamicPropertiesSetterTestCases() => new[]
1068+
{
1069+
new TestCaseData("True", null),
1070+
new TestCaseData("10", null),
1071+
new TestCaseData("10.1", null),
1072+
new TestCaseData("'Some string'", null),
1073+
new TestCaseData("datetime(2023, 6, 22)", null),
1074+
new TestCaseData("[1, 2, 3, 4, 5]", null),
1075+
new TestCaseData("System.DateTime(2023, 6, 22)", typeof(DateTime)),
1076+
new TestCaseData("TestPropertyAccess.TestPerson(123, 'John doe')", typeof(TestPerson)),
1077+
new TestCaseData("System.Collections.Generic.List[str]()", typeof(List<string>)),
1078+
};
1079+
1080+
[TestCaseSource(nameof(DynamicPropertiesSetterTestCases))]
1081+
public void TestSetPublicDynamicObjectPropertyWorks(string valueCode, Type expectedType)
1082+
{
1083+
dynamic model = PyModule.FromString("module", $@"
1084+
from clr import AddReference
1085+
AddReference(""Python.EmbeddingTest"")
1086+
AddReference(""System"")
1087+
1088+
from datetime import datetime
1089+
import System
1090+
from Python.EmbeddingTest import *
1091+
1092+
value = {valueCode}
1093+
1094+
class TestGetPublicDynamicObjectPropertyWorks:
1095+
def SetValue(self, fixture):
1096+
fixture.SomeProperty = value
1097+
1098+
def GetPythonValue(self):
1099+
return value
1100+
").GetAttr("TestGetPublicDynamicObjectPropertyWorks").Invoke();
1101+
1102+
dynamic fixture = new DynamicFixture();
1103+
1104+
using (Py.GIL())
1105+
{
1106+
model.SetValue(fixture);
1107+
var expectedAsPyObject = model.GetPythonValue() as PyObject;
1108+
var expected = expectedType != null ? expectedAsPyObject.AsManagedObject(expectedType) : expectedAsPyObject;
1109+
1110+
Assert.AreEqual(expected, fixture.SomeProperty);
1111+
}
1112+
}
1113+
9291114
[Explicit]
9301115
[TestCase(true, TestName = "CSharpGetPropertyPerformance")]
9311116
[TestCase(false, TestName = "PythonGetPropertyPerformance")]

src/perf_tests/BaselineComparisonConfig.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ public BaselineComparisonConfig()
2424
.WithLaunchCount(1)
2525
.WithWarmupCount(3)
2626
.WithMaxIterationCount(100)
27-
.WithIterationTime(TimeInterval.FromMilliseconds(100));
27+
.WithIterationTime(BenchmarkDotNet.Horology.TimeInterval.FromMilliseconds(100));
2828
this.Add(baseJob
2929
.WithId("baseline")
3030
.WithEnvironmentVariable(EnvironmentVariableName,

src/perf_tests/Python.PerformanceTests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
<PackageReference Include="quantconnect.pythonnet" Version="2.0.18" GeneratePathProperty="true">
1717
<IncludeAssets>compile</IncludeAssets>
1818
</PackageReference>
19+
<PackageReference Include="Perfolizer" Version="0.3.4" />
1920
</ItemGroup>
2021

2122
<Target Name="GetRuntimeLibBuildOutput" BeforeTargets="Build">

src/runtime/Python.Runtime.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,5 +65,6 @@
6565

6666
<ItemGroup>
6767
<PackageReference Include="fasterflect" Version="3.0.0" />
68+
<PackageReference Include="Perfolizer" Version="0.3.4" />
6869
</ItemGroup>
6970
</Project>

src/runtime/Types/ClassObject.cs

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
using System;
22
using System.Diagnostics;
3+
using System.Dynamic;
34
using System.Linq;
45
using System.Reflection;
6+
using System.Runtime.CompilerServices;
57
using System.Runtime.Serialization;
8+
using RuntimeBinder = Microsoft.CSharp.RuntimeBinder;
69

710
namespace Python.Runtime
811
{
@@ -275,5 +278,98 @@ public override NewReference type_subscript(BorrowedReference idx)
275278
}
276279
return Exceptions.RaiseTypeError("unsubscriptable object");
277280
}
281+
282+
/// <summary>
283+
/// Type __getattro__ implementation.
284+
/// </summary>
285+
public static NewReference tp_getattro(BorrowedReference ob, BorrowedReference key)
286+
{
287+
288+
if (!Runtime.PyString_Check(key))
289+
{
290+
return Exceptions.RaiseTypeError("string expected");
291+
}
292+
293+
var result = Runtime.PyObject_GenericGetAttr(ob, key);
294+
295+
// Property not found, but it can still be a dynamic one if the object is an IDynamicMetaObjectProvider
296+
if (result.IsNull())
297+
{
298+
var clrObj = (CLRObject)GetManagedObject(ob)!;
299+
if (clrObj?.inst is IDynamicMetaObjectProvider)
300+
{
301+
302+
// The call to Runtime.PyObject_GenericGetAttr above ended up with an AttributeError
303+
// for dynamic properties since they are not found.
304+
if (Exceptions.ExceptionMatches(Exceptions.AttributeError))
305+
{
306+
Exceptions.Clear();
307+
}
308+
309+
// TODO: Cache call site.
310+
311+
var name = Runtime.GetManagedString(key);
312+
var binder = RuntimeBinder.Binder.GetMember(
313+
RuntimeBinder.CSharpBinderFlags.None,
314+
name,
315+
clrObj.inst.GetType(),
316+
new[] { RuntimeBinder.CSharpArgumentInfo.Create(RuntimeBinder.CSharpArgumentInfoFlags.None, null) });
317+
var callsite = CallSite<Func<CallSite, object, object>>.Create(binder);
318+
319+
try
320+
{
321+
var res = callsite.Target(callsite, clrObj.inst);
322+
return Converter.ToPython(res);
323+
}
324+
catch (RuntimeBinder.RuntimeBinderException)
325+
{
326+
Exceptions.SetError(Exceptions.AttributeError, $"'{clrObj?.inst.GetType()}' object has no attribute '{name}'");
327+
}
328+
}
329+
}
330+
331+
return result;
332+
}
333+
334+
/// <summary>
335+
/// Type __setattro__ implementation.
336+
/// </summary>
337+
public static int tp_setattro(BorrowedReference ob, BorrowedReference key, BorrowedReference val)
338+
{
339+
if (!Runtime.PyString_Check(key))
340+
{
341+
Exceptions.RaiseTypeError("string expected");
342+
return -1;
343+
}
344+
345+
// If the object is an IDynamicMetaObjectProvider, the property is set as a C# dynamic property, not as a Python attribute
346+
var clrObj = (CLRObject)GetManagedObject(ob)!;
347+
if (clrObj?.inst is IDynamicMetaObjectProvider)
348+
{
349+
// TODO: Cache call site.
350+
351+
var name = Runtime.GetManagedString(key);
352+
var binder = RuntimeBinder.Binder.SetMember(
353+
RuntimeBinder.CSharpBinderFlags.None,
354+
name,
355+
clrObj.inst.GetType(),
356+
new[]
357+
{
358+
RuntimeBinder.CSharpArgumentInfo.Create(RuntimeBinder.CSharpArgumentInfoFlags.None, null),
359+
RuntimeBinder.CSharpArgumentInfo.Create(RuntimeBinder.CSharpArgumentInfoFlags.None, null)
360+
});
361+
var callsite = CallSite<Func<CallSite, object, object, object>>.Create(binder);
362+
363+
var value = ((CLRObject)GetManagedObject(val))?.inst ?? PyObject.FromNullableReference(val);
364+
callsite.Target(callsite, clrObj.inst, value);
365+
366+
return 0;
367+
}
368+
369+
int res = Runtime.PyObject_GenericSetAttr(ob, key, val);
370+
Runtime.PyType_Modified(ob);
371+
372+
return res;
373+
}
278374
}
279375
}

0 commit comments

Comments
 (0)