From 351d9ff22436d84fa27869a97ea54e9822d54721 Mon Sep 17 00:00:00 2001 From: Mohamed Koubaa Date: Sat, 19 Jan 2019 20:33:09 -0600 Subject: [PATCH 1/8] Provide hook to implement __repr__ Issue: 680 --- AUTHORS.md | 1 + CHANGELOG.md | 1 + src/runtime/classbase.cs | 68 +++++++++++++++++++++ src/runtime/exceptions.cs | 23 +++++++ src/testing/Python.Test.csproj | 1 + src/testing/ReprTest.cs | 108 +++++++++++++++++++++++++++++++++ src/tests/test_exceptions.py | 7 +-- src/tests/test_repr.py | 74 ++++++++++++++++++++++ src/tests/tests.pyproj | 1 + 9 files changed, 279 insertions(+), 5 deletions(-) create mode 100644 src/testing/ReprTest.cs create mode 100644 src/tests/test_repr.py diff --git a/AUTHORS.md b/AUTHORS.md index fe2d2b172..f8eff412e 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -36,6 +36,7 @@ - Luke Stratman ([@lstratman](https://github.com/lstratman)) - Konstantin Posudevskiy ([@konstantin-posudevskiy](https://github.com/konstantin-posudevskiy)) - Matthias Dittrich ([@matthid](https://github.com/matthid)) +- Mohamed Koubaa ([@koubaa](https://github.com/koubaa)) - Patrick Stewart ([@patstew](https://github.com/patstew)) - Raphael Nestler ([@rnestler](https://github.com/rnestler)) - Rickard Holmberg ([@rickardraysearch](https://github.com/rickardraysearch)) diff --git a/CHANGELOG.md b/CHANGELOG.md index a3816cd0c..beccf4b06 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ This document follows the conventions laid out in [Keep a CHANGELOG][]. - Incorporated reference-style links to issues and pull requests in the CHANGELOG ([#608][i608]) - Added detailed comments about aproaches and dangers to handle multi-app-domains ([#625][p625]) - Python 3.7 support, builds and testing added. Defaults changed from Python 3.6 to 3.7 ([#698][p698]) +- Added support for C# types to provide `__repr__` ([#680][p680]) ### Changed diff --git a/src/runtime/classbase.cs b/src/runtime/classbase.cs index 5846fa82a..825d969b2 100644 --- a/src/runtime/classbase.cs +++ b/src/runtime/classbase.cs @@ -233,7 +233,75 @@ public static IntPtr tp_str(IntPtr ob) } try { + //As per python doc: + //The return value must be a string object. If a class defines __repr__() but not __str__(), + //then __repr__() is also used when an “informal” string representation of instances of that + //class is required. + //In C#, everything provides ToString(), so the check here will be whether the type explicitly + //provides ToString() or if it is language provided (i.e. the fully qualified type name as a string) + + //First check which type in the object hierarchy provides ToString() + //ToString has two "official" overloads so loop over GetMethods to get the one without parameters + var instType = co.inst.GetType(); + foreach (var method in instType.GetMethods()) + { + + //TODO this could probably be done more cleanly with Linq + if (!method.IsPublic) continue; //skip private/protected methods + if (method.Name != "ToString") continue; //only look for ToString + if (method.DeclaringType == typeof(object)) continue; //ignore default from object + if (method.GetParameters().Length != 0) continue; //ignore Formatter overload of ToString + + //match! something other than object provides a parameter-less overload of ToString + return Runtime.PyString_FromString(co.inst.ToString()); + } + + //If the object defines __repr__, call it. + System.Reflection.MethodInfo reprMethodInfo = instType.GetMethod("__repr__"); + if (reprMethodInfo != null && reprMethodInfo.IsPublic) + { + var reprString = reprMethodInfo.Invoke(co.inst, null) as string; + return Runtime.PyString_FromString(reprString); + } + + //otherwise fallback to object's ToString() implementation return Runtime.PyString_FromString(co.inst.ToString()); + + } + catch (Exception e) + { + if (e.InnerException != null) + { + e = e.InnerException; + } + Exceptions.SetError(e); + return IntPtr.Zero; + } + } + + public static IntPtr tp_repr(IntPtr ob) + { + var co = GetManagedObject(ob) as CLRObject; + if (co == null) + { + return Exceptions.RaiseTypeError("invalid object"); + } + try + { + //if __repr__ is defined, use it + var instType = co.inst.GetType(); + System.Reflection.MethodInfo methodInfo = instType.GetMethod("__repr__"); + if (methodInfo != null && methodInfo.IsPublic) + { + var reprString = methodInfo.Invoke(co.inst, null) as string; + return Runtime.PyString_FromString(reprString); + } + + //otherwise use the standard object.__repr__(inst) + IntPtr args = Runtime.PyTuple_New(1); + Runtime.PyTuple_SetItem(args, 0, ob); + IntPtr reprFunc = Runtime.PyObject_GetAttrString(Runtime.PyBaseObjectType, "__repr__"); + return Runtime.PyObject_Call(reprFunc, args, IntPtr.Zero); } catch (Exception e) { diff --git a/src/runtime/exceptions.cs b/src/runtime/exceptions.cs index 8bed0abfd..31c367eb2 100644 --- a/src/runtime/exceptions.cs +++ b/src/runtime/exceptions.cs @@ -36,6 +36,29 @@ internal static Exception ToException(IntPtr ob) return e; } + /// + /// Exception __repr__ implementation + /// + public new static IntPtr tp_repr(IntPtr ob) + { + Exception e = ToException(ob); + if (e == null) + { + return Exceptions.RaiseTypeError("invalid object"); + } + string name = e.GetType().Name; + string message; + if (e.Message != String.Empty) + { + message = String.Format("{0}('{1}')", name, e.Message); + } + else + { + message = String.Format("{0}()", name); + } + return Runtime.PyUnicode_FromString(message); + } + /// /// Exception __str__ implementation /// diff --git a/src/testing/Python.Test.csproj b/src/testing/Python.Test.csproj index 27639ed5a..b15bd91de 100644 --- a/src/testing/Python.Test.csproj +++ b/src/testing/Python.Test.csproj @@ -91,6 +91,7 @@ + diff --git a/src/testing/ReprTest.cs b/src/testing/ReprTest.cs new file mode 100644 index 000000000..48e93683a --- /dev/null +++ b/src/testing/ReprTest.cs @@ -0,0 +1,108 @@ +using System; +using System.Text; + +namespace Python.Test +{ + /// + /// Supports repr unit tests. + /// + public class ReprTest + { + public class Point + { + public Point(double x, double y) + { + X = x; + Y = y; + } + + public double X { get; set; } + public double Y { get; set; } + + public override string ToString() + { + return base.ToString() + ": X=" + X.ToString() + ", Y=" + Y.ToString(); + } + + public string __repr__() + { + return "Point(" + X.ToString() + "," + Y.ToString() + ")"; + } + } + + public class Foo + { + public string __repr__() + { + return "I implement __repr__() but not ToString()!"; + } + } + + public class Bar + { + public override string ToString() + { + return "I implement ToString() but not __repr__()!"; + } + } + + public class BazBase + { + public override string ToString() + { + return "Base class implementing ToString()!"; + } + } + + public class BazMiddle : BazBase + { + public override string ToString() + { + return "Middle class implementing ToString()!"; + } + } + + //implements ToString via BazMiddle + public class Baz : BazMiddle + { + + } + + public class Quux + { + public string ToString(string format) + { + return "I implement ToString() with an argument!"; + } + } + + public class QuuzBase + { + protected string __repr__() + { + return "I implement __repr__ but it isn't public!"; + } + } + + public class Quuz : QuuzBase + { + + } + + public class Corge + { + public string __repr__(int i) + { + return "__repr__ implemention with input parameter!"; + } + } + + public class Grault + { + public int __repr__() + { + return "__repr__ implemention with wrong return type!".Length; + } + } + } +} diff --git a/src/tests/test_exceptions.py b/src/tests/test_exceptions.py index 08b00d77d..90558dd0f 100644 --- a/src/tests/test_exceptions.py +++ b/src/tests/test_exceptions.py @@ -288,11 +288,8 @@ def test_python_compat_of_managed_exceptions(): assert e.args == (msg,) assert isinstance(e.args, tuple) - if PY3: - strexp = "OverflowException('Simple message" - assert repr(e)[:len(strexp)] == strexp - elif PY2: - assert repr(e) == "OverflowException(u'Simple message',)" + strexp = "OverflowException('Simple message" + assert repr(e)[:len(strexp)] == strexp def test_exception_is_instance_of_system_object(): diff --git a/src/tests/test_repr.py b/src/tests/test_repr.py new file mode 100644 index 000000000..c7167eaea --- /dev/null +++ b/src/tests/test_repr.py @@ -0,0 +1,74 @@ +# -*- coding: utf-8 -*- + +"""Test __repr__ output""" + +import System +import pytest +from Python.Test import ReprTest + +def test_basic(): + """Test Point class which implements both ToString and __repr__ without inheritance""" + ob = ReprTest.Point(1,2) + # point implements ToString() and __repr__() + assert ob.__repr__() == "Point(1,2)" + assert str(ob) == "Python.Test.ReprTest+Point: X=1, Y=2" + +def test_system_string(): + """Test system string""" + ob = System.String("hello") + assert str(ob) == "hello" + assert " + From 2a002d74eb74c01cc257a5ba87590d43a8df0727 Mon Sep 17 00:00:00 2001 From: Mohamed Koubaa Date: Fri, 8 Mar 2019 17:50:51 -0600 Subject: [PATCH 2/8] fix casing --- src/testing/Python.Test.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/testing/Python.Test.csproj b/src/testing/Python.Test.csproj index b15bd91de..6bf5c2d22 100644 --- a/src/testing/Python.Test.csproj +++ b/src/testing/Python.Test.csproj @@ -91,7 +91,7 @@ - + @@ -112,4 +112,4 @@ - \ No newline at end of file + From 0916406aafa36c171553d50b4a4627d0b006a1a2 Mon Sep 17 00:00:00 2001 From: Mohamed Koubaa Date: Sun, 15 Sep 2019 13:32:15 -0500 Subject: [PATCH 3/8] update based on review --- src/runtime/classbase.cs | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/src/runtime/classbase.cs b/src/runtime/classbase.cs index 825d969b2..f604e4aa3 100644 --- a/src/runtime/classbase.cs +++ b/src/runtime/classbase.cs @@ -242,25 +242,18 @@ public static IntPtr tp_str(IntPtr ob) //First check which type in the object hierarchy provides ToString() //ToString has two "official" overloads so loop over GetMethods to get the one without parameters - var instType = co.inst.GetType(); - foreach (var method in instType.GetMethods()) - { - - //TODO this could probably be done more cleanly with Linq - if (!method.IsPublic) continue; //skip private/protected methods - if (method.Name != "ToString") continue; //only look for ToString - if (method.DeclaringType == typeof(object)) continue; //ignore default from object - if (method.GetParameters().Length != 0) continue; //ignore Formatter overload of ToString - + var method = type.GetMethod("ToString", new Type[]{}); + if (method.DeclaringTyppe != typeof(object)) + { //match! something other than object provides a parameter-less overload of ToString - return Runtime.PyString_FromString(co.inst.ToString()); - } + return Runtime.Pystring_FromString(co.inst.ToString()); + } //If the object defines __repr__, call it. System.Reflection.MethodInfo reprMethodInfo = instType.GetMethod("__repr__"); if (reprMethodInfo != null && reprMethodInfo.IsPublic) { - var reprString = reprMethodInfo.Invoke(co.inst, null) as string; + var reprString = (string)reprMethodInfo.Invoke(co.inst, null); return Runtime.PyString_FromString(reprString); } From 2fb6a430a0eb4f271cfdc9229bbe55ac8bc5b85e Mon Sep 17 00:00:00 2001 From: Mohamed Koubaa Date: Sun, 15 Sep 2019 13:41:28 -0500 Subject: [PATCH 4/8] fix whitespace --- src/runtime/classbase.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/runtime/classbase.cs b/src/runtime/classbase.cs index f604e4aa3..b633f1154 100644 --- a/src/runtime/classbase.cs +++ b/src/runtime/classbase.cs @@ -242,12 +242,12 @@ public static IntPtr tp_str(IntPtr ob) //First check which type in the object hierarchy provides ToString() //ToString has two "official" overloads so loop over GetMethods to get the one without parameters - var method = type.GetMethod("ToString", new Type[]{}); - if (method.DeclaringTyppe != typeof(object)) - { + var method = type.GetMethod("ToString", new Type[]{}); + if (method.DeclaringTyppe != typeof(object)) + { //match! something other than object provides a parameter-less overload of ToString - return Runtime.Pystring_FromString(co.inst.ToString()); - } + return Runtime.Pystring_FromString(co.inst.ToString()); + } //If the object defines __repr__, call it. System.Reflection.MethodInfo reprMethodInfo = instType.GetMethod("__repr__"); From 9fc5b5fc585f340b3c69172b558414616d12f65f Mon Sep 17 00:00:00 2001 From: Mohamed Koubaa Date: Sun, 15 Sep 2019 13:54:31 -0500 Subject: [PATCH 5/8] fix whitespace --- src/runtime/classbase.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/runtime/classbase.cs b/src/runtime/classbase.cs index b633f1154..dfdb2feb3 100644 --- a/src/runtime/classbase.cs +++ b/src/runtime/classbase.cs @@ -242,12 +242,12 @@ public static IntPtr tp_str(IntPtr ob) //First check which type in the object hierarchy provides ToString() //ToString has two "official" overloads so loop over GetMethods to get the one without parameters - var method = type.GetMethod("ToString", new Type[]{}); - if (method.DeclaringTyppe != typeof(object)) - { + var method = type.GetMethod("ToString", new Type[]{}); + if (method.DeclaringTyppe != typeof(object)) + { //match! something other than object provides a parameter-less overload of ToString - return Runtime.Pystring_FromString(co.inst.ToString()); - } + return Runtime.Pystring_FromString(co.inst.ToString()); + } //If the object defines __repr__, call it. System.Reflection.MethodInfo reprMethodInfo = instType.GetMethod("__repr__"); From 00b55db17ae0e3d15d0ff2bda6ead06fac06b040 Mon Sep 17 00:00:00 2001 From: Mohamed Koubaa Date: Sun, 15 Sep 2019 14:40:06 -0500 Subject: [PATCH 6/8] fix --- src/runtime/classbase.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/runtime/classbase.cs b/src/runtime/classbase.cs index dfdb2feb3..56bbaf35b 100644 --- a/src/runtime/classbase.cs +++ b/src/runtime/classbase.cs @@ -242,11 +242,12 @@ public static IntPtr tp_str(IntPtr ob) //First check which type in the object hierarchy provides ToString() //ToString has two "official" overloads so loop over GetMethods to get the one without parameters - var method = type.GetMethod("ToString", new Type[]{}); - if (method.DeclaringTyppe != typeof(object)) + var instType = co.inst.GetType(); + var method = instType.GetMethod("ToString", new Type[]{}); + if (method.DeclaringType != typeof(object)) { //match! something other than object provides a parameter-less overload of ToString - return Runtime.Pystring_FromString(co.inst.ToString()); + return Runtime.PyString_FromString(co.inst.ToString()); } //If the object defines __repr__, call it. From accc7978c657f0e524eb32f1d2a4cdc2a6e08db1 Mon Sep 17 00:00:00 2001 From: Mohamed Koubaa Date: Wed, 16 Oct 2019 17:56:01 -0500 Subject: [PATCH 7/8] Don't use repr in __str__. Clean up unneeded tests --- src/runtime/classbase.cs | 27 --------------------------- src/tests/test_repr.py | 6 ------ 2 files changed, 33 deletions(-) diff --git a/src/runtime/classbase.cs b/src/runtime/classbase.cs index 56bbaf35b..86b1de27b 100644 --- a/src/runtime/classbase.cs +++ b/src/runtime/classbase.cs @@ -233,34 +233,7 @@ public static IntPtr tp_str(IntPtr ob) } try { - //As per python doc: - //The return value must be a string object. If a class defines __repr__() but not __str__(), - //then __repr__() is also used when an “informal” string representation of instances of that - //class is required. - //In C#, everything provides ToString(), so the check here will be whether the type explicitly - //provides ToString() or if it is language provided (i.e. the fully qualified type name as a string) - - //First check which type in the object hierarchy provides ToString() - //ToString has two "official" overloads so loop over GetMethods to get the one without parameters - var instType = co.inst.GetType(); - var method = instType.GetMethod("ToString", new Type[]{}); - if (method.DeclaringType != typeof(object)) - { - //match! something other than object provides a parameter-less overload of ToString - return Runtime.PyString_FromString(co.inst.ToString()); - } - - //If the object defines __repr__, call it. - System.Reflection.MethodInfo reprMethodInfo = instType.GetMethod("__repr__"); - if (reprMethodInfo != null && reprMethodInfo.IsPublic) - { - var reprString = (string)reprMethodInfo.Invoke(co.inst, null); - return Runtime.PyString_FromString(reprString); - } - - //otherwise fallback to object's ToString() implementation return Runtime.PyString_FromString(co.inst.ToString()); - } catch (Exception e) { diff --git a/src/tests/test_repr.py b/src/tests/test_repr.py index c7167eaea..d120b0c4c 100644 --- a/src/tests/test_repr.py +++ b/src/tests/test_repr.py @@ -19,12 +19,6 @@ def test_system_string(): assert str(ob) == "hello" assert " Date: Wed, 16 Oct 2019 17:59:55 -0500 Subject: [PATCH 8/8] fix leak --- src/runtime/classbase.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/runtime/classbase.cs b/src/runtime/classbase.cs index 86b1de27b..41636c404 100644 --- a/src/runtime/classbase.cs +++ b/src/runtime/classbase.cs @@ -268,7 +268,10 @@ public static IntPtr tp_repr(IntPtr ob) IntPtr args = Runtime.PyTuple_New(1); Runtime.PyTuple_SetItem(args, 0, ob); IntPtr reprFunc = Runtime.PyObject_GetAttrString(Runtime.PyBaseObjectType, "__repr__"); - return Runtime.PyObject_Call(reprFunc, args, IntPtr.Zero); + var output = Runtime.PyObject_Call(reprFunc, args, IntPtr.Zero); + Runtime.XDecref(args); + Runtime.XDecref(reprFunc); + return output; } catch (Exception e) {