forked from pythonnet/pythonnet
-
Notifications
You must be signed in to change notification settings - Fork 31
Expand file tree
/
Copy pathMethodSignatureFormatter.cs
More file actions
297 lines (277 loc) · 11.5 KB
/
Copy pathMethodSignatureFormatter.cs
File metadata and controls
297 lines (277 loc) · 11.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
namespace Python.Runtime
{
/// <summary>
/// Formats method and constructor signatures the way Python callers see them
/// (snake_case names, Python type names). Used to hint the available overloads
/// in error messages when a call cannot be matched to any of them.
/// </summary>
public static class MethodSignatureFormatter
{
/// <summary>
/// Formats the signatures of the candidate overloads as an error message hint,
/// so the caller can see what the method expects, e.g.
/// "The following overloads are available:" followed by one signature per line.
/// Overloads taking PyObject parameters are skipped: they accept any Python
/// object and carry no type information (they are typically the overloads that
/// just rejected the value). If every candidate takes a PyObject, they are shown
/// anyway rather than producing no hint at all.
/// Returns an empty string if there are no signatures to show.
/// </summary>
/// <param name="methods">The candidate overloads</param>
/// <param name="maxShown">The maximum number of signatures to include</param>
/// <param name="displayName">Optional name to display for the methods, e.g. the type
/// name for constructors instead of the special <c>.ctor</c> token</param>
public static string FormatOverloads(IEnumerable<MethodBase> methods, int maxShown = 10, string displayName = null)
{
if (methods == null)
{
return string.Empty;
}
// Building this only runs on error paths; never let it throw and mask
// the original failure.
try
{
var candidates = methods.Where(method => method != null).ToList();
var withoutPyObject = candidates.Where(method => !TakesPyObject(method)).ToList();
if (withoutPyObject.Count > 0)
{
candidates = withoutPyObject;
}
// Distinct signatures, preserving order. Snake-cased duplicates and
// repeated overloads collapse into a single entry.
var signatures = new List<string>();
var seen = new HashSet<string>();
foreach (var method in candidates)
{
var signature = FormatSignature(method, displayName);
if (seen.Add(signature))
{
signatures.Add(signature);
}
}
if (signatures.Count == 0)
{
return string.Empty;
}
var to = new StringBuilder(signatures.Count == 1
? "The expected signature is:"
: "The following overloads are available:");
for (var i = 0; i < signatures.Count && i < maxShown; i++)
{
to.Append("\n ").Append(signatures[i]);
}
if (signatures.Count > maxShown)
{
to.Append($"\n ... and {signatures.Count - maxShown} more");
}
return to.ToString();
}
catch
{
// Best-effort hint only.
return string.Empty;
}
}
/// <summary>
/// Formats a method/constructor as a Python signature: snake_case name and
/// parameters annotated with the Python types a Python caller uses, e.g.
/// <c>range_consolidator(range: int, selector: Callable[[IBaseData], float] = None)</c>.
/// The constructor's special <c>.ctor</c> token is left as-is unless
/// <paramref name="displayName"/> is provided.
/// </summary>
public static string FormatSignature(MethodBase method, string displayName = null)
{
var to = new StringBuilder();
to.Append(displayName ?? SnakeCaseName(method)).Append('(');
var parameters = method.GetParameters();
for (var i = 0; i < parameters.Length; i++)
{
if (i > 0)
{
to.Append(", ");
}
var parameter = parameters[i];
if (parameter.IsDefined(typeof(ParamArrayAttribute), false))
{
// Python variadic form; annotate with the element type
var elementType = parameter.ParameterType.IsArray
? parameter.ParameterType.GetElementType()
: parameter.ParameterType;
to.Append('*').Append(parameter.Name.ToSnakeCase()).Append(": ").Append(FormatType(elementType));
continue;
}
to.Append(parameter.Name.ToSnakeCase()).Append(": ").Append(FormatType(parameter.ParameterType));
if (parameter.IsOptional)
{
to.Append(" = ").Append(FormatDefaultValue(parameter.DefaultValue));
}
}
to.Append(')');
return to.ToString();
}
/// <summary>
/// The snake_case name a Python caller uses for the given method. Constructors
/// keep their special <c>.ctor</c> token (a Python caller invokes the type).
/// </summary>
internal static string SnakeCaseName(MethodBase method)
{
return method.IsConstructor ? method.Name : method.Name.ToSnakeCase();
}
/// <summary>
/// Determines whether any of the method's parameters is a PyObject
/// </summary>
private static bool TakesPyObject(MethodBase method)
{
return method.GetParameters().Any(parameter =>
{
var type = parameter.ParameterType;
if (type.IsByRef)
{
type = type.GetElementType();
}
return typeof(PyObject).IsAssignableFrom(type);
});
}
/// <summary>
/// Produces the Python-side name for a CLR type, following the conversions the
/// runtime performs on arguments: primitives map to their Python equivalents
/// (str, int, float, bool, datetime, timedelta), Nullable to Optional, delegates
/// to Callable, list/dictionary shapes to List/Dict and PyObject/object to Any.
/// CLR types without a Python equivalent keep their name, with generics rendered
/// as <c>Name[Arg1, Arg2]</c>.
/// </summary>
internal static string FormatType(Type type)
{
if (type.IsByRef)
{
type = type.GetElementType();
}
var underlying = Nullable.GetUnderlyingType(type);
if (underlying != null)
{
return $"Optional[{FormatType(underlying)}]";
}
if (type == typeof(void))
{
return "None";
}
if (type == typeof(TimeSpan))
{
return "timedelta";
}
if (type == typeof(object))
{
return "Any";
}
if (typeof(Type).IsAssignableFrom(type))
{
return "type";
}
// pythonnet wrapper parameters accept any Python object of the matching shape
if (type == typeof(PyList))
{
return "List[Any]";
}
if (type == typeof(PyDict))
{
return "Dict[Any, Any]";
}
if (typeof(PyObject).IsAssignableFrom(type))
{
return "Any";
}
if (type.IsArray)
{
return $"List[{FormatType(type.GetElementType())}]";
}
if (typeof(Delegate).IsAssignableFrom(type) && !type.ContainsGenericParameters)
{
var invoke = type.GetMethod("Invoke");
if (invoke != null)
{
var args = string.Join(", ", invoke.GetParameters().Select(p => FormatType(p.ParameterType)));
return $"Callable[[{args}], {FormatType(invoke.ReturnType)}]";
}
}
// Enums have an integer type code but keep their Python-visible name
if (!type.IsEnum)
{
switch (Type.GetTypeCode(type))
{
case TypeCode.Boolean:
return "bool";
case TypeCode.Char:
case TypeCode.String:
return "str";
case TypeCode.SByte:
case TypeCode.Byte:
case TypeCode.Int16:
case TypeCode.UInt16:
case TypeCode.Int32:
case TypeCode.UInt32:
case TypeCode.Int64:
case TypeCode.UInt64:
return "int";
case TypeCode.Single:
case TypeCode.Double:
case TypeCode.Decimal:
return "float";
case TypeCode.DateTime:
return "datetime";
}
}
if (type.IsGenericType)
{
var definition = type.GetGenericTypeDefinition();
var genericArguments = type.GetGenericArguments();
// list and dictionary shapes the runtime converts from Python lists/dicts
if (definition == typeof(List<>) || definition == typeof(IList<>) ||
definition == typeof(IEnumerable<>) || definition == typeof(ICollection<>) ||
definition == typeof(IReadOnlyList<>) || definition == typeof(IReadOnlyCollection<>))
{
return $"List[{FormatType(genericArguments[0])}]";
}
if (definition == typeof(Dictionary<,>) || definition == typeof(IDictionary<,>) ||
definition == typeof(IReadOnlyDictionary<,>) || definition == typeof(KeyValuePair<,>))
{
return $"Dict[{FormatType(genericArguments[0])}, {FormatType(genericArguments[1])}]";
}
var name = type.Name;
var tick = name.IndexOf('`');
if (tick >= 0)
{
name = name.Substring(0, tick);
}
var args = genericArguments.Select(FormatType);
return $"{name}[{string.Join(", ", args)}]";
}
return type.Name;
}
private static string FormatDefaultValue(object value)
{
if (value == null || value is DBNull)
{
return "None";
}
if (value is string s)
{
return $"\"{s}\"";
}
if (value is bool b)
{
return b ? "True" : "False";
}
if (value is Enum e)
{
// Render enum defaults the way Python callers access them, e.g. Resolution.DAILY
return $"{e.GetType().Name}.{e.ToString().ToSnakeCase(constant: true)}";
}
return value.ToString();
}
}
}