From 100c038fdd1899852e4010950c8ceb4c08b43ce4 Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Fri, 8 Dec 2023 11:04:08 -0400 Subject: [PATCH 1/2] Allow accessing protected properties of managed dynamic objects --- src/embed_tests/TestPropertyAccess.cs | 37 +++++++++++++++++++++++++ src/runtime/Types/DynamicClassObject.cs | 10 +++++-- 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/src/embed_tests/TestPropertyAccess.cs b/src/embed_tests/TestPropertyAccess.cs index 685b3b28e..6aeb1bf4c 100644 --- a/src/embed_tests/TestPropertyAccess.cs +++ b/src/embed_tests/TestPropertyAccess.cs @@ -960,6 +960,12 @@ public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, o public Dictionary Properties { get { return _properties; } } public string NonDynamicProperty { get; set; } + + protected string NonDynamicProtectedProperty { get; set; } = "Default value"; + + protected static string NonDynamicProtectedStaticProperty { get; set; } = "Default value"; + + protected string NonDynamicProtectedField = "Default value"; } public class TestPerson : IComparable, IComparable @@ -1265,6 +1271,37 @@ def SetValue(self, fixture): } } + [TestCase("NonDynamicProtectedProperty")] + [TestCase("NonDynamicProtectedField")] + [TestCase("NonDynamicProtectedStaticProperty")] + public void TestSetPublicNonDynamicObjectProtectedPropertyToActualPropertyWorks(string attributeName) + { + var expected = "Non Dynamic Protected Property"; + dynamic model = PyModule.FromString("module", $@" +from clr import AddReference +AddReference(""Python.EmbeddingTest"") +AddReference(""System"") + +from datetime import datetime +import System +from Python.EmbeddingTest import * + +class RandomTestDynamicClass(TestPropertyAccess.DynamicFixture): + def SetValue(self): + self.{attributeName} = ""{expected}"" +").GetAttr("RandomTestDynamicClass").Invoke(); + + using (Py.GIL()) + { + Assert.AreNotEqual(expected, model.GetAttr(attributeName).As()); + + model.SetValue(); + + Assert.AreEqual(expected, model.GetAttr(attributeName).As()); + Assert.IsFalse(model.Properties.ContainsKey(attributeName).As()); + } + } + [Explicit] [TestCase(true, TestName = "CSharpGetPropertyPerformance")] [TestCase(false, TestName = "PythonGetPropertyPerformance")] diff --git a/src/runtime/Types/DynamicClassObject.cs b/src/runtime/Types/DynamicClassObject.cs index 8270d823a..b363cdc31 100644 --- a/src/runtime/Types/DynamicClassObject.cs +++ b/src/runtime/Types/DynamicClassObject.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Dynamic; +using System.Reflection; using System.Runtime.CompilerServices; using RuntimeBinder = Microsoft.CSharp.RuntimeBinder; @@ -87,7 +88,7 @@ public static NewReference tp_getattro(BorrowedReference ob, BorrowedReference k // Do nothing, AttributeError was already raised in Python side and it was not cleared. } // Catch C# exceptions and raise them as Python exceptions. - catch(Exception exception) + catch (Exception exception) { Exceptions.Clear(); Exceptions.SetError(exception); @@ -105,9 +106,12 @@ public static int tp_setattro(BorrowedReference ob, BorrowedReference key, Borro var clrObj = (CLRObject)GetManagedObject(ob)!; var name = Runtime.GetManagedString(key); - // If the key corresponds to a member of the class, we let the default implementation handle it. + // If the key corresponds to a valid property or field of the class, we let the default implementation handle it. var clrObjectType = clrObj.inst.GetType(); - if (clrObjectType.GetMember(name).Length != 0) + var bindingFlags = BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic; + var property = clrObjectType.GetProperty(name, bindingFlags); + var field = property == null ? clrObjectType.GetField(name, bindingFlags) : null; + if ((property != null && property.SetMethod != null) || field != null) { return Runtime.PyObject_GenericSetAttr(ob, key, val); } From c23c5ca2b76e427745de5dc96d16e94b61f114b8 Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Fri, 8 Dec 2023 11:05:26 -0400 Subject: [PATCH 2/2] Bump version to 2.0.26 --- src/perf_tests/Python.PerformanceTests.csproj | 4 ++-- src/runtime/Properties/AssemblyInfo.cs | 4 ++-- src/runtime/Python.Runtime.csproj | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/perf_tests/Python.PerformanceTests.csproj b/src/perf_tests/Python.PerformanceTests.csproj index 09bab9176..d081af07c 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/Properties/AssemblyInfo.cs b/src/runtime/Properties/AssemblyInfo.cs index a6413c82d..2f4055fed 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.25")] -[assembly: AssemblyFileVersion("2.0.25")] +[assembly: AssemblyVersion("2.0.26")] +[assembly: AssemblyFileVersion("2.0.26")] diff --git a/src/runtime/Python.Runtime.csproj b/src/runtime/Python.Runtime.csproj index 3254f1b97..0463bb748 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.25 + 2.0.26 false LICENSE https://github.com/pythonnet/pythonnet