Skip to content

Commit ff0fcb6

Browse files
committed
Skip PyObject overloads from the hinted signatures
PyObject parameters accept any Python object and render as Any, so hinting them carries no type information: they are typically the very overloads that just rejected the value. Skip them in FormatOverloads so consumers do not have to filter them out themselves. If every candidate takes a PyObject, they are shown anyway rather than producing no hint at all.
1 parent ace60d2 commit ff0fcb6

3 files changed

Lines changed: 77 additions & 7 deletions

File tree

src/embed_tests/TestFloatToIntConversion.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,9 +100,12 @@ public void ErrorMessage_SingleOverload_ShowsExpectedSignature()
100100
public void ErrorMessage_MultipleOverloads_ListsCandidates()
101101
{
102102
var ex = Assert.Throws<PythonException>(() => Call("overloaded_ctor", 5.5));
103-
StringAssert.Contains("The following overloads are available:", ex.Message);
104-
// The int overload is surfaced, hinting an integer was expected.
103+
// The int overload is surfaced, hinting an integer was expected. The
104+
// PyObject overload is skipped (it carries no type information), which
105+
// leaves a single hinted signature here.
106+
StringAssert.Contains("The expected signature is:", ex.Message);
105107
StringAssert.Contains("range: int", ex.Message);
108+
StringAssert.DoesNotContain("volume_selector", ex.Message);
106109
}
107110

108111
// The hinted signatures use the snake_case name Python callers use, not the

src/embed_tests/TestMethodSignatureFormatter.cs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,50 @@ public void RendersConstructorsWithDisplayName()
8080
Assert.AreEqual("SampleTarget(period: timedelta, start_time: Optional[timedelta] = None)", signature);
8181
}
8282

83+
[Test]
84+
public void SkipsPyObjectOverloadsFromHints()
85+
{
86+
var hint = MethodSignatureFormatter.FormatOverloads(typeof(MixedOverloadsTarget).GetConstructors(),
87+
displayName: nameof(MixedOverloadsTarget));
88+
89+
StringAssert.Contains("The following overloads are available:", hint);
90+
StringAssert.Contains("MixedOverloadsTarget(period: timedelta)", hint);
91+
StringAssert.Contains("MixedOverloadsTarget(max_count: int)", hint);
92+
StringAssert.DoesNotContain("py_func", hint);
93+
}
94+
95+
[Test]
96+
public void ShowsPyObjectOverloadsWhenThereIsNothingElseToHint()
97+
{
98+
var hint = MethodSignatureFormatter.FormatOverloads(typeof(PyObjectOnlyTarget).GetConstructors(),
99+
displayName: nameof(PyObjectOnlyTarget));
100+
101+
StringAssert.Contains("The expected signature is:", hint);
102+
StringAssert.Contains("PyObjectOnlyTarget(py_func: Any)", hint);
103+
}
104+
105+
private class MixedOverloadsTarget
106+
{
107+
public MixedOverloadsTarget(TimeSpan period)
108+
{
109+
}
110+
111+
public MixedOverloadsTarget(int maxCount)
112+
{
113+
}
114+
115+
public MixedOverloadsTarget(PyObject pyFunc)
116+
{
117+
}
118+
}
119+
120+
private class PyObjectOnlyTarget
121+
{
122+
public PyObjectOnlyTarget(PyObject pyFunc)
123+
{
124+
}
125+
}
126+
83127
private class SampleTarget
84128
{
85129
public SampleTarget(TimeSpan period, TimeSpan? startTime = null)

src/runtime/MethodSignatureFormatter.cs

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ public static class MethodSignatureFormatter
1717
/// Formats the signatures of the candidate overloads as an error message hint,
1818
/// so the caller can see what the method expects, e.g.
1919
/// "The following overloads are available:" followed by one signature per line.
20+
/// Overloads taking PyObject parameters are skipped: they accept any Python
21+
/// object and carry no type information (they are typically the overloads that
22+
/// just rejected the value). If every candidate takes a PyObject, they are shown
23+
/// anyway rather than producing no hint at all.
2024
/// Returns an empty string if there are no signatures to show.
2125
/// </summary>
2226
/// <param name="methods">The candidate overloads</param>
@@ -34,16 +38,19 @@ public static string FormatOverloads(IEnumerable<MethodBase> methods, int maxSho
3438
// the original failure.
3539
try
3640
{
41+
var candidates = methods.Where(method => method != null).ToList();
42+
var withoutPyObject = candidates.Where(method => !TakesPyObject(method)).ToList();
43+
if (withoutPyObject.Count > 0)
44+
{
45+
candidates = withoutPyObject;
46+
}
47+
3748
// Distinct signatures, preserving order. Snake-cased duplicates and
3849
// repeated overloads collapse into a single entry.
3950
var signatures = new List<string>();
4051
var seen = new HashSet<string>();
41-
foreach (var method in methods)
52+
foreach (var method in candidates)
4253
{
43-
if (method == null)
44-
{
45-
continue;
46-
}
4754
var signature = FormatSignature(method, displayName);
4855
if (seen.Add(signature))
4956
{
@@ -123,6 +130,22 @@ internal static string SnakeCaseName(MethodBase method)
123130
return method.IsConstructor ? method.Name : method.Name.ToSnakeCase();
124131
}
125132

133+
/// <summary>
134+
/// Determines whether any of the method's parameters is a PyObject
135+
/// </summary>
136+
private static bool TakesPyObject(MethodBase method)
137+
{
138+
return method.GetParameters().Any(parameter =>
139+
{
140+
var type = parameter.ParameterType;
141+
if (type.IsByRef)
142+
{
143+
type = type.GetElementType();
144+
}
145+
return typeof(PyObject).IsAssignableFrom(type);
146+
});
147+
}
148+
126149
/// <summary>
127150
/// Produces the Python-side name for a CLR type, following the conversions the
128151
/// runtime performs on arguments: primitives map to their Python equivalents

0 commit comments

Comments
 (0)