Skip to content

Commit 78551df

Browse files
authored
Throw AttributeError in tp_setattro for dynamic classes (QuantConnect#97)
* Throw AttributeError in tp_getattro for dynamic classes Python api documentation indicates it should throw AttributeError on failure * Cleanup
1 parent 360948f commit 78551df

File tree

2 files changed

+49
-4
lines changed

2 files changed

+49
-4
lines changed

src/embed_tests/TestPropertyAccess.cs

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1420,6 +1420,16 @@ public override bool TryGetMember(GetMemberBinder binder, out object result)
14201420
}
14211421
return true;
14221422
}
1423+
1424+
public override bool TrySetMember(SetMemberBinder binder, object value)
1425+
{
1426+
if (value is PyObject pyValue && PyString.IsStringType(pyValue))
1427+
{
1428+
throw new InvalidOperationException("Cannot set string value");
1429+
}
1430+
1431+
return base.TrySetMember(binder, value);
1432+
}
14231433
}
14241434

14251435
[Test]
@@ -1430,7 +1440,6 @@ public void TestHasAttrShouldNotThrowIfAttributeIsNotPresentForDynamicClassObjec
14301440
dynamic module = PyModule.FromString("TestHasAttrShouldNotThrowIfAttributeIsNotPresentForDynamicClassObjects", @"
14311441
from clr import AddReference
14321442
AddReference(""Python.EmbeddingTest"")
1433-
AddReference(""System"")
14341443
14351444
from Python.EmbeddingTest import TestPropertyAccess
14361445
@@ -1466,6 +1475,40 @@ def has_attribute(obj, attribute):
14661475
Assert.IsFalse(hasAttributeResult);
14671476
}
14681477

1478+
[Test]
1479+
public void TestSetAttrShouldThrowPythonExceptionOnFailure()
1480+
{
1481+
using var _ = Py.GIL();
1482+
1483+
dynamic module = PyModule.FromString("TestHasAttrShouldNotThrowIfAttributeIsNotPresentForDynamicClassObjects", @"
1484+
from clr import AddReference
1485+
AddReference(""Python.EmbeddingTest"")
1486+
1487+
from Python.EmbeddingTest import TestPropertyAccess
1488+
1489+
class TestDynamicClass(TestPropertyAccess.ThrowingDynamicFixture):
1490+
pass
1491+
1492+
def set_attribute(obj):
1493+
obj.int_attribute = 11
1494+
1495+
def set_string_attribute(obj):
1496+
obj.string_attribute = 'string'
1497+
");
1498+
1499+
dynamic fixture = module.GetAttr("TestDynamicClass")();
1500+
1501+
dynamic setAttribute = module.GetAttr("set_attribute");
1502+
Assert.DoesNotThrow(() => setAttribute(fixture));
1503+
1504+
dynamic setStringAttribute = module.GetAttr("set_string_attribute");
1505+
var exception = Assert.Throws<PythonException>(() => setStringAttribute(fixture));
1506+
Assert.AreEqual("Cannot set string value", exception.Message);
1507+
1508+
using var expectedExceptionType = new PyType(Exceptions.AttributeError);
1509+
Assert.AreEqual(expectedExceptionType, exception.Type);
1510+
}
1511+
14691512
public interface IModel
14701513
{
14711514
void InvokeModel();

src/runtime/Types/DynamicClassObject.cs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
using System;
22
using System.Collections.Generic;
3-
using System.Dynamic;
4-
using System.Reflection;
53
using System.Runtime.CompilerServices;
64

75
using RuntimeBinder = Microsoft.CSharp.RuntimeBinder;
@@ -94,6 +92,7 @@ public static NewReference tp_getattro(BorrowedReference ob, BorrowedReference k
9492
// e.g hasattr uses this method to check if the attribute exists. If we throw anything other than AttributeError,
9593
// hasattr will throw instead of catching and returning False.
9694
Exceptions.SetError(Exceptions.AttributeError, exception.Message);
95+
return default;
9796
}
9897
}
9998

@@ -120,7 +119,10 @@ public static int tp_setattro(BorrowedReference ob, BorrowedReference key, Borro
120119
// Catch C# exceptions and raise them as Python exceptions.
121120
catch (Exception exception)
122121
{
123-
Exceptions.SetError(exception);
122+
// tp_setattro should call PyObject_GenericSetAttr (which we already did)
123+
// which must throw AttributeError on failure and return -1 (see https://docs.python.org/3/c-api/object.html#c.PyObject_GenericSetAttr)
124+
Exceptions.SetError(Exceptions.AttributeError, exception.Message);
125+
return -1;
124126
}
125127

126128
return 0;

0 commit comments

Comments
 (0)