| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent 69ba861 commit 2408c43
3 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -169,19 +169,32 @@ public static int tp_setattro_dlr_proxy(BorrowedReference ob, BorrowedReference | |||
| 169 | 169 | if (!HasClrMember(instance, memberName) && !IsPythonSpecialAttributeName(memberName)) | |
| 170 | 170 | { | |
| 171 | 171 | // Try DLR member storage first | |
| 172 | - bool handled = false; | ||
| 172 | + bool handled; | ||
| 173 | 173 | ||
| 174 | - if (val.IsNull) | ||
| 174 | + try | ||
| 175 | 175 | { | |
| 176 | - handled = dynamicMemberAccessor.TryDeleteMember(dynamicObject, memberName); | ||
| 176 | + if (val.IsNull) | ||
| 177 | + { | ||
| 178 | + handled = dynamicMemberAccessor.TryDeleteMember(dynamicObject, memberName); | ||
| 179 | + } | ||
| 180 | + else | ||
| 181 | + { | ||
| 182 | + object? managedValue = null; | ||
| 183 | + if (val != Runtime.PyNone && !Converter.ToManaged(val, typeof(object), out managedValue, true)) | ||
| 184 | + return -1; | ||
| 185 | + | ||
| 186 | + handled = dynamicMemberAccessor.TrySetMember(dynamicObject, memberName, managedValue); | ||
| 187 | + if (!handled) | ||
| 188 | + { | ||
| 189 | + Exceptions.SetError(Exceptions.AttributeError, $"'{instance.GetType().Name}' object has no attribute '{memberName}'"); | ||
| 190 | + return -1; | ||
| 191 | + } | ||
| 192 | + } | ||
| 177 | 193 | } | |
| 178 | - else | ||
| 194 | + catch (Exception e) | ||
| 179 | 195 | { | |
| 180 | - object? managedValue = null; | ||
| 181 | - if (val != Runtime.PyNone && !Converter.ToManaged(val, typeof(object), out managedValue, true)) | ||
| 182 | - return -1; | ||
| 183 | - | ||
| 184 | - handled = dynamicMemberAccessor.TrySetMember(dynamicObject, memberName, managedValue); | ||
| 196 | + Exceptions.SetError(e); | ||
| 197 | + return -1; | ||
| 185 | 198 | } | |
| 186 | 199 | ||
| 187 | 200 | if (handled) | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -4,12 +4,40 @@ | |||
| 4 | 4 | ||
| 5 | 5 | namespace Python.Test; | |
| 6 | 6 | ||
| 7 | - public class DynamicMappingObject : DynamicObject | ||
| 7 | + /// <summary> | ||
| 8 | + /// Base class for dynamic test helpers. Uses lazy storage initialization so that | ||
| 9 | + /// Python-derived subclasses can safely call DynamicObject member hooks before | ||
| 10 | + /// managed field initializers have run. | ||
| 11 | + /// </summary> | ||
| 12 | + public class DynamicStorageObject : DynamicObject | ||
| 8 | 13 | { | |
| 9 | 14 | Dictionary<string, object> storage; | |
| 10 | 15 | ||
| 11 | - Dictionary<string, object> Storage => storage ??= []; | ||
| 16 | + // Python-defined subclasses may reach this type without running managed field | ||
| 17 | + // initializers (see ClassDerivedObject.NewObjectToPython). Via the lazy init | ||
| 18 | + // we can ensure that the access is still safe, even when the constructor has | ||
| 19 | + // not run. | ||
| 20 | + protected Dictionary<string, object> Storage => storage ??= []; | ||
| 12 | 21 | ||
| 22 | + public void AddDynamicMember(string name, object value) => Storage[name] = value; | ||
| 23 | + | ||
| 24 | + public override bool TryGetMember(GetMemberBinder binder, out object result) | ||
| 25 | + => Storage.TryGetValue(binder.Name, out result); | ||
| 26 | + | ||
| 27 | + public override bool TrySetMember(SetMemberBinder binder, object value) | ||
| 28 | + { | ||
| 29 | + Storage[binder.Name] = value; | ||
| 30 | + return true; | ||
| 31 | + } | ||
| 32 | + | ||
| 33 | + public override bool TryDeleteMember(DeleteMemberBinder binder) | ||
| 34 | + => Storage.Remove(binder.Name); | ||
| 35 | + | ||
| 36 | + public override IEnumerable<string> GetDynamicMemberNames() => Storage.Keys; | ||
| 37 | + } | ||
| 38 | + | ||
| 39 | + public class DynamicMappingObject : DynamicStorageObject | ||
| 40 | + { | ||
| 13 | 41 | // Native members for testing that regular CLR access is unaffected. | |
| 14 | 42 | public string Label = "default"; | |
| 15 | 43 | public int Multiplier { get; set; } = 1; | |
@@ -20,20 +48,39 @@ public class DynamicMappingObject : DynamicObject | |||
| 20 | 48 | ||
| 21 | 49 | // Test helper: retrieve the actual value stored in C# (for verification that None was stored as null) | |
| 22 | 50 | public object GetDynamicValue(string name) => Storage.TryGetValue(name, out var value) ? value : null; | |
| 51 | + } | ||
| 23 | 52 | ||
| 24 | - | ||
| 25 | - | ||
| 26 | - public override bool TryGetMember(GetMemberBinder binder, out object result) | ||
| 27 | - => Storage.TryGetValue(binder.Name, out result); | ||
| 28 | - | ||
| 53 | + public class RejectingSetDynamicObject : DynamicStorageObject | ||
| 54 | + { | ||
| 29 | 55 | public override bool TrySetMember(SetMemberBinder binder, object value) | |
| 30 | 56 | { | |
| 57 | + if (!Storage.ContainsKey(binder.Name)) | ||
| 58 | + return false; | ||
| 59 | + | ||
| 31 | 60 | Storage[binder.Name] = value; | |
| 32 | 61 | return true; | |
| 33 | 62 | } | |
| 63 | + } | ||
| 64 | + | ||
| 65 | + public class ThrowingSetDynamicObject : DynamicStorageObject | ||
| 66 | + { | ||
| 67 | + public override bool TrySetMember(SetMemberBinder binder, object value) | ||
| 68 | + => throw new InvalidOperationException($"TrySetMember failed for '{binder.Name}'"); | ||
| 69 | + } | ||
| 34 | 70 | ||
| 71 | + public class RejectingDeleteDynamicObject : DynamicStorageObject | ||
| 72 | + { | ||
| 35 | 73 | public override bool TryDeleteMember(DeleteMemberBinder binder) | |
| 36 | - => binder is not null && Storage.Remove(binder.Name); | ||
| 74 | + { | ||
| 75 | + if (!Storage.ContainsKey(binder.Name)) | ||
| 76 | + return false; | ||
| 37 | 77 | ||
| 38 | - public override IEnumerable<string> GetDynamicMemberNames() => Storage.Keys; | ||
| 78 | + return Storage.Remove(binder.Name); | ||
| 79 | + } | ||
| 80 | + } | ||
| 81 | + | ||
| 82 | + public class ThrowingDeleteDynamicObject : DynamicStorageObject | ||
| 83 | + { | ||
| 84 | + public override bool TryDeleteMember(DeleteMemberBinder binder) | ||
| 85 | + => throw new InvalidOperationException($"TryDeleteMember failed for '{binder.Name}'"); | ||
| 39 | 86 | } | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -5,6 +5,10 @@ | |||
| 5 | 5 | from System.Dynamic import ExpandoObject | |
| 6 | 6 | ||
| 7 | 7 | from Python.Test import DynamicMappingObject | |
| 8 | + from Python.Test import RejectingDeleteDynamicObject | ||
| 9 | + from Python.Test import RejectingSetDynamicObject | ||
| 10 | + from Python.Test import ThrowingDeleteDynamicObject | ||
| 11 | + from Python.Test import ThrowingSetDynamicObject | ||
| 8 | 12 | ||
| 9 | 13 | ||
| 10 | 14 | def _mro_names(obj): | |
@@ -170,4 +174,36 @@ def custom_property(self, i): | |||
| 170 | 174 | assert obj.custom_property == 10 | |
| 171 | 175 | ||
| 172 | 176 | obj.other_property = None | |
| 173 | - assert obj.other_property is None | ||
| 177 | + assert obj.other_property is None | ||
| 178 | + | ||
| 179 | + | ||
| 180 | + def test_trysetmember_false_raises_attributeerror_instead_of_silent_python_setattr(): | ||
| 181 | + obj = RejectingSetDynamicObject() | ||
| 182 | + | ||
| 183 | + with pytest.raises(AttributeError): | ||
| 184 | + obj.typoed_name = 42 | ||
| 185 | + | ||
| 186 | + assert not hasattr(obj, "typoed_name") | ||
| 187 | + | ||
| 188 | + | ||
| 189 | + def test_trysetmember_exception_is_raised_in_python(): | ||
| 190 | + obj = ThrowingSetDynamicObject() | ||
| 191 | + | ||
| 192 | + with pytest.raises(Exception, match="TrySetMember failed for 'bad_name'"): | ||
| 193 | + obj.bad_name = 42 | ||
| 194 | + | ||
| 195 | + | ||
| 196 | + def test_trydeletemember_false_raises_attributeerror(): | ||
| 197 | + obj = RejectingDeleteDynamicObject() | ||
| 198 | + obj.AddDynamicMember("existing_name", 42) | ||
| 199 | + | ||
| 200 | + with pytest.raises(AttributeError): | ||
| 201 | + del obj.missing_name | ||
| 202 | + | ||
| 203 | + | ||
| 204 | + def test_trydeletemember_exception_is_raised_in_python(): | ||
| 205 | + obj = ThrowingDeleteDynamicObject() | ||
| 206 | + obj.bad_name = 42 | ||
| 207 | + | ||
| 208 | + with pytest.raises(Exception, match="TryDeleteMember failed for 'bad_name'"): | ||
| 209 | + del obj.bad_name | ||
| Back | FazBrowse Home | New Git URL |
0 commit comments