Skip to content

Commit 825c647

Browse files
committed
implemented __signature__ and __name__ on methodbinding
1 parent 1e32d8c commit 825c647

File tree

7 files changed

+134
-7
lines changed

7 files changed

+134
-7
lines changed

CHANGELOG.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,10 @@ This document follows the conventions laid out in [Keep a CHANGELOG][].
1414
- Add GetPythonThreadID and Interrupt methods in PythonEngine
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`
17-
- Improved exception handling:
18-
- exceptions can now be converted with codecs
19-
- `InnerException` and `__cause__` are propagated properly
17+
- Improved exception handling:
18+
- exceptions can now be converted with codecs
19+
- `__name__` and `__signature__` to reflected .NET methods
20+
- `InnerException` and `__cause__` are propagated properly
2021

2122
### Changed
2223
- Drop support for Python 2, 3.4, and 3.5

src/embed_tests/Inspect.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,5 +30,21 @@ 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+
var obj = new Class();
38+
using var scope = Py.CreateScope();
39+
scope.Import("inspect");
40+
scope.Set(nameof(obj), obj);
41+
var spec = scope.Eval($"inspect.getfullargspec({nameof(obj)}.{nameof(Class.Method)})");
42+
}
43+
44+
class Class
45+
{
46+
public void Method(int a, int b = 10) { }
47+
public void Method(int a, object b) { }
48+
}
3349
}
3450
}

src/runtime/exceptions.cs

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

src/runtime/methodbinding.cs

Lines changed: 68 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,59 @@ public static IntPtr mp_subscript(IntPtr tp, IntPtr idx)
6566
return mb.pyHandle;
6667
}
6768

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

69123
/// <summary>
70124
/// MethodBinding __getattribute__ implementation.
@@ -91,6 +145,20 @@ public static IntPtr tp_getattro(IntPtr ob, IntPtr key)
91145
case "Overloads":
92146
var om = new OverloadMapper(self.m, self.target);
93147
return om.pyHandle;
148+
case "__signature__" when Runtime.InspectModule is not null:
149+
var sig = self.Signature;
150+
if (sig is null)
151+
{
152+
return Runtime.PyObject_GenericGetAttr(ob, key);
153+
}
154+
return sig.Reference.IsNull
155+
? IntPtr.Zero
156+
: sig.NewReferenceOrNull().DangerousGetAddress();
157+
case "__name__":
158+
var pyName = self.m.GetName();
159+
return pyName == IntPtr.Zero
160+
? IntPtr.Zero
161+
: Runtime.SelfIncRef(pyName);
94162
default:
95163
return Runtime.PyObject_GenericGetAttr(ob, key);
96164
}

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 IntPtr 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 IntPtr.Zero;
110+
}
111+
return 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: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,8 @@ internal static void Initialize(bool initSigs = false, ShutdownMode mode = Shutd
178178
}
179179
XDecref(item);
180180
AssemblyManager.UpdatePath();
181+
182+
inspect = GetInspectModuleLazy();
181183
}
182184

183185
private static void InitPyMembers()
@@ -291,6 +293,9 @@ private static IntPtr Get_PyObject_NextNotImplemented()
291293
return iternext;
292294
}
293295

296+
private static Lazy<PyObject?> GetInspectModuleLazy()
297+
=> new Lazy<PyObject?>(() => PyModule.TryImport("inspect"), isThreadSafe: false);
298+
294299
/// <summary>
295300
/// Tries to downgrade the shutdown mode, if possible.
296301
/// The only possibles downgrades are:
@@ -586,6 +591,9 @@ public static PyObject None
586591
}
587592
}
588593

594+
private static Lazy<PyObject?> inspect;
595+
internal static PyObject? InspectModule => inspect.Value;
596+
589597
/// <summary>
590598
/// Check if any Python Exceptions occurred.
591599
/// If any exist throw new PythonException.
@@ -2038,7 +2046,7 @@ internal static bool PyType_IsSameAsOrSubtype(BorrowedReference type, BorrowedRe
20382046
internal static NewReference PyType_FromSpecWithBases(in NativeTypeSpec spec, BorrowedReference bases) => Delegates.PyType_FromSpecWithBases(in spec, bases);
20392047

20402048
/// <summary>
2041-
/// Finalize a type object. This should be called on all type objects to finish their initialization. This function is responsible for adding inherited slots from a types base class. Return 0 on success, or return -1 and sets an exception on error.
2049+
/// Finalize a type object. This should be called on all type objects to finish their initialization. This function is responsible for adding inherited slots from a type's base class. Return 0 on success, or return -1 and sets an exception on error.
20422050
/// </summary>
20432051

20442052
internal static int PyType_Ready(IntPtr type) => Delegates.PyType_Ready(type);

0 commit comments

Comments
 (0)