Skip to content

Commit 351d9ff

Browse files
committed
Provide hook to implement __repr__
Issue: 680
1 parent d3ca2e8 commit 351d9ff

File tree

9 files changed

+279
-5
lines changed

9 files changed

+279
-5
lines changed

AUTHORS.md

+1
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
- Luke Stratman ([@lstratman](https://github.com/lstratman))
3737
- Konstantin Posudevskiy ([@konstantin-posudevskiy](https://github.com/konstantin-posudevskiy))
3838
- Matthias Dittrich ([@matthid](https://github.com/matthid))
39+
- Mohamed Koubaa ([@koubaa](https://github.com/koubaa))
3940
- Patrick Stewart ([@patstew](https://github.com/patstew))
4041
- Raphael Nestler ([@rnestler](https://github.com/rnestler))
4142
- Rickard Holmberg ([@rickardraysearch](https://github.com/rickardraysearch))

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ This document follows the conventions laid out in [Keep a CHANGELOG][].
2323
- Incorporated reference-style links to issues and pull requests in the CHANGELOG ([#608][i608])
2424
- Added detailed comments about aproaches and dangers to handle multi-app-domains ([#625][p625])
2525
- Python 3.7 support, builds and testing added. Defaults changed from Python 3.6 to 3.7 ([#698][p698])
26+
- Added support for C# types to provide `__repr__` ([#680][p680])
2627

2728
### Changed
2829

src/runtime/classbase.cs

+68
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,75 @@ public static IntPtr tp_str(IntPtr ob)
233233
}
234234
try
235235
{
236+
//As per python doc:
237+
//The return value must be a string object. If a class defines __repr__() but not __str__(),
238+
//then __repr__() is also used when an “informal” string representation of instances of that
239+
//class is required.
240+
//In C#, everything provides ToString(), so the check here will be whether the type explicitly
241+
//provides ToString() or if it is language provided (i.e. the fully qualified type name as a string)
242+
243+
//First check which type in the object hierarchy provides ToString()
244+
//ToString has two "official" overloads so loop over GetMethods to get the one without parameters
245+
var instType = co.inst.GetType();
246+
foreach (var method in instType.GetMethods())
247+
{
248+
249+
//TODO this could probably be done more cleanly with Linq
250+
if (!method.IsPublic) continue; //skip private/protected methods
251+
if (method.Name != "ToString") continue; //only look for ToString
252+
if (method.DeclaringType == typeof(object)) continue; //ignore default from object
253+
if (method.GetParameters().Length != 0) continue; //ignore Formatter overload of ToString
254+
255+
//match! something other than object provides a parameter-less overload of ToString
256+
return Runtime.PyString_FromString(co.inst.ToString());
257+
}
258+
259+
//If the object defines __repr__, call it.
260+
System.Reflection.MethodInfo reprMethodInfo = instType.GetMethod("__repr__");
261+
if (reprMethodInfo != null && reprMethodInfo.IsPublic)
262+
{
263+
var reprString = reprMethodInfo.Invoke(co.inst, null) as string;
264+
return Runtime.PyString_FromString(reprString);
265+
}
266+
267+
//otherwise fallback to object's ToString() implementation
236268
return Runtime.PyString_FromString(co.inst.ToString());
269+
270+
}
271+
catch (Exception e)
272+
{
273+
if (e.InnerException != null)
274+
{
275+
e = e.InnerException;
276+
}
277+
Exceptions.SetError(e);
278+
return IntPtr.Zero;
279+
}
280+
}
281+
282+
public static IntPtr tp_repr(IntPtr ob)
283+
{
284+
var co = GetManagedObject(ob) as CLRObject;
285+
if (co == null)
286+
{
287+
return Exceptions.RaiseTypeError("invalid object");
288+
}
289+
try
290+
{
291+
//if __repr__ is defined, use it
292+
var instType = co.inst.GetType();
293+
System.Reflection.MethodInfo methodInfo = instType.GetMethod("__repr__");
294+
if (methodInfo != null && methodInfo.IsPublic)
295+
{
296+
var reprString = methodInfo.Invoke(co.inst, null) as string;
297+
return Runtime.PyString_FromString(reprString);
298+
}
299+
300+
//otherwise use the standard object.__repr__(inst)
301+
IntPtr args = Runtime.PyTuple_New(1);
302+
Runtime.PyTuple_SetItem(args, 0, ob);
303+
IntPtr reprFunc = Runtime.PyObject_GetAttrString(Runtime.PyBaseObjectType, "__repr__");
304+
return Runtime.PyObject_Call(reprFunc, args, IntPtr.Zero);
237305
}
238306
catch (Exception e)
239307
{

src/runtime/exceptions.cs

+23
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,29 @@ internal static Exception ToException(IntPtr ob)
3636
return e;
3737
}
3838

39+
/// <summary>
40+
/// Exception __repr__ implementation
41+
/// </summary>
42+
public new static IntPtr tp_repr(IntPtr ob)
43+
{
44+
Exception e = ToException(ob);
45+
if (e == null)
46+
{
47+
return Exceptions.RaiseTypeError("invalid object");
48+
}
49+
string name = e.GetType().Name;
50+
string message;
51+
if (e.Message != String.Empty)
52+
{
53+
message = String.Format("{0}('{1}')", name, e.Message);
54+
}
55+
else
56+
{
57+
message = String.Format("{0}()", name);
58+
}
59+
return Runtime.PyUnicode_FromString(message);
60+
}
61+
3962
/// <summary>
4063
/// Exception __str__ implementation
4164
/// </summary>

src/testing/Python.Test.csproj

+1
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@
9191
<Compile Include="threadtest.cs" />
9292
<Compile Include="doctest.cs" />
9393
<Compile Include="subclasstest.cs" />
94+
<Compile Include="reprtest.cs" />
9495
</ItemGroup>
9596
<ItemGroup>
9697
<Reference Include="Microsoft.CSharp" />

src/testing/ReprTest.cs

+108
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
using System;
2+
using System.Text;
3+
4+
namespace Python.Test
5+
{
6+
/// <summary>
7+
/// Supports repr unit tests.
8+
/// </summary>
9+
public class ReprTest
10+
{
11+
public class Point
12+
{
13+
public Point(double x, double y)
14+
{
15+
X = x;
16+
Y = y;
17+
}
18+
19+
public double X { get; set; }
20+
public double Y { get; set; }
21+
22+
public override string ToString()
23+
{
24+
return base.ToString() + ": X=" + X.ToString() + ", Y=" + Y.ToString();
25+
}
26+
27+
public string __repr__()
28+
{
29+
return "Point(" + X.ToString() + "," + Y.ToString() + ")";
30+
}
31+
}
32+
33+
public class Foo
34+
{
35+
public string __repr__()
36+
{
37+
return "I implement __repr__() but not ToString()!";
38+
}
39+
}
40+
41+
public class Bar
42+
{
43+
public override string ToString()
44+
{
45+
return "I implement ToString() but not __repr__()!";
46+
}
47+
}
48+
49+
public class BazBase
50+
{
51+
public override string ToString()
52+
{
53+
return "Base class implementing ToString()!";
54+
}
55+
}
56+
57+
public class BazMiddle : BazBase
58+
{
59+
public override string ToString()
60+
{
61+
return "Middle class implementing ToString()!";
62+
}
63+
}
64+
65+
//implements ToString via BazMiddle
66+
public class Baz : BazMiddle
67+
{
68+
69+
}
70+
71+
public class Quux
72+
{
73+
public string ToString(string format)
74+
{
75+
return "I implement ToString() with an argument!";
76+
}
77+
}
78+
79+
public class QuuzBase
80+
{
81+
protected string __repr__()
82+
{
83+
return "I implement __repr__ but it isn't public!";
84+
}
85+
}
86+
87+
public class Quuz : QuuzBase
88+
{
89+
90+
}
91+
92+
public class Corge
93+
{
94+
public string __repr__(int i)
95+
{
96+
return "__repr__ implemention with input parameter!";
97+
}
98+
}
99+
100+
public class Grault
101+
{
102+
public int __repr__()
103+
{
104+
return "__repr__ implemention with wrong return type!".Length;
105+
}
106+
}
107+
}
108+
}

src/tests/test_exceptions.py

+2-5
Original file line numberDiff line numberDiff line change
@@ -288,11 +288,8 @@ def test_python_compat_of_managed_exceptions():
288288

289289
assert e.args == (msg,)
290290
assert isinstance(e.args, tuple)
291-
if PY3:
292-
strexp = "OverflowException('Simple message"
293-
assert repr(e)[:len(strexp)] == strexp
294-
elif PY2:
295-
assert repr(e) == "OverflowException(u'Simple message',)"
291+
strexp = "OverflowException('Simple message"
292+
assert repr(e)[:len(strexp)] == strexp
296293

297294

298295
def test_exception_is_instance_of_system_object():

src/tests/test_repr.py

+74
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
# -*- coding: utf-8 -*-
2+
3+
"""Test __repr__ output"""
4+
5+
import System
6+
import pytest
7+
from Python.Test import ReprTest
8+
9+
def test_basic():
10+
"""Test Point class which implements both ToString and __repr__ without inheritance"""
11+
ob = ReprTest.Point(1,2)
12+
# point implements ToString() and __repr__()
13+
assert ob.__repr__() == "Point(1,2)"
14+
assert str(ob) == "Python.Test.ReprTest+Point: X=1, Y=2"
15+
16+
def test_system_string():
17+
"""Test system string"""
18+
ob = System.String("hello")
19+
assert str(ob) == "hello"
20+
assert "<System.String object at " in ob.__repr__()
21+
22+
def test_repr_only():
23+
"""Test class implementing __repr__() but not ToString()"""
24+
ob = ReprTest.Foo()
25+
assert str(ob) == "I implement __repr__() but not ToString()!"
26+
assert ob.__repr__() == "I implement __repr__() but not ToString()!"
27+
28+
def test_str_only():
29+
"""Test class implementing ToString() but not __repr__()"""
30+
ob = ReprTest.Bar()
31+
assert str(ob) == "I implement ToString() but not __repr__()!"
32+
assert "<Python.Test.Bar object at " in ob.__repr__()
33+
34+
def test_hierarchy1():
35+
"""Test inheritance heirarchy with base & middle class implementing ToString"""
36+
ob1 = ReprTest.BazBase()
37+
assert str(ob1) == "Base class implementing ToString()!"
38+
assert "<Python.Test.BazBase object at " in ob1.__repr__()
39+
40+
ob2 = ReprTest.BazMiddle()
41+
assert str(ob2) == "Middle class implementing ToString()!"
42+
assert "<Python.Test.BazMiddle object at " in ob2.__repr__()
43+
44+
ob3 = ReprTest.Baz()
45+
assert str(ob3) == "Middle class implementing ToString()!"
46+
assert "<Python.Test.Baz object at " in ob3.__repr__()
47+
48+
def bad_tostring():
49+
"""Test ToString that can't be used by str()"""
50+
ob = ReprTest.Quux()
51+
assert str(ob) == "Python.Test.ReprTest+Quux"
52+
assert "<Python.Test.Quux object at " in ob.__repr__()
53+
54+
def bad_repr():
55+
"""Test incorrect implementation of repr"""
56+
ob1 = ReprTest.QuuzBase()
57+
assert str(ob1) == "Python.Test.ReprTest+QuuzBase"
58+
assert "<Python.Test.QuuzBase object at " in ob.__repr__()
59+
60+
ob2 = ReprTest.Quuz()
61+
assert str(ob2) == "Python.Test.ReprTest+Quuz"
62+
assert "<Python.Test.Quuz object at " in ob.__repr__()
63+
64+
ob3 = ReprTest.Corge()
65+
with pytest.raises(Exception):
66+
str(ob3)
67+
with pytest.raises(Exception):
68+
ob3.__repr__()
69+
70+
ob4 = ReprTest.Grault()
71+
with pytest.raises(Exception):
72+
str(ob4)
73+
with pytest.raises(Exception):
74+
ob4.__repr__()

src/tests/tests.pyproj

+1
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
<Compile Include="test_recursive_types.py" />
6060
<Compile Include="test_subclass.py" />
6161
<Compile Include="test_thread.py" />
62+
<Compile Include="test_repr.py" />
6263
<Compile Include="utils.py" />
6364
<Compile Include="fixtures\argv-fixture.py" />
6465
</ItemGroup>

0 commit comments

Comments
 (0)