Skip to content

Commit 187b5dd

Browse files
committed
implemented __signature__ and __name__ on methodbinding
1 parent f64194c commit 187b5dd

File tree

7 files changed

+137
-5
lines changed

7 files changed

+137
-5
lines changed

CHANGELOG.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,9 @@ This document follows the conventions laid out in [Keep a CHANGELOG][].
1515
- Ability to implement delegates with `ref` and `out` parameters in Python, by returning the modified parameter values in a tuple. ([#1355][i1355])
1616
- `PyType` - a wrapper for Python type objects, that also permits creating new heap types from `TypeSpec`
1717
- Improved exception handling:
18-
- exceptions can now be converted with codecs
19-
- `InnerException` and `__cause__` are propagated properly
18+
* exceptions can now be converted with codecs
19+
* `InnerException` and `__cause__` are propagated properly
20+
- `__name__` and `__signature__` to reflected .NET methods
2021
- .NET collection types now implement standard Python collection interfaces from `collections.abc`.
2122
See [Mixins/collections.py](src/runtime/Mixins/collections.py).
2223
- .NET arrays implement Python buffer protocol

src/embed_tests/Inspect.cs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,5 +30,30 @@ public void InstancePropertiesVisibleOnClass()
3030
var pyProp = (PropertyObject)ManagedType.GetManagedObject(property.Reference);
3131
Assert.AreEqual(nameof(Uri.AbsoluteUri), pyProp.info.Value.Name);
3232
}
33+
34+
[Test]
35+
public void BoundMethodsAreInspectable()
36+
{
37+
using var scope = Py.CreateScope();
38+
try
39+
{
40+
scope.Import("inspect");
41+
}
42+
catch (PythonException)
43+
{
44+
Assert.Inconclusive("Python build does not include inspect module");
45+
return;
46+
}
47+
48+
var obj = new Class();
49+
scope.Set(nameof(obj), obj);
50+
using var spec = scope.Eval($"inspect.getfullargspec({nameof(obj)}.{nameof(Class.Method)})");
51+
}
52+
53+
class Class
54+
{
55+
public void Method(int a, int b = 10) { }
56+
public void Method(int a, object b) { }
57+
}
3358
}
3459
}

src/runtime/exceptions.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,7 @@ public static variables on the Exceptions class filled in from
420420
public static IntPtr IOError;
421421
public static IntPtr OSError;
422422
public static IntPtr ImportError;
423+
public static IntPtr ModuleNotFoundError;
423424
public static IntPtr IndexError;
424425
public static IntPtr KeyError;
425426
public static IntPtr KeyboardInterrupt;

src/runtime/methodbinding.cs

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Collections.Generic;
3+
using System.Linq;
34
using System.Reflection;
45

56
namespace Python.Runtime
@@ -65,6 +66,65 @@ public static IntPtr mp_subscript(IntPtr tp, IntPtr idx)
6566
return mb.pyHandle;
6667
}
6768

69+
PyObject Signature
70+
{
71+
get
72+
{
73+
var infos = this.info.Valid ? new[] { this.info.Value } : this.m.info;
74+
Type type = infos.Select(i => i.DeclaringType)
75+
.OrderByDescending(t => t, new TypeSpecificityComparer())
76+
.First();
77+
infos = infos.Where(info => info.DeclaringType == type).ToArray();
78+
// this is a primitive version
79+
// the overload with the maximum number of parameters should be used
80+
MethodInfo primary = infos.OrderByDescending(i => i.GetParameters().Length).First();
81+
var primaryParameters = primary.GetParameters();
82+
PyObject signatureClass = Runtime.InspectModule.GetAttr("Signature");
83+
var primaryReturn = primary.ReturnParameter;
84+
85+
using var parameters = new PyList();
86+
using var parameterClass = primaryParameters.Length > 0 ? Runtime.InspectModule.GetAttr("Parameter") : null;
87+
using var positionalOrKeyword = parameterClass?.GetAttr("POSITIONAL_OR_KEYWORD");
88+
for (int i = 0; i < primaryParameters.Length; i++)
89+
{
90+
var parameter = primaryParameters[i];
91+
var alternatives = infos.Select(info =>
92+
{
93+
ParameterInfo[] altParamters = info.GetParameters();
94+
return i < altParamters.Length ? altParamters[i] : null;
95+
}).Where(p => p != null);
96+
using var defaultValue = alternatives
97+
.Select(alternative => alternative.DefaultValue != DBNull.Value ? alternative.DefaultValue.ToPython() : null)
98+
.FirstOrDefault(v => v != null) ?? parameterClass.GetAttr("empty");
99+
100+
if (alternatives.Any(alternative => alternative.Name != parameter.Name))
101+
{
102+
return signatureClass.Invoke();
103+
}
104+
105+
using var args = new PyTuple(new[] { parameter.Name.ToPython(), positionalOrKeyword });
106+
using var kw = new PyDict();
107+
if (defaultValue is not null)
108+
{
109+
kw["default"] = defaultValue;
110+
}
111+
using var parameterInfo = parameterClass.Invoke(args: args, kw: kw);
112+
parameters.Append(parameterInfo);
113+
}
114+
115+
// TODO: add return annotation
116+
return signatureClass.Invoke(parameters);
117+
}
118+
}
119+
120+
struct TypeSpecificityComparer : IComparer<Type> {
121+
public int Compare(Type a, Type b) {
122+
if (a == b) return 0;
123+
if (a.IsSubclassOf(b)) return 1;
124+
if (b.IsSubclassOf(a)) return -1;
125+
throw new NotSupportedException();
126+
}
127+
}
68128

