From 1b25274e00382fc5cdb577b750d4daa9a04b0a7c Mon Sep 17 00:00:00 2001 From: "quantconnect-kronos[bot]" <346399058+quantconnect-kronos[bot]@users.noreply.github.com> Date: Fri, 30 Oct 2026 13:21:40 +0000 Subject: [PATCH 1/4] Show an overridden ToString() in the default repr of .NET objects Containers such as lists print their elements with repr(), so a list of .NET objects whose type overrides ToString() printed only opaque '' entries. Like IronPython, the default repr now appends the ToString() text: ''. Types without a ToString() override, or whose ToString() returns null or throws, keep the previous repr, and a public __repr__() still wins. Co-Authored-By: Claude Opus 5.5 --- src/runtime/Types/ClassBase.cs | 26 +++++++++++++++++++++++++- src/testing/ReprTest.cs | 16 ++++++++++++++++ tests/test_repr.py | 28 ++++++++++++++++++++++++++++ 3 files changed, 69 insertions(+), 1 deletion(-) diff --git a/src/runtime/Types/ClassBase.cs b/src/runtime/Types/ClassBase.cs index cdb00c548..da9af20e3 100644 --- a/src/runtime/Types/ClassBase.cs +++ b/src/runtime/Types/ClassBase.cs @@ -379,7 +379,31 @@ public static NewReference tp_repr(BorrowedReference ob) using var args = Runtime.PyTuple_New(1); Runtime.PyTuple_SetItem(args.Borrow(), 0, ob); using var reprFunc = Runtime.PyObject_GetAttr(Runtime.PyBaseObjectType, PyIdentifier.__repr__); - return Runtime.PyObject_Call(reprFunc.Borrow(), args.Borrow(), null); + var defaultRepr = Runtime.PyObject_Call(reprFunc.Borrow(), args.Borrow(), null); + if (defaultRepr.IsNull() || instType.GetMethod("ToString", Type.EmptyTypes)?.DeclaringType == typeof(object)) + { + return defaultRepr; + } + + // like IronPython, show an overridden ToString() so containers of .NET objects print readably: + // + string text; + try + { + text = co.inst.ToString(); + } + catch (Exception) + { + // repr() used to succeed for types whose ToString() throws, keep it that way + return defaultRepr; + } + if (text is null) + { + return defaultRepr; + } + var defaultReprString = Runtime.GetManagedString(defaultRepr.Borrow()); + defaultRepr.Dispose(); + return Runtime.PyString_FromString($"{defaultReprString.Substring(0, defaultReprString.Length - 1)} [{text}]>"); } catch (Exception e) { 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_repr.py b/tests/test_repr.py index 5131f5d88..a2d50ce1c 100644 --- a/tests/test_repr.py +++ b/tests/test_repr.py @@ -18,12 +18,38 @@ def test_system_string(): ob = System.String("hello") assert str(ob) == "hello" assert "") def test_str_only(): """Test class implementing ToString() but not __repr__()""" ob = ReprTest.Bar() assert str(ob) == "I implement ToString() but not __repr__()!" assert "") + +def test_str_only_nested_in_list(): + """Test containers show the ToString() of elements implementing ToString() but not __repr__()""" + obs = [ReprTest.Bar(), ReprTest.Bar()] + assert str(obs).count("[I implement ToString() but not __repr__()!]>") == 2 + +def test_no_tostring_override(): + """Test object not overriding ToString() keeps the default repr""" + ob = System.Object() + assert repr(ob).startswith("") + assert "[" not in repr(ob) + +def test_tostring_returns_null(): + """Test class whose ToString() returns null""" + ob = ReprTest.NullToString() + assert "") ob3 = ReprTest.Baz() assert str(ob3) == "Middle class implementing ToString()!" assert "") def bad_tostring(): """Test ToString that can't be used by str()""" From 50f9c474b2232e35c7ebd67fdfff0420ab3cb1e6 Mon Sep 17 00:00:00 2001 From: "quantconnect-kronos[bot]" <346399058+quantconnect-kronos[bot]@users.noreply.github.com> Date: Fri, 30 Oct 2026 13:42:17 +0000 Subject: [PATCH 2/4] Use the overridden ToString() text as the repr of .NET objects Drop the '' wrapper: end users get nothing from the type name and address, so a list of Symbols now prints as [SPY, AIG]. Types without a ToString() override, or whose ToString() returns null or throws, keep the default object repr, and a public __repr__() still wins. Co-Authored-By: Claude Opus 5.5 --- src/runtime/Types/ClassBase.cs | 44 +++++++++++++++------------------- tests/test_class.py | 4 ++-- tests/test_repr.py | 20 +++++----------- 3 files changed, 27 insertions(+), 41 deletions(-) diff --git a/src/runtime/Types/ClassBase.cs b/src/runtime/Types/ClassBase.cs index da9af20e3..b880d0cfc 100644 --- a/src/runtime/Types/ClassBase.cs +++ b/src/runtime/Types/ClassBase.cs @@ -375,35 +375,29 @@ 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] + if (instType.GetMethod("ToString", Type.EmptyTypes)?.DeclaringType != typeof(object)) + { + string? text = null; + try + { + text = 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); using var reprFunc = Runtime.PyObject_GetAttr(Runtime.PyBaseObjectType, PyIdentifier.__repr__); - var defaultRepr = Runtime.PyObject_Call(reprFunc.Borrow(), args.Borrow(), null); - if (defaultRepr.IsNull() || instType.GetMethod("ToString", Type.EmptyTypes)?.DeclaringType == typeof(object)) - { - return defaultRepr; - } - - // like IronPython, show an overridden ToString() so containers of .NET objects print readably: - // - string text; - try - { - text = co.inst.ToString(); - } - catch (Exception) - { - // repr() used to succeed for types whose ToString() throws, keep it that way - return defaultRepr; - } - if (text is null) - { - return defaultRepr; - } - var defaultReprString = Runtime.GetManagedString(defaultRepr.Borrow()); - defaultRepr.Dispose(); - return Runtime.PyString_FromString($"{defaultReprString.Substring(0, defaultReprString.Length - 1)} [{text}]>"); + return Runtime.PyObject_Call(reprFunc.Borrow(), args.Borrow(), null); } catch (Exception e) { 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("") + assert ob.__repr__() == "hello" def test_str_only(): """Test class implementing ToString() but not __repr__()""" ob = ReprTest.Bar() assert str(ob) == "I implement ToString() but not __repr__()!" - assert "") + assert ob.__repr__() == "I implement ToString() but not __repr__()!" def test_str_only_nested_in_list(): """Test containers show the ToString() of elements implementing ToString() but not __repr__()""" obs = [ReprTest.Bar(), ReprTest.Bar()] - assert str(obs).count("[I implement ToString() but not __repr__()!]>") == 2 + assert str(obs) == "[I implement ToString() but not __repr__()!, I implement ToString() but not __repr__()!]" def test_no_tostring_override(): """Test object not overriding ToString() keeps the default repr""" ob = System.Object() assert repr(ob).startswith("") - assert "[" not in repr(ob) def test_tostring_returns_null(): """Test class whose ToString() returns null""" ob = ReprTest.NullToString() assert "") + assert ob2.__repr__() == "Middle class implementing ToString()!" ob3 = ReprTest.Baz() assert str(ob3) == "Middle class implementing ToString()!" - assert "") + assert ob3.__repr__() == "Middle class implementing ToString()!" def bad_tostring(): """Test ToString that can't be used by str()""" From 657faff64c0724c4daa6c94ea9205be4cd700fb8 Mon Sep 17 00:00:00 2001 From: "quantconnect-kronos[bot]" <346399058+quantconnect-kronos[bot]@users.noreply.github.com> Date: Fri, 30 Oct 2026 14:14:50 +0000 Subject: [PATCH 3/4] Match str() for enums and keep the default repr for plain structs Enum.ToString() gives the .NET member name, while str() of an enum gives the Python-facing one (MONDAY), so repr() now uses the same name. ValueType.ToString() only returns the type name, so structs without their own ToString() keep the default object repr. Add a CHANGELOG entry. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 1 + src/runtime/Types/ClassBase.cs | 9 ++++++--- tests/test_repr.py | 14 ++++++++++++++ 3 files changed, 21 insertions(+), 3 deletions(-) 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 b880d0cfc..79ce90a65 100644 --- a/src/runtime/Types/ClassBase.cs +++ b/src/runtime/Types/ClassBase.cs @@ -375,13 +375,16 @@ 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] - if (instType.GetMethod("ToString", Type.EmptyTypes)?.DeclaringType != typeof(object)) + // 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 toStringType = instType.GetMethod("ToString", Type.EmptyTypes)?.DeclaringType; + if (toStringType != typeof(object) && toStringType != typeof(ValueType)) { string? text = null; try { - text = co.inst.ToString(); + // enums match their str(), the Python-facing member name + text = co.inst is Enum enumValue ? EnumObject.ToPythonString(enumValue) : co.inst.ToString(); } catch (Exception) { diff --git a/tests/test_repr.py b/tests/test_repr.py index eb02b5694..2e4ddf6cb 100644 --- a/tests/test_repr.py +++ b/tests/test_repr.py @@ -30,6 +30,20 @@ def test_str_only_nested_in_list(): obs = [ReprTest.Bar(), ReprTest.Bar()] assert str(obs) == "[I implement ToString() but not __repr__()!, I implement ToString() but not __repr__()!]" +def test_enum(): + """Test enums show the same Python-facing member name as str()""" + from System import DayOfWeek + from System.IO import FileAccess + assert repr(DayOfWeek.Monday) == str(DayOfWeek.Monday) == "MONDAY" + assert str([DayOfWeek.Monday, FileAccess.ReadWrite]) == "[MONDAY, READ_WRITE]" + +def test_struct_without_tostring_override(): + """Test struct not overriding ToString() keeps the default repr instead of ValueType's type name""" + from System.Collections.Generic import List + ob = List[int]().GetEnumerator() + assert repr(ob).startswith(" Date: Fri, 30 Oct 2026 14:50:04 +0000 Subject: [PATCH 4/4] Keep the default repr for Python subclasses that don't override ToString The generated type of a Python subclass with __namespace__ overrides every virtual method, ToString() included, so its DeclaringType always passed the check and repr() returned the bare type name. Like PythonDerivedType.InvokeMethod, only count a ToString the Python class defines; otherwise look through the generated types to the .NET base. Co-Authored-By: Claude Opus 5.5 --- src/runtime/Types/ClassBase.cs | 20 +++++++++++++++++++- tests/test_repr.py | 18 ++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/src/runtime/Types/ClassBase.cs b/src/runtime/Types/ClassBase.cs index 79ce90a65..3d08f8ca6 100644 --- a/src/runtime/Types/ClassBase.cs +++ b/src/runtime/Types/ClassBase.cs @@ -377,7 +377,25 @@ public static NewReference tp_repr(BorrowedReference ob) // 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 toStringType = instType.GetMethod("ToString", Type.EmptyTypes)?.DeclaringType; + 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; diff --git a/tests/test_repr.py b/tests/test_repr.py index 2e4ddf6cb..1fce44228 100644 --- a/tests/test_repr.py +++ b/tests/test_repr.py @@ -44,6 +44,24 @@ def test_struct_without_tostring_override(): assert repr(ob).startswith("