Skip to content

Commit 9daa794

Browse files
Merge pull request #72 from jhonabreul/feature-csharp-dynamic-object-properties-support
Support for C# dynamic objects
2 parents 0e43fd4 + 6ac987f commit 9daa794

File tree

6 files changed

+404
-7
lines changed

6 files changed

+404
-7
lines changed

src/embed_tests/TestPropertyAccess.cs

Lines changed: 274 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,278 @@ def SetValue(self):
926927
}
927928
}
928929

930+
public class DynamicFixture : DynamicObject
931+
{
932+
private Dictionary<string, object> _properties = new Dictionary<string, object>();
933+
934+
public override bool TryGetMember(GetMemberBinder binder, out object result)
935+
{
936+
return _properties.TryGetValue(binder.Name, out result);
937+
}
938+
939+
public override bool TrySetMember(SetMemberBinder binder, object value)
940+
{
941+
_properties[binder.Name] = value;
942+
return true;
943+
}
944+
945+
public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result)
946+
{
947+
try
948+
{
949+
result = _properties.GetType().InvokeMember(binder.Name, BindingFlags.InvokeMethod, null, _properties, args,
950+
CultureInfo.InvariantCulture);
951+
return true;
952+
}
953+
catch
954+
{
955+
result = null;
956+
return false;
957+
}
958+
}
959+
960+
public Dictionary<string, object> Properties { get { return _properties; } }
961+
962+
public string NonDynamicProperty { get; set; }
963+
}
964+
965+
public class TestPerson : IComparable, IComparable<TestPerson>
966+
{
967+
public int Id { get; private set; }
968+
public string Name { get; private set; }
969+
970+
public TestPerson(int id, string name)
971+
{
972+
Id = id;
973+
Name = name;
974+
}
975+
976+
public int CompareTo(object obj)
977+
{
978+
return CompareTo(obj as TestPerson);
979+
}
980+
981+
public int CompareTo(TestPerson other)
982+
{
983+
if (ReferenceEquals(this, other)) return 0;
984+
if (other == null) return 1;
985+
if (Id < other.Id) return -1;
986+
if (Id > other.Id) return 1;
987+
return 0;
988+
}
989+
990+
public override bool Equals(object obj)
991+
{
992+
return Equals(obj as TestPerson);
993+
}
994+
995+
public bool Equals(TestPerson other)
996+
{
997+
return CompareTo(other) == 0;
998+
}
999+
}
1000+
1001+
private static TestCaseData[] DynamicPropertiesGetterTestCases() => new[]
1002+
{
1003+
new TestCaseData(true),
1004+
new TestCaseData(10),
1005+
new TestCaseData(10.1),
1006+
new TestCaseData(10.2m),
1007+
new TestCaseData("Some string"),
1008+
new TestCaseData(new DateTime(2023, 6, 22)),
1009+
new TestCaseData(new List<int> { 1, 2, 3, 4, 5 }),
1010+
new TestCaseData(new Dictionary<string, int> { { "first", 1 }, { "second", 2 }, { "third", 3 } }),
1011+
new TestCaseData(new Fixture()),
1012+
};
1013+
1014+
[TestCaseSource(nameof(DynamicPropertiesGetterTestCases))]
1015+
public void TestGetPublicDynamicObjectPropertyWorks(object property)
1016+
{
1017+
dynamic model = PyModule.FromString("module", @"
1018+
from clr import AddReference
1019+
AddReference(""Python.EmbeddingTest"")
1020+
AddReference(""System"")
1021+
1022+
from Python.EmbeddingTest import *
1023+
1024+
class TestGetPublicDynamicObjectPropertyWorks:
1025+
def GetValue(self, fixture):
1026+
return fixture.DynamicProperty
1027+
").GetAttr("TestGetPublicDynamicObjectPropertyWorks").Invoke();
1028+
1029+
dynamic fixture = new DynamicFixture();
1030+
fixture.DynamicProperty = property;
1031+
1032+
using (Py.GIL())
1033+
{
1034+
Assert.AreEqual(property, (model.GetValue(fixture) as PyObject).AsManagedObject(property.GetType()));
1035+
}
1036+
}
1037+
1038+
[Test]
1039+
public void TestGetNullPublicDynamicObjectPropertyWorks()
1040+
{
1041+
dynamic model = PyModule.FromString("module", @"
1042+
from clr import AddReference
1043+
AddReference(""Python.EmbeddingTest"")
1044+
AddReference(""System"")
1045+
1046+
from Python.EmbeddingTest import *
1047+
1048+
class TestGetNullPublicDynamicObjectPropertyWorks:
1049+
def GetValue(self, fixture):
1050+
return fixture.DynamicProperty
1051+
1052+
def IsNone(self, fixture):
1053+
return fixture.DynamicProperty is None
1054+
").GetAttr("TestGetNullPublicDynamicObjectPropertyWorks").Invoke();
1055+
1056+
dynamic fixture = new DynamicFixture();
1057+
fixture.DynamicProperty = null;
1058+
1059+
using (Py.GIL())
1060+
{
1061+
Assert.IsNull(model.GetValue(fixture));
1062+
Assert.IsTrue(model.IsNone(fixture).As<bool>());
1063+
}
1064+
}
1065+
1066+
[Test]
1067+
public void TestGetNonExistingPublicDynamicObjectPropertyThrows()
1068+
{
1069+
dynamic model = PyModule.FromString("module", @"
1070+
from clr import AddReference
1071+
AddReference(""Python.EmbeddingTest"")
1072+
AddReference(""System"")
1073+
1074+
from Python.EmbeddingTest import *
1075+
1076+
class TestGetNonExistingPublicDynamicObjectPropertyThrows:
1077+
def GetValue(self, fixture):
1078+
try:
1079+
prop = fixture.AnotherProperty
1080+
except AttributeError as e:
1081+
return e
1082+
1083+
return None
1084+
").GetAttr("TestGetNonExistingPublicDynamicObjectPropertyThrows").Invoke();
1085+
1086+
dynamic fixture = new DynamicFixture();
1087+
fixture.DynamicProperty = "Dynamic property";
1088+
1089+
using (Py.GIL())
1090+
{
1091+
var result = model.GetValue(fixture) as PyObject;
1092+
Assert.IsFalse(result.IsNone());
1093+
Assert.AreEqual(result.PyType, Exceptions.AttributeError);
1094+
Assert.AreEqual("'DynamicFixture' object has no attribute 'AnotherProperty'",
1095+
result.ToString());
1096+
}
1097+
}
1098+
1099+
private static TestCaseData[] DynamicPropertiesSetterTestCases() => new[]
1100+
{
1101+
new TestCaseData("True", null),
1102+
new TestCaseData("10", null),
1103+
new TestCaseData("10.1", null),
1104+
new TestCaseData("'Some string'", null),
1105+
new TestCaseData("datetime(2023, 6, 22)", null),
1106+
new TestCaseData("[1, 2, 3, 4, 5]", null),
1107+
new TestCaseData("System.DateTime(2023, 6, 22)", typeof(DateTime)),
1108+
new TestCaseData("TestPropertyAccess.TestPerson(123, 'John doe')", typeof(TestPerson)),
1109+
new TestCaseData("System.Collections.Generic.List[str]()", typeof(List<string>)),
1110+
};
1111+
1112+
[TestCaseSource(nameof(DynamicPropertiesSetterTestCases))]
1113+
public void TestSetPublicDynamicObjectPropertyWorks(string valueCode, Type expectedType)
1114+
{
1115+
dynamic model = PyModule.FromString("module", $@"
1116+
from clr import AddReference
1117+
AddReference(""Python.EmbeddingTest"")
1118+
AddReference(""System"")
1119+
1120+
from datetime import datetime
1121+
import System
1122+
from Python.EmbeddingTest import *
1123+
1124+
value = {valueCode}
1125+
1126+
class TestGetPublicDynamicObjectPropertyWorks:
1127+
def SetValue(self, fixture):
1128+
fixture.DynamicProperty = value
1129+
1130+
def GetPythonValue(self):
1131+
return value
1132+
").GetAttr("TestGetPublicDynamicObjectPropertyWorks").Invoke();
1133+
1134+
dynamic fixture = new DynamicFixture();
1135+
1136+
using (Py.GIL())
1137+
{
1138+
model.SetValue(fixture);
1139+
var expectedAsPyObject = model.GetPythonValue() as PyObject;
1140+
var expected = expectedType != null ? expectedAsPyObject.AsManagedObject(expectedType) : expectedAsPyObject;
1141+
1142+
Assert.AreEqual(expected, fixture.DynamicProperty);
1143+
}
1144+
}
1145+
1146+
[Test]
1147+
public void TestSetNullPublicDynamicObjectPropertyWorks()
1148+
{
1149+
dynamic model = PyModule.FromString("module", $@"
1150+
from clr import AddReference
1151+
AddReference(""Python.EmbeddingTest"")
1152+
AddReference(""System"")
1153+
1154+
from datetime import datetime
1155+
import System
1156+
from Python.EmbeddingTest import *
1157+
1158+
class TestSetNullPublicDynamicObjectPropertyWorks:
1159+
def SetValue(self, fixture):
1160+
fixture.DynamicProperty = None
1161+
").GetAttr("TestSetNullPublicDynamicObjectPropertyWorks").Invoke();
1162+
1163+
dynamic fixture = new DynamicFixture();
1164+
1165+
using (Py.GIL())
1166+
{
1167+
model.SetValue(fixture);
1168+
1169+
Assert.IsTrue(fixture.DynamicProperty.IsNone());
1170+
}
1171+
}
1172+
1173+
[Test]
1174+
public void TestSetPublicNonDynamicObjectPropertyToActualPropertyWorks()
1175+
{
1176+
var expected = "Non Dynamic Property";
1177+
dynamic model = PyModule.FromString("module", $@"
1178+
from clr import AddReference
1179+
AddReference(""Python.EmbeddingTest"")
1180+
AddReference(""System"")
1181+
1182+
from datetime import datetime
1183+
import System
1184+
from Python.EmbeddingTest import *
1185+
1186+
class TestSetPublicNonDynamicObjectPropertyToActualPropertyWorks:
1187+
def SetValue(self, fixture):
1188+
fixture.NonDynamicProperty = ""{expected}""
1189+
").GetAttr("TestSetPublicNonDynamicObjectPropertyToActualPropertyWorks").Invoke();
1190+
1191+
var fixture = new DynamicFixture();
1192+
1193+
using (Py.GIL())
1194+
{
1195+
model.SetValue(fixture);
1196+
Assert.AreEqual(expected, fixture.NonDynamicProperty);
1197+
Assert.AreEqual(expected, ((dynamic)fixture).NonDynamicProperty);
1198+
Assert.IsFalse(fixture.Properties.ContainsKey(nameof(fixture.NonDynamicProperty)));
1199+
}
1200+
}
1201+
9291202
[Explicit]
9301203
[TestCase(true, TestName = "CSharpGetPropertyPerformance")]
9311204
[TestCase(false, TestName = "PythonGetPropertyPerformance")]

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.18" GeneratePathProperty="true">
16+
<PackageReference Include="quantconnect.pythonnet" Version="2.0.19" 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.18\lib\net5.0\Python.Runtime.dll" DestinationFolder="$(OutDir)baseline" />
28+
<Copy SourceFiles="$(NuGetPackageRoot)quantconnect.pythonnet\2.0.19\lib\net5.0\Python.Runtime.dll" DestinationFolder="$(OutDir)baseline" />
2929
</Target>
3030

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

src/runtime/ClassManager.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System;
22
using System.Collections.Generic;
33
using System.Diagnostics;
4+
using System.Dynamic;
45
using System.Linq;
56
using System.Reflection;
67
using System.Runtime.InteropServices;
@@ -201,6 +202,11 @@ internal static ClassBase CreateClass(Type type)
201202
impl = new ClassDerivedObject(type);
202203
}
203204

205+
else if (typeof(IDynamicMetaObjectProvider).IsAssignableFrom(type))
206+
{
207+
impl = new DynamicClassObject(type);
208+
}
209+
204210
else
205211
{
206212
impl = new ClassObject(type);
@@ -221,7 +227,7 @@ internal static void InitClassBase(Type type, ClassBase impl, ReflectedClrType p
221227
impl.indexer = info.indexer;
222228
impl.richcompare.Clear();
223229

224-
230+
225231
// Finally, initialize the class __dict__ and return the object.
226232
using var newDict = Runtime.PyObject_GenericGetDict(pyType.Reference);
227233
BorrowedReference dict = newDict.Borrow();

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.18")]
8-
[assembly: AssemblyFileVersion("2.0.18")]
7+
[assembly: AssemblyVersion("2.0.19")]
8+
[assembly: AssemblyFileVersion("2.0.19")]

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.18</Version>
8+
<Version>2.0.19</Version>
99
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
1010
<PackageLicenseFile>LICENSE</PackageLicenseFile>
1111
<RepositoryUrl>https://github.com/pythonnet/pythonnet</RepositoryUrl>

0 commit comments

Comments
 (0)