Skip to content

Commit 4bf4b28

Browse files
authored
Raise a proper TypeError when a property assignment cannot be performed (#141)
* Set a TypeError when a conversion to a CLR class fails with setError Converter.ToManagedValue had three exits that reported failure without setting a Python error: the final fall-through when a plain Python object has no conversion to the target CLR class, the ClassBase exit when a reflected class object is converted to something other than Type, and the exit for managed values that are neither CLRObject, ClassBase, nor MethodBinding (e.g. a MethodObject). Callers that pass setError true, such as PropertyObject.tp_descr_set, trust the failure to carry a pending error and return -1, so CPython raised "SystemError: error return without exception set" instead of a useful message. Assigning a pure Python object to a C# property of a CLR class type reproduced this. These exits now set the conventional TypeError ("'<type>' value cannot be converted to <target>") when setError is true and no more specific error is already pending from a probing sub-path; setError false behavior is unchanged. * Update version to 2.0.64 Bump package <Version>, AssemblyVersion/AssemblyFileVersion and the perf-test baseline reference to 2.0.64.
1 parent 9bcc291 commit 4bf4b28

8 files changed

Lines changed: 129 additions & 13 deletions

File tree

src/embed_tests/TestConverter.cs

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -546,6 +546,75 @@ public void OverflowConversionOnlyLeavesErrorWhenSettingErrors(bool setError)
546546
Assert.AreEqual(setError, Exceptions.ErrorOccurred());
547547
Exceptions.Clear();
548548
}
549+
550+
[TestCase(true)]
551+
[TestCase(false)]
552+
public void FailedConversionToClrClassOnlyLeavesErrorWhenSettingErrors(bool setError)
553+
{
554+
using var _ = Py.GIL();
555+
556+
var module = PyModule.FromString("FailedConversionToClrClassModule", @"
557+
class PurePythonValue:
558+
pass
559+
");
560+
using var instance = module.GetAttr("PurePythonValue").Invoke();
561+
562+
Assert.IsFalse(Converter.ToManaged(instance, typeof(UriBuilder), out var result, setError));
563+
Assert.IsNull(result);
564+
Assert.AreEqual(setError, Exceptions.ErrorOccurred());
565+
if (setError)
566+
{
567+
Assert.IsTrue(Exceptions.ExceptionMatches(Exceptions.TypeError));
568+
}
569+
Exceptions.Clear();
570+
}
571+
572+
[TestCase(true)]
573+
[TestCase(false)]
574+
public void FailedConversionOfClassObjectOnlyLeavesErrorWhenSettingErrors(bool setError)
575+
{
576+
using var _ = Py.GIL();
577+
578+
var module = PyModule.FromString("FailedConversionOfClassObjectModule", @"
579+
from System import Uri
580+
");
581+
using var classObject = module.GetAttr("Uri");
582+
583+
Assert.IsFalse(Converter.ToManaged(classObject, typeof(UriBuilder), out var result, setError));
584+
Assert.IsNull(result);
585+
Assert.AreEqual(setError, Exceptions.ErrorOccurred());
586+
if (setError)
587+
{
588+
Assert.IsTrue(Exceptions.ExceptionMatches(Exceptions.TypeError));
589+
}
590+
Exceptions.Clear();
591+
}
592+
593+
[TestCase(true)]
594+
[TestCase(false)]
595+
public void FailedConversionOfNonClrValueManagedTypesOnlyLeavesErrorWhenSettingErrors(bool setError)
596+
{
597+
using var _ = Py.GIL();
598+
599+
// a CLR namespace module is a managed type that is neither a CLRObject,
600+
// a class, nor a method binding; a class attribute access yields a
601+
// MethodBinding that reaches the final conversion fall-through
602+
using var systemModule = Py.Import("System");
603+
using var uriClass = systemModule.GetAttr("Uri");
604+
using var compareBinding = uriClass.GetAttr("Compare");
605+
606+
foreach (var value in new[] { systemModule, compareBinding })
607+
{
608+
Assert.IsFalse(Converter.ToManaged(value, typeof(UriBuilder), out var result, setError));
609+
Assert.IsNull(result);
610+
Assert.AreEqual(setError, Exceptions.ErrorOccurred());
611+
if (setError)
612+
{
613+
Assert.IsTrue(Exceptions.ExceptionMatches(Exceptions.TypeError));
614+
}
615+
Exceptions.Clear();
616+
}
617+
}
549618
}
550619

551620
public interface IGetList

src/embed_tests/TestPropertyAccess.cs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ public class Fixture
5252
public static readonly string PublicStaticReadOnlyField = "Default value";
5353
protected static readonly string ProtectedStaticReadOnlyField = "Default value";
5454

55+
public Fixture ObjectProperty { get; set; }
56+
5557
public static Fixture Create()
5658
{
5759
return new Fixture();
@@ -291,6 +293,34 @@ def SetValue(self, fixture):
291293
}
292294
}
293295

