diff --git a/CHANGELOG.md b/CHANGELOG.md index f8b0aed48..35ef66882 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -71,6 +71,9 @@ See [Mixins/collections.py](src/runtime/Mixins/collections.py). - BREAKING: When trying to convert Python `int` to `System.Object`, result will be of type `PyInt` instead of `System.Int32` due to possible loss of information. Python `float` will continue to be converted to `System.Double`. +- BREAKING: Python.NET will no longer implicitly convert types like `numpy.float64`, that implement `__float__` to +`System.Single` and `System.Double`. An explicit conversion is required on Python or .NET side. +- BREAKING: Python.NET will no longer implicitly convert any Python object to `System.Boolean`. - BREAKING: `PyObject.GetAttr(name, default)` now only ignores `AttributeError` (previously ignored all exceptions). - BREAKING: `PyObject` no longer implements `IEnumerable`. Instead, `PyIterable` does that. diff --git a/README.rst b/README.rst index c0e4229d3..18e15a7b2 100644 --- a/README.rst +++ b/README.rst @@ -77,7 +77,7 @@ Example dynamic sin = np.sin; Console.WriteLine(sin(5)); - double c = np.cos(5) + sin(5); + double c = (double)(np.cos(5) + sin(5)); Console.WriteLine(c); dynamic a = np.array(new List { 1, 2, 3 }); diff --git a/src/embed_tests/NumPyTests.cs b/src/embed_tests/NumPyTests.cs index f31f7b25b..8b76f4ca1 100644 --- a/src/embed_tests/NumPyTests.cs +++ b/src/embed_tests/NumPyTests.cs @@ -40,7 +40,7 @@ public void TestReadme() dynamic sin = np.sin; StringAssert.StartsWith("-0.95892", sin(5).ToString()); - double c = np.cos(5) + sin(5); + double c = (double)(np.cos(5) + sin(5)); Assert.AreEqual(-0.675262, c, 0.01); dynamic a = np.array(new List { 1, 2, 3 }); diff --git a/src/embed_tests/TestConverter.cs b/src/embed_tests/TestConverter.cs index 1657aaf79..3a01763d3 100644 --- a/src/embed_tests/TestConverter.cs +++ b/src/embed_tests/TestConverter.cs @@ -116,6 +116,13 @@ public void ConvertOverflow() } } + [Test] + public void NoImplicitConversionToBool() + { + var pyObj = new PyList(items: new[] { 1.ToPython(), 2.ToPython() }).ToPython(); + Assert.Throws(() => pyObj.As()); + } + [Test] public void ToNullable() { diff --git a/src/python_tests_runner/PythonTestRunner.cs b/src/python_tests_runner/PythonTestRunner.cs index 36e8049d4..05298997b 100644 --- a/src/python_tests_runner/PythonTestRunner.cs +++ b/src/python_tests_runner/PythonTestRunner.cs @@ -33,8 +33,8 @@ public void Dispose() static IEnumerable PythonTestCases() { // Add the test that you want to debug here. - yield return new[] { "test_enum", "test_enum_standard_attrs" }; - yield return new[] { "test_generic", "test_missing_generic_type" }; + yield return new[] { "test_indexer", "test_boolean_indexer" }; + yield return new[] { "test_delegate", "test_bool_delegate" }; } /// diff --git a/src/runtime/converter.cs b/src/runtime/converter.cs index 2b79caf39..94df2a484 100644 --- a/src/runtime/converter.cs +++ b/src/runtime/converter.cs @@ -2,6 +2,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Diagnostics; using System.Globalization; using System.Reflection; using System.Runtime.InteropServices; @@ -501,6 +502,44 @@ internal static bool ToManagedValue(IntPtr value, Type obType, return ToPrimitive(value, obType, out result, setError); } + /// + /// Unlike , + /// this method does not have a setError parameter, because it should + /// only be called after . + /// + internal static bool ToManagedExplicit(BorrowedReference value, Type obType, + out object? result) + { + result = null; + + // this method would potentially clean any existing error resulting in information loss + Debug.Assert(Runtime.PyErr_Occurred() == null); + + string? converterName = + IsInteger(obType) ? "__int__" + : IsFloatingNumber(obType) ? "__float__" + : null; + + if (converterName is null) return false; + + Debug.Assert(obType.IsPrimitive); + + using var converter = Runtime.PyObject_GetAttrString(value, converterName); + if (converter.IsNull()) + { + Exceptions.Clear(); + return false; + } + + using var explicitlyCoerced = Runtime.PyObject_CallObject(converter, BorrowedReference.Null); + if (explicitlyCoerced.IsNull()) + { + Exceptions.Clear(); + return false; + } + return ToPrimitive(explicitlyCoerced, obType, out result, false); + } + static object? ToPyObjectSubclass(ConstructorInfo ctor, PyObject instance, bool setError) { try @@ -544,6 +583,8 @@ internal static int ToInt32(BorrowedReference value) return checked((int)num); } + private static bool ToPrimitive(BorrowedReference value, Type obType, out object? result, bool setError) + => ToPrimitive(value.DangerousGetAddress(), obType, out result, setError); /// /// Convert a Python value to an instance of a primitive managed type. /// @@ -590,8 +631,21 @@ private static bool ToPrimitive(IntPtr value, Type obType, out object? result, b } case TypeCode.Boolean: - result = Runtime.PyObject_IsTrue(value) != 0; - return true; + if (value == Runtime.PyTrue) + { + result = true; + return true; + } + if (value == Runtime.PyFalse) + { + result = false; + return true; + } + if (setError) + { + goto type_error; + } + return false; case TypeCode.Byte: { @@ -768,6 +822,10 @@ private static bool ToPrimitive(IntPtr value, Type obType, out object? result, b case TypeCode.Single: { + if (!Runtime.PyFloat_Check(value) && !Runtime.PyInt_Check(value)) + { + goto type_error; + } double num = Runtime.PyFloat_AsDouble(value); if (num == -1.0 && Exceptions.ErrorOccurred()) { @@ -786,6 +844,10 @@ private static bool ToPrimitive(IntPtr value, Type obType, out object? result, b case TypeCode.Double: { + if (!Runtime.PyFloat_Check(value) && !Runtime.PyInt_Check(value)) + { + goto type_error; + } double num = Runtime.PyFloat_AsDouble(value); if (num == -1.0 && Exceptions.ErrorOccurred()) { @@ -933,6 +995,13 @@ private static bool ToArray(IntPtr value, Type obType, out object? result, bool result = items; return true; } + + internal static bool IsFloatingNumber(Type type) => type == typeof(float) || type == typeof(double); + internal static bool IsInteger(Type type) + => type == typeof(Byte) || type == typeof(SByte) + || type == typeof(Int16) || type == typeof(UInt16) + || type == typeof(Int32) || type == typeof(UInt32) + || type == typeof(Int64) || type == typeof(UInt64); } public static class ConverterExtension diff --git a/src/runtime/pyobject.cs b/src/runtime/pyobject.cs index 70461552c..bd767307b 100644 --- a/src/runtime/pyobject.cs +++ b/src/runtime/pyobject.cs @@ -1293,7 +1293,21 @@ public override bool TryInvoke(InvokeBinder binder, object[] args, out object re public override bool TryConvert(ConvertBinder binder, out object result) { - return Converter.ToManaged(this.obj, binder.Type, out result, false); + // always try implicit conversion first + if (Converter.ToManaged(this.obj, binder.Type, out result, false)) + { + return true; + } + + if (binder.Explicit) + { + Runtime.PyErr_Fetch(out var errType, out var errValue, out var tb); + bool converted = Converter.ToManagedExplicit(Reference, binder.Type, out result); + Runtime.PyErr_Restore(errType.StealNullable(), errValue.StealNullable(), tb.StealNullable()); + return converted; + } + + return false; } public override bool TryBinaryOperation(BinaryOperationBinder binder, object arg, out object result) diff --git a/tests/test_conversion.py b/tests/test_conversion.py index c895951e1..341b11b90 100644 --- a/tests/test_conversion.py +++ b/tests/test_conversion.py @@ -13,53 +13,36 @@ def test_bool_conversion(): """Test bool conversion.""" ob = ConversionTest() assert ob.BooleanField is False - assert ob.BooleanField is False assert ob.BooleanField == 0 ob.BooleanField = True assert ob.BooleanField is True - assert ob.BooleanField is True assert ob.BooleanField == 1 ob.BooleanField = False assert ob.BooleanField is False - assert ob.BooleanField is False assert ob.BooleanField == 0 - ob.BooleanField = 1 - assert ob.BooleanField is True - assert ob.BooleanField is True - assert ob.BooleanField == 1 - - ob.BooleanField = 0 - assert ob.BooleanField is False - assert ob.BooleanField is False - assert ob.BooleanField == 0 + with pytest.raises(TypeError): + ob.BooleanField = 1 + + with pytest.raises(TypeError): + ob.BooleanField = 0 - ob.BooleanField = System.Boolean(None) - assert ob.BooleanField is False - assert ob.BooleanField is False - assert ob.BooleanField == 0 + with pytest.raises(TypeError): + ob.BooleanField = None - ob.BooleanField = System.Boolean('') - assert ob.BooleanField is False - assert ob.BooleanField is False - assert ob.BooleanField == 0 + with pytest.raises(TypeError): + ob.BooleanField = '' - ob.BooleanField = System.Boolean(0) - assert ob.BooleanField is False - assert ob.BooleanField is False - assert ob.BooleanField == 0 + with pytest.raises(TypeError): + ob.BooleanField = System.Boolean(0) - ob.BooleanField = System.Boolean(1) - assert ob.BooleanField is True - assert ob.BooleanField is True - assert ob.BooleanField == 1 + with pytest.raises(TypeError): + ob.BooleanField = System.Boolean(1) - ob.BooleanField = System.Boolean('a') - assert ob.BooleanField is True - assert ob.BooleanField is True - assert ob.BooleanField == 1 + with pytest.raises(TypeError): + ob.BooleanField = System.Boolean('a') def test_sbyte_conversion(): diff --git a/tests/test_delegate.py b/tests/test_delegate.py index 52ac8226d..55115203c 100644 --- a/tests/test_delegate.py +++ b/tests/test_delegate.py @@ -247,7 +247,7 @@ def test_bool_delegate(): from Python.Test import BoolDelegate def always_so_negative(): - return 0 + return False d = BoolDelegate(always_so_negative) ob = DelegateTest() @@ -256,6 +256,12 @@ def always_so_negative(): assert not d() assert not ob.CallBoolDelegate(d) + def always_so_positive(): + return 1 + bad = BoolDelegate(always_so_positive) + with pytest.raises(TypeError): + ob.CallBoolDelegate(bad) + def test_object_delegate(): """Test object delegate.""" from Python.Test import ObjectDelegate diff --git a/tests/test_field.py b/tests/test_field.py index 0becd99e5..52fed54cb 100644 --- a/tests/test_field.py +++ b/tests/test_field.py @@ -200,11 +200,11 @@ def test_boolean_field(): ob.BooleanField = False assert ob.BooleanField is False - ob.BooleanField = 1 - assert ob.BooleanField is True + with pytest.raises(TypeError): + ob.BooleanField = 1 - ob.BooleanField = 0 - assert ob.BooleanField is False + with pytest.raises(TypeError): + ob.BooleanField = 0 def test_sbyte_field(): diff --git a/tests/test_indexer.py b/tests/test_indexer.py index 0af6e6c45..8cf3150ba 100644 --- a/tests/test_indexer.py +++ b/tests/test_indexer.py @@ -65,13 +65,15 @@ def test_boolean_indexer(): ob = Test.BooleanIndexerTest() assert ob[True] is None - assert ob[1] is None - - ob[0] = "false" - assert ob[0] == "false" - ob[1] = "true" - assert ob[1] == "true" + with pytest.raises(TypeError): + ob[1] + with pytest.raises(TypeError): + ob[0] + with pytest.raises(TypeError): + ob[1] = "true" + with pytest.raises(TypeError): + ob[0] = "false" ob[False] = "false" assert ob[False] == "false" diff --git a/tests/test_module.py b/tests/test_module.py index 6949f2712..4e1a1a1ef 100644 --- a/tests/test_module.py +++ b/tests/test_module.py @@ -41,10 +41,6 @@ def test_preload_var(): try: clr.setPreload(True) assert clr.getPreload() is True, clr.getPreload() - clr.setPreload(0) - assert clr.getPreload() is False, clr.getPreload() - clr.setPreload(1) - assert clr.getPreload() is True, clr.getPreload() import System.Configuration content = dir(System.Configuration)