Skip to content

Commit 36837f5

Browse files
committed
Merge upstream/master into bug-attribute-suggestion-quality
Keeps the Jaro-Winkler scoring from this branch in ClassBase while retaining Util.LevenshteinDistance, which MethodBinder's unexpected-kwarg suggestion (#144) still uses. Also moves the DiagnoseClosestOverloadMismatch block inside the bind-failure try so master compiles again: #144 wrapped the message construction (including the candidates declaration) in try/catch after #145 had added the mismatch diagnosis below it, leaving 'candidates' out of scope at its use site.
2 parents 62b3f6a + 3464917 commit 36837f5

9 files changed

Lines changed: 721 additions & 27 deletions

File tree

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
using NUnit.Framework;
2+
using Python.Runtime;
3+
4+
namespace Python.EmbeddingTest
5+
{
6+
/// <summary>
7+
/// The bind-failure TypeError must pinpoint the first argument that fails to
8+
/// match the nearest overload.
9+
/// </summary>
10+
public class TestBindFailureDiagnosis
11+
{
12+
public class OrdersTarget
13+
{
14+
public string PlaceOrder(string symbol, decimal quantity, bool asynchronous = false, string tag = "", int depth = 0) => "decimal";
15+
public string PlaceOrder(string symbol, int quantity, bool asynchronous = false, string tag = "", int depth = 0) => "int";
16+
}
17+
18+
public class SingleOverloadTarget
19+
{
20+
public int Compute(int periods) => periods;
21+
}
22+
23+
[OneTimeSetUp]
24+
public void SetUp()
25+
{
26+
PythonEngine.Initialize();
27+
}
28+
29+
[OneTimeTearDown]
30+
public void Dispose()
31+
{
32+
PythonEngine.Shutdown();
33+
}
34+
35+
private static string TypeErrorMessageOf(string call)
36+
{
37+
using var _ = Py.GIL();
38+
var module = PyModule.FromString("TestBindFailureDiagnosis_" + TestContext.CurrentContext.Test.Name, $@"
39+
from clr import AddReference
40+
AddReference(""Python.EmbeddingTest"")
41+
AddReference(""System"")
42+
43+
from Python.EmbeddingTest import *
44+
45+
def get_error():
46+
target = TestBindFailureDiagnosis.OrdersTarget()
47+
single = TestBindFailureDiagnosis.SingleOverloadTarget()
48+
try:
49+
{call}
50+
except TypeError as e:
51+
return str(e)
52+
return None
53+
");
54+
using var result = module.GetAttr("get_error").Invoke();
55+
Assert.IsFalse(result.IsNone(), "expected the call to raise a TypeError");
56+
return result.As<string>();
57+
}
58+
59+
[Test]
60+
public void PinpointsFirstMismatchedPositionalArgument()
61+
{
62+
var message = TypeErrorMessageOf("target.place_order('SPY', -10, 'exit signal')");
63+
64+
Assert.That(message, Does.StartWith("No method matches given arguments for place_order: "));
65+
Assert.That(message, Does.Contain("The following overloads are available:"));
66+
Assert.That(message, Does.Contain("Argument mismatch: argument 3 ('asynchronous') expected bool, got str."));
67+
}
68+
69+
[Test]
70+
public void PinpointsMismatchedKeywordArgument()
71+
{
72+
var message = TypeErrorMessageOf("target.place_order('SPY', 10, tag=5)");
73+
74+
Assert.That(message, Does.Contain("Argument mismatch: keyword argument 'tag' expected str, got int."));
75+
}
76+
77+
[Test]
78+
public void PinpointsMismatchOnSingleOverloadMethods()
79+
{
80+
var message = TypeErrorMessageOf("single.compute('abc')");
81+
82+
Assert.That(message, Does.Contain("The expected signature is:"));
83+
Assert.That(message, Does.Contain("Argument mismatch: argument 1 ('periods') expected int, got str."));
84+
}
85+
86+
[Test]
87+
public void SkipsDiagnosisWhenAllGivenArgumentsMatch()
88+
{
89+
// Pure arity failure: no mismatched argument to single out.
90+
var message = TypeErrorMessageOf("single.compute(1, 2)");
91+
92+
Assert.That(message, Does.Contain("No method matches given arguments for compute"));
93+
Assert.That(message, Does.Not.Contain("Argument mismatch:"));
94+
}
95+
96+
[Test]
97+
public void DiagnosisSurvivesTheOverloadsHintExtraction()
98+
{
99+
// Lean keeps the message from the overloads marker onwards; the diagnosis
100+
// must be inside that region to reach users.
101+
var message = TypeErrorMessageOf("target.place_order('SPY', -10, 'exit signal')");
102+
103+
var hintStart = message.IndexOf("The following overloads are available:");
104+
Assert.GreaterOrEqual(hintStart, 0);
105+
var hint = message.Substring(hintStart);
106+
Assert.That(hint, Does.Contain("Argument mismatch: argument 3 ('asynchronous') expected bool, got str."));
107+
}
108+
}
109+
}

src/embed_tests/TestFloatToIntConversion.cs

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,43 @@ def overloaded_named(value):
4141
4242
def single_params(value):
4343
return IntTaker(0).ComputeScaled(value)
44+
45+
class FloatSubclass(float):
46+
# numpy.float64-like: a float subclass
47+
pass
48+
49+
class FloatLike:
50+
# numpy.float32-like: float and (truncating) int conversions, no __index__
51+
def __init__(self, v):
52+
self._v = v
53+
def __float__(self):
54+
return float(self._v)
55+
def __int__(self):
56+
return int(self._v)
57+
58+
class IndexLike:
59+
# numpy.int64-like: a true integer type advertising __index__
60+
def __init__(self, v):
61+
self._v = v
62+
def __index__(self):
63+
return int(self._v)
64+
def __float__(self):
65+
return float(self._v)
66+
67+
def single_ctor_float_subclass(value):
68+
return IntTaker(FloatSubclass(value)).Value
69+
70+
def overloaded_ctor_float_subclass(value):
71+
return OverloadedIntTaker(FloatSubclass(value)).Value
72+
73+
def single_ctor_float_like(value):
74+
return IntTaker(FloatLike(value)).Value
75+
76+
def overloaded_ctor_float_like(value):
77+
return OverloadedIntTaker(FloatLike(value)).Value
78+
79+
def single_ctor_index_like(value):
80+
return IntTaker(IndexLike(value)).Value
4481
";
4582

4683
[OneTimeSetUp]
@@ -87,6 +124,45 @@ public void NonIntegralFloat_IsRejected(string func)
87124
Assert.AreEqual("TypeError", ex.Type.Name);
88125
}
89126

