diff --git a/CHANGELOG.md b/CHANGELOG.md index f1ac49d51..abe58ce6e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ This document follows the conventions laid out in [Keep a CHANGELOG][]. ### Added - Extension methods declared in non-framework assemblies are bound to the type they extend, so they can be called from the instance like in C# (e.g. `order_status.is_open()`). Those marked `[EditorBrowsable(EditorBrowsableState.Never)]`, or declared in a class marked so, keep their static form only +- `repr()` of a .NET object whose type overrides `ToString()` returns that text (the Python-facing member name for enums), so containers of such objects print readably (e.g. `[SPY, AIG]`), unless the type declares a public `__repr__()` - Types with an implicit conversion to a numeric type support Python arithmetic, comparisons, `float()` and `int()` through that conversion, behaving like a Python `int` for integral conversions and a `float` otherwise, unless they declare or inherit the operator - Ability to instantiate new .NET arrays using `Array[T](dim1, dim2, ...)` syntax - Python operator method will call C# operator method for supported binary and unary operators ([#1324][p1324]). diff --git a/src/runtime/Types/ClassBase.cs b/src/runtime/Types/ClassBase.cs index cdb00c548..3d08f8ca6 100644 --- a/src/runtime/Types/ClassBase.cs +++ b/src/runtime/Types/ClassBase.cs @@ -375,6 +375,45 @@ public static NewReference tp_repr(BorrowedReference ob) return reprString is null ? new NewReference(Runtime.PyNone) : Runtime.PyString_FromString(reprString); } + // show an overridden ToString() so containers of .NET objects print readably: [SPY, AIG]. + // ValueType.ToString() only returns the type name, so structs without their own keep the default + var toStringOwner = instType; + if (co.inst is IPythonDerivedType) + { + // the generated type of a Python subclass overrides every virtual method, so like + // PythonDerivedType.InvokeMethod only count a ToString the Python class defines + using var pyToString = Runtime.PyObject_GetAttrString(ob, "ToString"); + if (pyToString.IsNull()) + { + Exceptions.Clear(); + } + if (pyToString.IsNull() || GetManagedObject(pyToString.Borrow()) is not null) + { + while (typeof(IPythonDerivedType).IsAssignableFrom(toStringOwner)) + { + toStringOwner = toStringOwner.BaseType; + } + } + } + var toStringType = toStringOwner.GetMethod("ToString", Type.EmptyTypes)?.DeclaringType; + if (toStringType != typeof(object) && toStringType != typeof(ValueType)) + { + string? text = null; + try + { + // enums match their str(), the Python-facing member name + text = co.inst is Enum enumValue ? EnumObject.ToPythonString(enumValue) : co.inst.ToString(); + } + catch (Exception) + { + // repr() used to succeed for types whose ToString() throws, keep it that way + } + if (text is not null) + { + return Runtime.PyString_FromString(text); + } + } + //otherwise use the standard object.__repr__(inst) using var args = Runtime.PyTuple_New(1); Runtime.PyTuple_SetItem(args.Borrow(), 0, ob); diff --git a/src/testing/ReprTest.cs b/src/testing/ReprTest.cs index 48e93683a..53f386989 100644 --- a/src/testing/ReprTest.cs +++ b/src/testing/ReprTest.cs @@ -68,6 +68,22 @@ public class Baz : BazMiddle } + public class NullToString + { + public override string ToString() + { + return null; + } + } + + public class ThrowingToString + { + public override string ToString() + { + throw new InvalidOperationException("ToString() failed"); + } + } + public class Quux { public string ToString(string format) diff --git a/tests/test_class.py b/tests/test_class.py index c5d34ca51..b90dcc51a 100644 --- a/tests/test_class.py +++ b/tests/test_class.py @@ -45,8 +45,8 @@ def test_class_default_str(): def test_class_default_repr(): """Test the default __repr__ implementation for managed objects.""" - s = System.String("this is a test") - assert repr(s).startswith("