Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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]).
Expand Down
39 changes: 39 additions & 0 deletions src/runtime/Types/ClassBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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))

@quantconnect-kronos-reviewer quantconnect-kronos-reviewer Bot Oct 30, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For __namespace__/__assembly__ subclasses, toStringType is always the generated derived type, because ClassDerived overrides every virtual method. When the Python class has no ToString, that override returns object.ToString(), so repr() becomes the bare type name, e.g. rvtest2.PlainNs. Please handle IPythonDerivedType here: look through the generated type to its CLR base unless the Python class defines ToString.

{
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);
Expand Down
16 changes: 16 additions & 0 deletions src/testing/ReprTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
4 changes: 2 additions & 2 deletions tests/test_class.py
Original file line number Diff line number Diff line change
Expand Up @@ -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("<System.String object")
o = System.Object()
assert repr(o).startswith("<System.Object object")


def test_non_public_class():
Expand Down
62 changes: 57 additions & 5 deletions tests/test_repr.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,27 +17,79 @@ def test_system_string():
"""Test system string"""
ob = System.String("hello")
assert str(ob) == "hello"
assert "<System.String object at " in ob.__repr__()
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 "<Python.Test.Bar object at " in ob.__repr__()
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) == "[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("<System.Collections.Generic.List")
assert " object at " in repr(ob)

def test_namespace_subclass():
"""Test Python subclasses with a __namespace__ only show a ToString() that is actually overridden"""
class ReprPlainNs(System.Object):
__namespace__ = "ReprTestNs"

class ReprBarNs(ReprTest.Bar):
__namespace__ = "ReprTestNs"

class ReprOverrideNs(System.Object):
__namespace__ = "ReprTestNs"

def ToString(self):
return "I implement ToString() in Python!"

assert " object at " in repr(ReprPlainNs())
assert repr(ReprBarNs()) == "I implement ToString() but not __repr__()!"
assert repr(ReprOverrideNs()) == "I implement ToString() in Python!"

def test_no_tostring_override():
"""Test object not overriding ToString() keeps the default repr"""
ob = System.Object()
assert repr(ob).startswith("<System.Object object at ")

def test_tostring_returns_null():
"""Test class whose ToString() returns null"""
ob = ReprTest.NullToString()
assert "<Python.Test.NullToString object at " in repr(ob)

def test_tostring_throws():
"""Test class whose ToString() throws"""
ob = ReprTest.ThrowingToString()
assert "<Python.Test.ThrowingToString object at " in repr(ob)

def test_hierarchy1():
"""Test inheritance hierarchy with base & middle class implementing ToString"""
ob1 = ReprTest.BazBase()
assert str(ob1) == "Base class implementing ToString()!"
assert "<Python.Test.BazBase object at " in ob1.__repr__()
assert ob1.__repr__() == "Base class implementing ToString()!"

ob2 = ReprTest.BazMiddle()
assert str(ob2) == "Middle class implementing ToString()!"
assert "<Python.Test.BazMiddle object at " in ob2.__repr__()
assert ob2.__repr__() == "Middle class implementing ToString()!"

ob3 = ReprTest.Baz()
assert str(ob3) == "Middle class implementing ToString()!"
assert "<Python.Test.Baz object at " in ob3.__repr__()
assert ob3.__repr__() == "Middle class implementing ToString()!"

def bad_tostring():
"""Test ToString that can't be used by str()"""
Expand Down
Loading