127+
// Float subclasses (e.g. numpy.float64) follow the plain-float rule.
128+
[TestCase("single_ctor_float_subclass")]
129+
[TestCase("overloaded_ctor_float_subclass")]
130+
public void IntegralFloatSubclass_IsAccepted(string func)
131+
{
132+
Assert.AreEqual(5, Call(func, 5.0));
133+
}
134+
135+
[TestCase("single_ctor_float_subclass")]
136+
[TestCase("overloaded_ctor_float_subclass")]
137+
public void NonIntegralFloatSubclass_IsRejected(string func)
138+
{
139+
var ex = Assert.Throws<PythonException>(() => Call(func, 5.5));
140+
Assert.AreEqual("TypeError", ex.Type.Name);
141+
}
142+
143+
// __float__-only numbers (e.g. numpy.float32) follow the plain-float rule.
144+
[TestCase("single_ctor_float_like")]
145+
[TestCase("overloaded_ctor_float_like")]
146+
public void IntegralFloatLike_IsAccepted(string func)
147+
{
148+
Assert.AreEqual(5, Call(func, 5.0));
149+
}
150+
151+
[TestCase("single_ctor_float_like")]
152+
[TestCase("overloaded_ctor_float_like")]
153+
public void NonIntegralFloatLike_IsRejected(string func)
154+
{
155+
var ex = Assert.Throws<PythonException>(() => Call(func, 5.5));
156+
Assert.AreEqual("TypeError", ex.Type.Name);
157+
}
158+
159+
// __index__ types (e.g. numpy.int64) are integers, not float-like.
160+
[Test]
161+
public void IndexLike_IsAccepted()
162+
{
163+
Assert.AreEqual(5, Call("single_ctor_index_like", 5.0));
164+
}
165+
90166
// When no overload matches, the error should hint the expected signature(s).
91167
[Test]
92168
public void ErrorMessage_SingleOverload_ShowsExpectedSignature()

src/runtime/Converter.cs

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -907,6 +907,28 @@ internal static int ToInt32(BorrowedReference value)
907907
return checked((int)num);
908908
}
909909

910+
/// <summary>
911+
/// True for Python floats (including subclasses like numpy.float64) and for
912+
/// numbers with __float__ but no __index__ (like numpy.float32); __index__
913+
/// marks a type as losslessly int-convertible, so those are not float-like.
914+
/// </summary>
915+
private static bool IsFloatLike(BorrowedReference value)
916+
{
917+
// fast path for the common case: actual ints
918+
if (Runtime.PyInt_Check(value) || Runtime.PyBool_Check(value))
919+
{
920+
return false;
921+
}
922+
923+
if (Runtime.PyObject_TypeCheck(value, Runtime.PyFloatType))
924+
{
925+
return true;
926+
}
927+
928+
return Runtime.PyObject_HasAttrString(value, "__float__") != 0
929+
&& Runtime.PyObject_HasAttrString(value, "__index__") == 0;
930+
}
931+
910932
/// <summary>
911933
/// Convert a Python value to an instance of a primitive managed type.
912934
/// </summary>
@@ -918,14 +940,17 @@ internal static bool ToPrimitive(BorrowedReference value, Type obType, out objec
918940

919941
TypeCode tc = Type.GetTypeCode(obType);
920942

921-
// A Python float with a fractional part must not be silently truncated
922-
// into an integer parameter. Integral-valued floats (e.g. 5.0) are still
923-
// accepted. This keeps single- and multi-overload binding consistent:
924-
// MethodBinder only treats integral floats as candidates for integer
925-
// parameters, and this guard enforces the same rule at conversion time.
926-
if (tc.IsInteger() && Runtime.PyFloat_Check(value))
943+
// Reject non-integral float-like values (incl. numpy floats) for integer
944+
// targets; the PyNumber_Long path below would silently truncate them.
945+
if (tc.IsInteger() && IsFloatLike(value))
927946
{
928947
double dbl = Runtime.PyFloat_AsDouble(value);
948+
if (dbl == -1.0 && Exceptions.ErrorOccurred())
949+
{
950+
// don't let a failed __float__ probe leak
951+
Exceptions.Clear();
952+
goto type_error;
953+
}
929954
if (double.IsNaN(dbl) || double.IsInfinity(dbl) || Math.Truncate(dbl) != dbl)
930955
{
931956
goto type_error;

0 commit comments

Comments
 (0)