Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions src/embed_tests/TestFloatToIntConversion.cs
Original file line number Diff line number Diff line change
Expand Up @@ -93,16 +93,19 @@ public void ErrorMessage_SingleOverload_ShowsExpectedSignature()
{
var ex = Assert.Throws<PythonException>(() => Call("single_ctor", 5.5));
StringAssert.Contains("The expected signature is:", ex.Message);
StringAssert.Contains("Int32 value", ex.Message);
StringAssert.Contains("value: int", ex.Message);
}

[Test]
public void ErrorMessage_MultipleOverloads_ListsCandidates()
{
var ex = Assert.Throws<PythonException>(() => Call("overloaded_ctor", 5.5));
StringAssert.Contains("The following overloads are available:", ex.Message);
// The int overload is surfaced, hinting an integer was expected.
StringAssert.Contains("Int32 range", ex.Message);
// The int overload is surfaced, hinting an integer was expected. The
// PyObject overload is skipped (it carries no type information), which
// leaves a single hinted signature here.
StringAssert.Contains("The expected signature is:", ex.Message);
StringAssert.Contains("range: int", ex.Message);
StringAssert.DoesNotContain("volume_selector", ex.Message);
}

// The hinted signatures use the snake_case name Python callers use, not the
Expand Down
162 changes: 162 additions & 0 deletions src/embed_tests/TestMethodSignatureFormatter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
using System;
using System.Collections.Generic;
using NUnit.Framework;
using Python.Runtime;