296+
[Test]
297+
public void TestSetPropertyNonConvertibleValueRaisesTypeError()
298+
{
299+
dynamic model = PyModule.FromString("module", @"
300+
from clr import AddReference
301+
AddReference(""System"")
302+
AddReference(""Python.EmbeddingTest"")
303+
304+
from Python.EmbeddingTest import *
305+
306+
class PurePythonValue:
307+
pass
308+
309+
class TestSetPropertyNonConvertibleValueRaisesTypeError:
310+
def SetValue(self, fixture):
311+
fixture.ObjectProperty = PurePythonValue()
312+
").GetAttr("TestSetPropertyNonConvertibleValueRaisesTypeError").Invoke();
313+
314+
var fixture = new Fixture();
315+
316+
using (Py.GIL())
317+
{
318+
var exception = Assert.Throws<PythonException>(() => model.SetValue(fixture));
319+
Assert.AreEqual("TypeError", exception.Type.Name);
320+
StringAssert.Contains("value cannot be converted to", exception.Message);
321+
}
322+
}
323+
294324
[Test]
295325
public void TestGetPublicReadOnlyPropertyFailsWhenAccessedOnClass()
296326
{

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

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

src/runtime/Converter.cs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -526,6 +526,7 @@ internal static bool ToManagedValue(BorrowedReference value, Type obType,
526526
// The value being converted is a class type, so it will only succeed if it's being converted into a Type
527527
if (obType != typeof(Type))
528528
{
529+
SetConversionError(value, obType, setError);
529530
return false;
530531
}
531532

@@ -540,6 +541,7 @@ internal static bool ToManagedValue(BorrowedReference value, Type obType,
540541
// Method bindings will be handled below along with actual Python callables
541542
if (mt is not MethodBinding)
542543
{
544+
SetConversionError(value, obType, setError);
543545
return false;
544546
}
545547
}
@@ -723,9 +725,26 @@ internal static bool ToManagedValue(BorrowedReference value, Type obType,
723725
}
724726
}
725727

728+
SetConversionError(value, obType, setError);
726729
return false;
727730
}
728731

732+
/// <summary>
733+
/// Sets a TypeError for a failed conversion, unless the caller did not ask for
734+
/// errors or a probing sub-path already raised a more specific error that must
735+
/// be surfaced instead. Callers that report failure with setError true must
736+
/// always leave a Python error pending, otherwise CPython turns the bare error
737+
/// return into a "SystemError: error return without exception set".
738+
/// </summary>
739+
static void SetConversionError(BorrowedReference value, Type obType, bool setError)
740+
{
741+
if (setError && !Exceptions.ErrorOccurred())
742+
{
743+
string tpName = Runtime.PyObject_GetTypeName(value);
744+
Exceptions.SetError(Exceptions.TypeError, $"'{tpName}' value cannot be converted to {obType}");
745+
}
746+
}
747+
729748
static bool EncodableByUser(Type type, object value)
730749
{
731750
// When no encoders are registered (the common case) skip the type

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.63")]
8-
[assembly: AssemblyFileVersion("2.0.63")]
7+
[assembly: AssemblyVersion("2.0.64")]
8+
[assembly: AssemblyFileVersion("2.0.64")]

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

tests/test_array.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -761,8 +761,6 @@ def test_null_array():
761761
ob = Test.NullArrayTest()
762762
_ = ob.items["wrong"]
763763

764-
# TODO: Error Type should be TypeError for all cases
765-
# Currently throws SystemErrors instead
766764
def test_interface_array():
767765
"""Test interface arrays."""
768766
from Python.Test import Spam
@@ -789,15 +787,15 @@ def test_interface_array():
789787
items[0] = None
790788
assert items[0] is None
791789

792-
with pytest.raises(SystemError):
790+
with pytest.raises(TypeError):
793791
ob = Test.InterfaceArrayTest()
794792
ob.items[0] = 99
795793

796794
with pytest.raises(TypeError):
797795
ob = Test.InterfaceArrayTest()
798796
_ = ob.items["wrong"]
799797

800-
with pytest.raises(SystemError):
798+
with pytest.raises(TypeError):
801799
ob = Test.InterfaceArrayTest()
802800
ob.items["wrong"] = "wrong"
803801

@@ -828,7 +826,7 @@ def test_typed_array():
828826
items[0] = None
829827
assert items[0] is None
830828

831-
with pytest.raises(SystemError):
829+
with pytest.raises(TypeError):
832830
ob = Test.TypedArrayTest()
833831
ob.items[0] = 99
834832

tests/test_delegate.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -279,9 +279,9 @@ def test_invalid_object_delegate():
279279

280280
d = ObjectDelegate(hello_func)
281281
ob = DelegateTest()
282-
# QuantConnect fork: a mismatched delegate return surfaces as a .NET
283-
# InvalidOperationException rather than a Python SystemError.
284-
with pytest.raises(System.InvalidOperationException):
282+
# The mismatched delegate return fails its conversion to the expected
283+
# type, which surfaces as a TypeError describing the failed conversion.
284+
with pytest.raises(TypeError):
285285
ob.CallObjectDelegate(d)
286286

287287
def test_out_int_delegate():

0 commit comments

Comments
 (0)