From b88be08ce1c2a35f24316005607a83520780dbae Mon Sep 17 00:00:00 2001 From: Victor Milovanov Date: Tue, 25 Jun 2019 11:50:19 -0700 Subject: [PATCH] implemented __signature__ and __name__ on methodbinding --- CHANGELOG.md | 5 ++- src/embed_tests/Inspect.cs | 25 +++++++++++++ src/runtime/exceptions.cs | 1 + src/runtime/methodbinding.cs | 71 ++++++++++++++++++++++++++++++++++++ src/runtime/methodobject.cs | 12 +++++- src/runtime/runtime.cs | 3 ++ 6 files changed, 114 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c3075e2e2..5cb3fc14f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,8 +15,9 @@ This document follows the conventions laid out in [Keep a CHANGELOG][]. - Ability to implement delegates with `ref` and `out` parameters in Python, by returning the modified parameter values in a tuple. ([#1355][i1355]) - `PyType` - a wrapper for Python type objects, that also permits creating new heap types from `TypeSpec` - Improved exception handling: -- exceptions can now be converted with codecs -- `InnerException` and `__cause__` are propagated properly + * exceptions can now be converted with codecs + * `InnerException` and `__cause__` are propagated properly +- `__name__` and `__signature__` to reflected .NET methods - .NET collection types now implement standard Python collection interfaces from `collections.abc`. See [Mixins/collections.py](src/runtime/Mixins/collections.py). - .NET arrays implement Python buffer protocol diff --git a/src/embed_tests/Inspect.cs b/src/embed_tests/Inspect.cs index 823a0169a..8ff94e02c 100644 --- a/src/embed_tests/Inspect.cs +++ b/src/embed_tests/Inspect.cs @@ -30,5 +30,30 @@ public void InstancePropertiesVisibleOnClass() var pyProp = (PropertyObject)ManagedType.GetManagedObject(property.Reference); Assert.AreEqual(nameof(Uri.AbsoluteUri), pyProp.info.Value.Name); } + + [Test] + public void BoundMethodsAreInspectable() + { + using var scope = Py.CreateScope(); + try + { + scope.Import("inspect"); + } + catch (PythonException) + { + Assert.Inconclusive("Python build does not include inspect module"); + return; + } + + var obj = new Class(); + scope.Set(nameof(obj), obj); + using var spec = scope.Eval($"inspect.getfullargspec({nameof(obj)}.{nameof(Class.Method)})"); + } + + class Class + { + public void Method(int a, int b = 10) { } + public void Method(int a, object b) { } + } } } diff --git a/src/runtime/exceptions.cs b/src/runtime/exceptions.cs index f1a06c328..8c09cd608 100644 --- a/src/runtime/exceptions.cs +++ b/src/runtime/exceptions.cs @@ -420,6 +420,7 @@ public static variables on the Exceptions class filled in from public static IntPtr IOError; public static IntPtr OSError; public static IntPtr ImportError; + public static IntPtr ModuleNotFoundError; public static IntPtr IndexError; public static IntPtr KeyError; public static IntPtr KeyboardInterrupt; diff --git a/src/runtime/methodbinding.cs b/src/runtime/methodbinding.cs index c1e729f9e..dcd2175b0 100644 --- a/src/runtime/methodbinding.cs +++ b/src/runtime/methodbinding.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Reflection; namespace Python.Runtime @@ -65,6 +66,67 @@ public static IntPtr mp_subscript(IntPtr tp, IntPtr idx) return mb.pyHandle; } + PyObject Signature + { + get + { + var infos = this.info.Valid ? new[] { this.info.Value } : this.m.info; + Type type = infos.Select(i => i.DeclaringType) + .OrderByDescending(t => t, new TypeSpecificityComparer()) + .First(); + infos = infos.Where(info => info.DeclaringType == type).ToArray(); + // this is a primitive version + // the overload with the maximum number of parameters should be used + MethodInfo primary = infos.OrderByDescending(i => i.GetParameters().Length).First(); + var primaryParameters = primary.GetParameters(); + PyObject signatureClass = Runtime.InspectModule.GetAttr("Signature"); + var primaryReturn = primary.ReturnParameter; + + using var parameters = new PyList(); + using var parameterClass = primaryParameters.Length > 0 ? Runtime.InspectModule.GetAttr("Parameter") : null; + using var positionalOrKeyword = parameterClass?.GetAttr("POSITIONAL_OR_KEYWORD"); + for (int i = 0; i < primaryParameters.Length; i++) + { + var parameter = primaryParameters[i]; + var alternatives = infos.Select(info => + { + ParameterInfo[] altParamters = info.GetParameters(); + return i < altParamters.Length ? altParamters[i] : null; + }).Where(p => p != null); + using var defaultValue = alternatives + .Select(alternative => alternative.DefaultValue != DBNull.Value ? alternative.DefaultValue.ToPython() : null) + .FirstOrDefault(v => v != null) ?? parameterClass.GetAttr("empty"); + + if (alternatives.Any(alternative => alternative.Name != parameter.Name)) + { + return signatureClass.Invoke(); + } + + using var args = new PyTuple(new[] { parameter.Name.ToPython(), positionalOrKeyword }); + using var kw = new PyDict(); + if (defaultValue is not null) + { + kw["default"] = defaultValue; + } + using var parameterInfo = parameterClass.Invoke(args: args, kw: kw); + parameters.Append(parameterInfo); + } + + // TODO: add return annotation + return signatureClass.Invoke(parameters); + } + } + + struct TypeSpecificityComparer : IComparer + { + public int Compare(Type a, Type b) + { + if (a == b) return 0; + if (a.IsSubclassOf(b)) return 1; + if (b.IsSubclassOf(a)) return -1; + throw new NotSupportedException(); + } + } /// /// MethodBinding __getattribute__ implementation. @@ -91,6 +153,15 @@ public static IntPtr tp_getattro(IntPtr ob, IntPtr key) case "Overloads": var om = new OverloadMapper(self.m, self.target); return om.pyHandle; + case "__signature__" when Runtime.InspectModule is not null: + var sig = self.Signature; + if (sig is null) + { + return Runtime.PyObject_GenericGetAttr(ob, key); + } + return sig.NewReferenceOrNull().DangerousMoveToPointerOrNull(); + case "__name__": + return self.m.GetName().DangerousMoveToPointerOrNull(); default: return Runtime.PyObject_GenericGetAttr(ob, key); } diff --git a/src/runtime/methodobject.cs b/src/runtime/methodobject.cs index 2787ec999..655ac4b43 100644 --- a/src/runtime/methodobject.cs +++ b/src/runtime/methodobject.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; -using System.Reflection; using System.Linq; +using System.Reflection; namespace Python.Runtime { @@ -101,6 +101,16 @@ internal IntPtr GetDocString() return doc; } + internal NewReference GetName() + { + var names = new HashSet(binder.GetMethods().Select(m => m.Name)); + if (names.Count != 1) { + Exceptions.SetError(Exceptions.AttributeError, "a method has no name"); + return default; + } + return NewReference.DangerousFromPointer(Runtime.PyString_FromString(names.First())); + } + /// /// This is a little tricky: a class can actually have a static method diff --git a/src/runtime/runtime.cs b/src/runtime/runtime.cs index 8cdd6eb70..d2653a510 100644 --- a/src/runtime/runtime.cs +++ b/src/runtime/runtime.cs @@ -178,6 +178,7 @@ internal static void Initialize(bool initSigs = false, ShutdownMode mode = Shutd AssemblyManager.UpdatePath(); clrInterop = GetModuleLazy("clr.interop"); + inspect = GetModuleLazy("inspect"); } private static void InitPyMembers() @@ -573,6 +574,8 @@ private static void MoveClrInstancesOnwershipToPython() internal static IntPtr PyNone; internal static IntPtr Error; + private static Lazy inspect; + internal static PyObject InspectModule => inspect.Value; private static Lazy clrInterop; internal static PyObject InteropModule => clrInterop.Value;