69129
/// <summary>
70130
/// MethodBinding __getattribute__ implementation.
@@ -91,6 +151,15 @@ public static IntPtr tp_getattro(IntPtr ob, IntPtr key)
91151
case "Overloads":
92152
var om = new OverloadMapper(self.m, self.target);
93153
return om.pyHandle;
154+
case "__signature__" when Runtime.InspectModule is not null:
155+
var sig = self.Signature;
156+
if (sig is null)
157+
{
158+
return Runtime.PyObject_GenericGetAttr(ob, key);
159+
}
160+
return sig.NewReferenceOrNull().DangerousMoveToPointerOrNull();
161+
case "__name__":
162+
return self.m.GetName().DangerousMoveToPointerOrNull();
94163
default:
95164
return Runtime.PyObject_GenericGetAttr(ob, key);
96165
}

src/runtime/methodobject.cs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
using System;
22
using System.Collections.Generic;
3-
using System.Reflection;
43
using System.Linq;
4+
using System.Reflection;
55

66
namespace Python.Runtime
77
{
@@ -101,6 +101,16 @@ internal IntPtr GetDocString()
101101
return doc;
102102
}
103103

104+
internal NewReference GetName()
105+
{
106+
var names = new HashSet<string>(binder.GetMethods().Select(m => m.Name));
107+
if (names.Count != 1) {
108+
Exceptions.SetError(Exceptions.AttributeError, "a method has no name");
109+
return default;
110+
}
111+
return NewReference.DangerousFromPointer(Runtime.PyString_FromString(names.First()));
112+
}
113+
104114

105115
/// <summary>
106116
/// This is a little tricky: a class can actually have a static method

src/runtime/pymodule.cs

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
#nullable enable
12
using System;
23

34
namespace Python.Runtime
@@ -6,7 +7,7 @@ public class PyModule : PyScope
67
{
78
internal PyModule(ref NewReference reference) : base(ref reference, PyScopeManager.Global) { }
89
public PyModule(PyObject o) : base(o.Reference, PyScopeManager.Global) { }
9-
public PyModule(string name, string filename = null) : this(Create(name, filename)) { }
10+
public PyModule(string name, string? filename = null) : this(Create(name, filename)) { }
1011

1112
/// <summary>
1213
/// Given a module or package name, import the module and return the resulting object.
@@ -19,6 +20,28 @@ public static PyObject Import(string name)
1920
return IsModule(op) ? new PyModule(ref op) : op.MoveToPyObject();
2021
}
2122

23+
/// <summary>
24+
/// Given a module or package name, import the module and return the resulting object.
25+
/// Returns <c>null</c> when module was not found (Python 3.6+).
26+
/// </summary>
27+
/// <param name="name">Fully-qualified module or package name</param>
28+
public static PyObject? TryImport(string name)
29+
{
30+
NewReference op = Runtime.PyImport_ImportModule(name);
31+
if (op.IsNull())
32+
{
33+
if (Exceptions.ModuleNotFoundError == IntPtr.Zero
34+
|| !PythonException.CurrentMatches(Exceptions.ModuleNotFoundError))
35+
{
36+
throw PythonException.ThrowLastAsClrException();
37+
}
38+
39+
Exceptions.Clear();
40+
return null;
41+
}
42+
return IsModule(op) ? new PyModule(ref op) : op.MoveToPyObject();
43+
}
44+
2245
/// <summary>
2346
/// Reloads the module, and returns the updated object
2447
/// </summary>
@@ -38,7 +61,7 @@ public static PyModule FromString(string name, string code)
3861
return new PyModule(ref m);
3962
}
4063

41-
private static PyModule Create(string name, string filename=null)
64+
private static PyModule Create(string name, string? filename = null)
4265
{
4366
if(string.IsNullOrWhiteSpace(name))
4467
{

src/runtime/runtime.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,7 @@ internal static void Initialize(bool initSigs = false, ShutdownMode mode = Shutd
180180
AssemblyManager.UpdatePath();
181181

182182
clrInterop = GetModuleLazy("clr.interop");
183+
inspect = GetModuleLazy("inspect");
183184
}
184185

185186
private static void InitPyMembers()
@@ -581,6 +582,8 @@ private static void MoveClrInstancesOnwershipToPython()
581582
internal static IntPtr PyNone;
582583
internal static IntPtr Error;
583584

585+
private static Lazy<PyObject> inspect;
586+
internal static PyObject InspectModule => inspect.Value;
584587
private static Lazy<PyObject> clrInterop;
585588
internal static PyObject InteropModule => clrInterop.Value;
586589

0 commit comments

Comments
 (0)