namespace Python.EmbeddingTest
{
/// <summary>
/// The overload hint signatures must show the Python types a caller uses,
/// following the conversions the runtime performs on arguments.
/// </summary>
public class TestMethodSignatureFormatter
{
private static string SignatureOf(string methodName, string displayName = null)
{
return MethodSignatureFormatter.FormatSignature(typeof(SampleTarget).GetMethod(methodName), displayName);
}

[Test]
public void FormatsPrimitivesAsPythonTypes()
{
Assert.AreEqual(
"primitives(count: int, price: float, ratio: float, scale: float, flag: bool, name: str, letter: str)",
SignatureOf(nameof(SampleTarget.Primitives)));
}

[Test]
public void FormatsTimeTypesAsDatetimeAndTimedelta()
{
Assert.AreEqual(
"time_types(time: datetime, period: timedelta)",
SignatureOf(nameof(SampleTarget.TimeTypes)));
}

[Test]
public void FormatsNullablesAsOptional()
{
Assert.AreEqual(
"nullables(start_time: Optional[timedelta] = None, max_count: Optional[int] = None)",
SignatureOf(nameof(SampleTarget.Nullables)));
}

[Test]
public void FormatsDelegatesAsCallable()
{
Assert.AreEqual(
"delegates(selector: Callable[[datetime], int], handler: Callable[[str], None])",
SignatureOf(nameof(SampleTarget.Delegates)));
}

[Test]
public void FormatsCollectionsAsListAndDict()
{
Assert.AreEqual(
"collections(names: List[str], values: List[int], prices: List[float], lookup: Dict[str, float])",
SignatureOf(nameof(SampleTarget.Collections)));
}

[Test]
public void FormatsObjectAndPyObjectAsAny()
{
Assert.AreEqual(
"any_types(anything: Any, py_object: Any, py_list: List[Any], py_dict: Dict[Any, Any])",
SignatureOf(nameof(SampleTarget.AnyTypes)));
}

[Test]
public void KeepsClrOnlyTypeNames()
{
Assert.AreEqual(
"clr_types(address: Uri, mode: StringComparison = StringComparison.ORDINAL)",
SignatureOf(nameof(SampleTarget.ClrTypes)));
}

[Test]
public void RendersConstructorsWithDisplayName()
{
var signature = MethodSignatureFormatter.FormatSignature(
typeof(SampleTarget).GetConstructors()[0], nameof(SampleTarget));
Assert.AreEqual("SampleTarget(period: timedelta, start_time: Optional[timedelta] = None)", signature);
}

[Test]
public void SkipsPyObjectOverloadsFromHints()
{
var hint = MethodSignatureFormatter.FormatOverloads(typeof(MixedOverloadsTarget).GetConstructors(),
displayName: nameof(MixedOverloadsTarget));

StringAssert.Contains("The following overloads are available:", hint);
StringAssert.Contains("MixedOverloadsTarget(period: timedelta)", hint);
StringAssert.Contains("MixedOverloadsTarget(max_count: int)", hint);
StringAssert.DoesNotContain("py_func", hint);
}

[Test]
public void ShowsPyObjectOverloadsWhenThereIsNothingElseToHint()
{
var hint = MethodSignatureFormatter.FormatOverloads(typeof(PyObjectOnlyTarget).GetConstructors(),
displayName: nameof(PyObjectOnlyTarget));

StringAssert.Contains("The expected signature is:", hint);
StringAssert.Contains("PyObjectOnlyTarget(py_func: Any)", hint);
}

private class MixedOverloadsTarget
{
public MixedOverloadsTarget(TimeSpan period)
{
}

public MixedOverloadsTarget(int maxCount)
{
}

public MixedOverloadsTarget(PyObject pyFunc)
{
}
}

private class PyObjectOnlyTarget
{
public PyObjectOnlyTarget(PyObject pyFunc)
{
}
}

private class SampleTarget
{
public SampleTarget(TimeSpan period, TimeSpan? startTime = null)
{
}

public void Primitives(int count, double price, decimal ratio, float scale, bool flag, string name, char letter)
{
}

public void TimeTypes(DateTime time, TimeSpan period)
{
}

public void Nullables(TimeSpan? startTime = null, int? maxCount = null)
{
}

public void Delegates(Func<DateTime, int> selector, Action<string> handler)
{
}

public void Collections(List<string> names, IEnumerable<int> values, decimal[] prices, Dictionary<string, decimal> lookup)
{
}

public void AnyTypes(object anything, PyObject pyObject, PyList pyList, PyDict pyDict)
{
}

public void ClrTypes(Uri address, StringComparison mode = StringComparison.Ordinal)
{
}
}
}
}
151 changes: 7 additions & 144 deletions src/runtime/MethodBinder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1012,11 +1012,11 @@ internal virtual NewReference Invoke(BorrowedReference inst, BorrowedReference a
// Use the snake_case name Python callers use, matching the hinted signatures below.
if (methodinfo != null && methodinfo.Length > 0)
{
value.Append($" for {SnakeCaseName(methodinfo[0])}");
value.Append($" for {MethodSignatureFormatter.SnakeCaseName(methodinfo[0])}");
}
else if (list.Count > 0)
{
value.Append($" for {SnakeCaseName(list[0].MethodBase)}");
value.Append($" for {MethodSignatureFormatter.SnakeCaseName(list[0].MethodBase)}");
}

value.Append(": ");
Expand All @@ -1028,7 +1028,11 @@ internal virtual NewReference Invoke(BorrowedReference inst, BorrowedReference a
var candidates = methodinfo != null && methodinfo.Length > 0
? methodinfo.Cast<MethodBase>()
: list?.Select(m => m.MethodBase);
AppendOverloads(value, candidates);
var overloads = MethodSignatureFormatter.FormatOverloads(candidates);
if (overloads.Length > 0)
{
value.Append(". ").Append(overloads);
}

Exceptions.RaiseTypeError(value.ToString());
}
Expand Down Expand Up @@ -1235,147 +1239,6 @@ protected static void AppendArgumentTypes(StringBuilder to, BorrowedReference ar
to.Append(')');
}

/// <summary>
/// Appends the signatures of the candidate overloads to the given error
/// message, so a failed bind hints the caller at what the method expects.
/// </summary>
private static void AppendOverloads(StringBuilder to, IEnumerable<MethodBase> methods)
{
if (methods == null)
{
return;
}

// Building this only runs on the error path; never let it throw and mask
// the original binding failure.
try
{
// 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 methods)
{
if (method == null)
{
continue;
}
var signature = FormatSignature(method);
if (seen.Add(signature))
{
signatures.Add(signature);
}
}

if (signatures.Count == 0)
{
return;
}

const int maxShown = 10;
to.Append(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");
}
}
catch
{
// Best-effort hint only.
}
}

/// <summary>
/// Formats a method/constructor as a readable signature using the snake_case
/// name Python callers use, e.g.
/// <c>range_consolidator(Int32 range, Func[IBaseData, Decimal] selector = None)</c>.
/// The constructor's special <c>.ctor</c> token is left as-is.
/// </summary>
private static string FormatSignature(MethodBase method)
{
var to = new StringBuilder();
to.Append(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))
{
to.Append("params ");
}
to.Append(FormatType(parameter.ParameterType)).Append(' ').Append(parameter.Name.ToSnakeCase());
if (parameter.IsOptional)
{
to.Append(" = ").Append(FormatDefaultValue(parameter.DefaultValue));
}
}
to.Append(')');
return to.ToString();
}

/// <summary>
/// Produces a concise, readable name for a CLR type, unwrapping by-ref and
/// nullable types and rendering generics as <c>Name[Arg1, Arg2]</c>.
/// </summary>
private static string FormatType(Type type)
{
if (type.IsByRef)
{
type = type.GetElementType();
}

var underlying = Nullable.GetUnderlyingType(type);
if (underlying != null)
{
return FormatType(underlying) + "?";
}

if (type.IsGenericType)
{
var name = type.Name;
var tick = name.IndexOf('`');
if (tick >= 0)
{
name = name.Substring(0, tick);
}
var args = type.GetGenericArguments().Select(FormatType);
return $"{name}[{string.Join(", ", args)}]";
}

return type.Name;
}

/// <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>
private static string SnakeCaseName(MethodBase method)
{
return method.IsConstructor ? method.Name : method.Name.ToSnakeCase();
}

private static string FormatDefaultValue(object value)
{
if (value == null || value is DBNull)
{
return "None";
}
if (value is string s)
{
return $"\"{s}\"";
}
return value.ToString();
}
}


Expand Down
Loading
Loading