From 0d2cdd2ae5e86e65621ddb36bf8752b0ede37640 Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Tue, 11 Aug 2026 11:42:53 -0400 Subject: [PATCH 1/5] Raise a proper unexpected-keyword-argument TypeError on bind failure When a method call fails to bind and one of the supplied keyword arguments matches no parameter of any candidate overload, the generic 'No method matches given arguments' message did not mention the keyword argument at all (only positional argument types are echoed), leaving the actual mistake invisible, e.g.: market_order(symbol, -10, as_tag="EmergencyFlatten") -> No method matches given arguments for market_order: (, ). The following overloads ... Now such calls raise the Python-style error instead, naming the offending kwarg and suggesting the closest parameter name when one exists: market_order() got an unexpected keyword argument 'as_tag'. Did you mean 'tag'? When every kwarg name is valid for some overload but binding still fails, the existing no-method-matches message is preserved. --- src/runtime/MethodBinder.cs | 136 ++++++++++++++++++++++++++++++++++++ src/testing/methodtest.cs | 5 ++ tests/test_method.py | 47 +++++++++++++ 3 files changed, 188 insertions(+) diff --git a/src/runtime/MethodBinder.cs b/src/runtime/MethodBinder.cs index a20624d2b..2bd565eec 100644 --- a/src/runtime/MethodBinder.cs +++ b/src/runtime/MethodBinder.cs @@ -1016,6 +1016,14 @@ internal virtual NewReference Invoke(BorrowedReference inst, BorrowedReference a // If we already have an exception pending, don't create a new one if (!Exceptions.ErrorOccurred()) { + // A keyword argument whose name no candidate overload accepts gets the + // Python-native "unexpected keyword argument" error: the generic no-match + // message below does not echo kwargs, leaving the actual mistake invisible. + if (TryRaiseUnexpectedKeywordArgumentError(kw, info, methodinfo)) + { + return default; + } + var value = new StringBuilder("No method matches given arguments"); // Use the snake_case name Python callers use, matching the hinted signatures below. if (methodinfo != null && methodinfo.Length > 0) @@ -1123,6 +1131,134 @@ internal virtual NewReference Invoke(BorrowedReference inst, BorrowedReference a return Converter.ToPython(result, returnType); } + /// + /// When a bind failure involves a keyword argument whose name no candidate overload + /// accepts, raises the Python-style "got an unexpected keyword argument" TypeError + /// (with a "Did you mean" hint when a similarly-named parameter exists) and returns + /// true. Returns false when every kwarg name is accepted by at least one overload, + /// so the generic no-match error is raised instead. + /// + private bool TryRaiseUnexpectedKeywordArgumentError(BorrowedReference kw, MethodBase info, MethodInfo[] methodinfo) + { + var kwCount = kw == null ? 0 : (int)Runtime.PyDict_Size(kw); + if (kwCount <= 0) + { + return false; + } + + // The same candidate set Bind considered: parameter names are snake_case for + // snake_case-registered methods and original for the original ones, matching + // the names the caller can actually use. + var methods = info == null + ? GetMethods() + : new List(1) { new MethodInformation(info, true) }; + var parameterNames = new HashSet(StringComparer.Ordinal); + foreach (var method in methods) + { + foreach (var parameterName in method.ParameterNames) + { + parameterNames.Add(parameterName); + } + } + + // Report the first unknown kwarg in call order, like CPython does. + string unexpectedName = null; + using (var keyList = Runtime.PyDict_Keys(kw)) + { + for (var i = 0; i < kwCount && unexpectedName == null; i++) + { + var name = Runtime.GetManagedString(Runtime.PyList_GetItem(keyList.Borrow(), i)); + if (name != null && !parameterNames.Contains(name)) + { + unexpectedName = name; + } + } + } + + if (unexpectedName == null) + { + return false; + } + + string methodName = null; + if (methodinfo != null && methodinfo.Length > 0) + { + methodName = MethodSignatureFormatter.SnakeCaseName(methodinfo[0]); + } + else if (list.Count > 0) + { + methodName = MethodSignatureFormatter.SnakeCaseName(list[0].MethodBase); + } + if (string.IsNullOrEmpty(methodName)) + { + return false; + } + + var message = $"{methodName}() got an unexpected keyword argument '{unexpectedName}'"; + var suggestion = ClosestParameterName(unexpectedName, parameterNames); + if (suggestion != null) + { + message += $". Did you mean '{suggestion}'?"; + } + + Exceptions.RaiseTypeError(message); + return true; + } + + /// + /// The candidate parameter name closest to the unexpected kwarg name, or null when + /// none is similar enough to suggest. A candidate is considered when it is within + /// a small edit distance of the name, or when one contains the other (e.g. 'as_tag' + /// suggests 'tag'); containment requires 3+ characters so tiny names don't match. + /// + private static string ClosestParameterName(string name, HashSet parameterNames) + { + const int MinContainmentLength = 3; + var threshold = Math.Max(2, name.Length / 3); + string best = null; + var bestDistance = int.MaxValue; + foreach (var candidate in parameterNames) + { + var distance = KeywordEditDistance(name, candidate); + var related = distance <= threshold + || (candidate.Length >= MinContainmentLength && name.Length >= MinContainmentLength + && (candidate.IndexOf(name, StringComparison.OrdinalIgnoreCase) >= 0 + || name.IndexOf(candidate, StringComparison.OrdinalIgnoreCase) >= 0)); + if (related && (distance < bestDistance + || (distance == bestDistance && string.CompareOrdinal(candidate, best) < 0))) + { + bestDistance = distance; + best = candidate; + } + } + return best; + } + + // Case-insensitive Levenshtein distance, local to keyword suggestions. + private static int KeywordEditDistance(string a, string b) + { + a = a.ToLowerInvariant(); + b = b.ToLowerInvariant(); + if (a.Length == 0) return b.Length; + if (b.Length == 0) return a.Length; + + var prev = new int[b.Length + 1]; + var curr = new int[b.Length + 1]; + for (var j = 0; j <= b.Length; j++) prev[j] = j; + + for (var i = 1; i <= a.Length; i++) + { + curr[0] = i; + for (var j = 1; j <= b.Length; j++) + { + var cost = a[i - 1] == b[j - 1] ? 0 : 1; + curr[j] = Math.Min(Math.Min(curr[j - 1] + 1, prev[j] + 1), prev[j - 1] + cost); + } + (prev, curr) = (curr, prev); + } + return prev[b.Length]; + } + /// /// Utility class to store the information about a /// diff --git a/src/testing/methodtest.cs b/src/testing/methodtest.cs index fe49de88d..4b62c07a4 100644 --- a/src/testing/methodtest.cs +++ b/src/testing/methodtest.cs @@ -709,6 +709,11 @@ public static string DefaultParamsWithOverloading(int a = 5, int b = 6, int c = return $"{a}{b}{c}{d}XXX"; } + public static string OrderLikeMethod(string symbol, decimal quantity, bool asynchronous = false, string tag = "", object orderProperties = null) + { + return string.Format("{0}:{1}:{2}:{3}", symbol, quantity, asynchronous, tag); + } + public static string ParamsArrayOverloaded(int i = 1) { return "without params-array"; diff --git a/tests/test_method.py b/tests/test_method.py index 07b5c5a34..f2f6f2acb 100644 --- a/tests/test_method.py +++ b/tests/test_method.py @@ -1104,6 +1104,53 @@ def test_default_params(): with pytest.raises(TypeError): MethodTest.DefaultParams(1,2,3,4,5) +def test_unexpected_keyword_argument_with_suggestion(): + # A kwarg no overload accepts raises the Python-style error naming the kwarg, + # with a did-you-mean hint when a similarly-named parameter exists. + with pytest.raises(TypeError) as excinfo: + MethodTest.order_like_method("SPY", 10, as_tag="EmergencyFlatten") + message = str(excinfo.value) + assert "order_like_method() got an unexpected keyword argument 'as_tag'" in message + assert "Did you mean 'tag'?" in message + + # Same behavior when calling through the original PascalCase name; parameter + # names are the original ones there. + with pytest.raises(TypeError) as excinfo: + MethodTest.OrderLikeMethod("SPY", 10, asTag="EmergencyFlatten") + message = str(excinfo.value) + assert "order_like_method() got an unexpected keyword argument 'asTag'" in message + assert "Did you mean 'tag'?" in message + + +def test_unexpected_keyword_argument_without_suggestion(): + # No parameter is remotely similar: the kwarg is still named, but no hint is added. + with pytest.raises(TypeError) as excinfo: + MethodTest.order_like_method("SPY", 10, completely_unrelated_name=1) + message = str(excinfo.value) + assert "order_like_method() got an unexpected keyword argument " \ + "'completely_unrelated_name'" in message + assert "Did you mean" not in message + + +def test_unexpected_keyword_argument_reports_first_in_call_order(): + with pytest.raises(TypeError) as excinfo: + MethodTest.order_like_method("SPY", 10, first_bogus=1, second_bogus=2) + assert "got an unexpected keyword argument 'first_bogus'" in str(excinfo.value) + + +def test_valid_keyword_arguments_still_bind(): + res = MethodTest.order_like_method("SPY", 10, asynchronous=True, tag="mytag") + assert res == "SPY:10:True:mytag" + + +def test_valid_keyword_argument_names_keep_no_match_message(): + # All kwarg names are real parameters, but the call still cannot bind ('d' is + # supplied both positionally and by name): the classic no-method-matches + # message must be preserved for this case. + with pytest.raises(TypeError) as excinfo: + MethodTest.DefaultParams(1, 2, 3, 4, d=5) + assert "No method matches given arguments for default_params" in str(excinfo.value) + def test_optional_params(): res = MethodTest.OptionalParams(1, 2, 3, 4) assert res == "1234" From 8c9b27f1714fdeea35ac72e1f3b4c94d2680f5d3 Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Tue, 11 Aug 2026 13:07:09 -0400 Subject: [PATCH 2/5] Tighten comments in unexpected-keyword-argument error path --- src/runtime/MethodBinder.cs | 23 +++++++---------------- tests/test_method.py | 10 ++-------- 2 files changed, 9 insertions(+), 24 deletions(-) diff --git a/src/runtime/MethodBinder.cs b/src/runtime/MethodBinder.cs index 2bd565eec..77c753653 100644 --- a/src/runtime/MethodBinder.cs +++ b/src/runtime/MethodBinder.cs @@ -1016,9 +1016,7 @@ internal virtual NewReference Invoke(BorrowedReference inst, BorrowedReference a // If we already have an exception pending, don't create a new one if (!Exceptions.ErrorOccurred()) { - // A keyword argument whose name no candidate overload accepts gets the - // Python-native "unexpected keyword argument" error: the generic no-match - // message below does not echo kwargs, leaving the actual mistake invisible. + // Unknown kwarg names get the Python-style error; the generic message below does not echo kwargs. if (TryRaiseUnexpectedKeywordArgumentError(kw, info, methodinfo)) { return default; @@ -1132,11 +1130,8 @@ internal virtual NewReference Invoke(BorrowedReference inst, BorrowedReference a } /// - /// When a bind failure involves a keyword argument whose name no candidate overload - /// accepts, raises the Python-style "got an unexpected keyword argument" TypeError - /// (with a "Did you mean" hint when a similarly-named parameter exists) and returns - /// true. Returns false when every kwarg name is accepted by at least one overload, - /// so the generic no-match error is raised instead. + /// Raises "got an unexpected keyword argument" and returns true when a kwarg name is + /// accepted by no candidate overload; returns false to let the generic no-match error be raised. /// private bool TryRaiseUnexpectedKeywordArgumentError(BorrowedReference kw, MethodBase info, MethodInfo[] methodinfo) { @@ -1146,9 +1141,7 @@ private bool TryRaiseUnexpectedKeywordArgumentError(BorrowedReference kw, Method return false; } - // The same candidate set Bind considered: parameter names are snake_case for - // snake_case-registered methods and original for the original ones, matching - // the names the caller can actually use. + // Same candidate set Bind considered; ParameterNames are already in the caller's convention. var methods = info == null ? GetMethods() : new List(1) { new MethodInformation(info, true) }; @@ -1206,10 +1199,8 @@ private bool TryRaiseUnexpectedKeywordArgumentError(BorrowedReference kw, Method } /// - /// The candidate parameter name closest to the unexpected kwarg name, or null when - /// none is similar enough to suggest. A candidate is considered when it is within - /// a small edit distance of the name, or when one contains the other (e.g. 'as_tag' - /// suggests 'tag'); containment requires 3+ characters so tiny names don't match. + /// Closest parameter name to suggest, or null: small edit distance, or containment + /// between names of 3+ characters (e.g. 'as_tag' suggests 'tag'). /// private static string ClosestParameterName(string name, HashSet parameterNames) { @@ -1234,7 +1225,7 @@ private static string ClosestParameterName(string name, HashSet paramete return best; } - // Case-insensitive Levenshtein distance, local to keyword suggestions. + // Case-insensitive Levenshtein distance. private static int KeywordEditDistance(string a, string b) { a = a.ToLowerInvariant(); diff --git a/tests/test_method.py b/tests/test_method.py index f2f6f2acb..14836c8a3 100644 --- a/tests/test_method.py +++ b/tests/test_method.py @@ -1105,16 +1105,13 @@ def test_default_params(): MethodTest.DefaultParams(1,2,3,4,5) def test_unexpected_keyword_argument_with_suggestion(): - # A kwarg no overload accepts raises the Python-style error naming the kwarg, - # with a did-you-mean hint when a similarly-named parameter exists. with pytest.raises(TypeError) as excinfo: MethodTest.order_like_method("SPY", 10, as_tag="EmergencyFlatten") message = str(excinfo.value) assert "order_like_method() got an unexpected keyword argument 'as_tag'" in message assert "Did you mean 'tag'?" in message - # Same behavior when calling through the original PascalCase name; parameter - # names are the original ones there. + # PascalCase call path: parameter names are the original ones. with pytest.raises(TypeError) as excinfo: MethodTest.OrderLikeMethod("SPY", 10, asTag="EmergencyFlatten") message = str(excinfo.value) @@ -1123,7 +1120,6 @@ def test_unexpected_keyword_argument_with_suggestion(): def test_unexpected_keyword_argument_without_suggestion(): - # No parameter is remotely similar: the kwarg is still named, but no hint is added. with pytest.raises(TypeError) as excinfo: MethodTest.order_like_method("SPY", 10, completely_unrelated_name=1) message = str(excinfo.value) @@ -1144,9 +1140,7 @@ def test_valid_keyword_arguments_still_bind(): def test_valid_keyword_argument_names_keep_no_match_message(): - # All kwarg names are real parameters, but the call still cannot bind ('d' is - # supplied both positionally and by name): the classic no-method-matches - # message must be preserved for this case. + # 'd' is supplied both positionally and by name: valid names, unbindable call. with pytest.raises(TypeError) as excinfo: MethodTest.DefaultParams(1, 2, 3, 4, d=5) assert "No method matches given arguments for default_params" in str(excinfo.value) From 3d9173a119b042a4a1e7de5b4eea9405b23830a8 Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Tue, 11 Aug 2026 13:22:25 -0400 Subject: [PATCH 3/5] Share the Levenshtein distance helper between ClassBase and MethodBinder Moves ClassBase's private LevenshteinDistance implementation verbatim to Util.LevenshteinDistance and uses it from both call sites, removing the duplicate introduced for keyword-argument suggestions. --- src/runtime/MethodBinder.cs | 27 +-------------------------- src/runtime/Types/ClassBase.cs | 27 +-------------------------- src/runtime/Util/Util.cs | 27 +++++++++++++++++++++++++++ 3 files changed, 29 insertions(+), 52 deletions(-) diff --git a/src/runtime/MethodBinder.cs b/src/runtime/MethodBinder.cs index 77c753653..710f89cc0 100644 --- a/src/runtime/MethodBinder.cs +++ b/src/runtime/MethodBinder.cs @@ -1210,7 +1210,7 @@ private static string ClosestParameterName(string name, HashSet paramete var bestDistance = int.MaxValue; foreach (var candidate in parameterNames) { - var distance = KeywordEditDistance(name, candidate); + var distance = Util.LevenshteinDistance(name, candidate); var related = distance <= threshold || (candidate.Length >= MinContainmentLength && name.Length >= MinContainmentLength && (candidate.IndexOf(name, StringComparison.OrdinalIgnoreCase) >= 0 @@ -1225,31 +1225,6 @@ private static string ClosestParameterName(string name, HashSet paramete return best; } - // Case-insensitive Levenshtein distance. - private static int KeywordEditDistance(string a, string b) - { - a = a.ToLowerInvariant(); - b = b.ToLowerInvariant(); - if (a.Length == 0) return b.Length; - if (b.Length == 0) return a.Length; - - var prev = new int[b.Length + 1]; - var curr = new int[b.Length + 1]; - for (var j = 0; j <= b.Length; j++) prev[j] = j; - - for (var i = 1; i <= a.Length; i++) - { - curr[0] = i; - for (var j = 1; j <= b.Length; j++) - { - var cost = a[i - 1] == b[j - 1] ? 0 : 1; - curr[j] = Math.Min(Math.Min(curr[j - 1] + 1, prev[j] + 1), prev[j - 1] + cost); - } - (prev, curr) = (curr, prev); - } - return prev[b.Length]; - } - /// /// Utility class to store the information about a /// diff --git a/src/runtime/Types/ClassBase.cs b/src/runtime/Types/ClassBase.cs index 1342b6a3f..aa32662d8 100644 --- a/src/runtime/Types/ClassBase.cs +++ b/src/runtime/Types/ClassBase.cs @@ -845,7 +845,7 @@ private static string ComputeSimilarMemberNames(Type type, string name) var scored = new List<(string Name, int Distance, SuggestionKind Kind)>(); foreach (var candidate in GetCandidateMemberNames(type)) { - var distance = LevenshteinDistance(name, candidate.Key); + var distance = Util.LevenshteinDistance(name, candidate.Key); var related = distance <= threshold || candidate.Key.IndexOf(name, StringComparison.OrdinalIgnoreCase) >= 0 || name.IndexOf(candidate.Key, StringComparison.OrdinalIgnoreCase) >= 0; @@ -895,30 +895,5 @@ private static (string Name, SuggestionKind Kind) ToSnakeCaseMemberName(MemberIn }; } - private static int LevenshteinDistance(string a, string b) - { - a = a.ToLowerInvariant(); - b = b.ToLowerInvariant(); - var n = a.Length; - var m = b.Length; - if (n == 0) return m; - if (m == 0) return n; - - var prev = new int[m + 1]; - var curr = new int[m + 1]; - for (var j = 0; j <= m; j++) prev[j] = j; - - for (var i = 1; i <= n; i++) - { - curr[0] = i; - for (var j = 1; j <= m; j++) - { - var cost = a[i - 1] == b[j - 1] ? 0 : 1; - curr[j] = Math.Min(Math.Min(curr[j - 1] + 1, prev[j] + 1), prev[j - 1] + cost); - } - (prev, curr) = (curr, prev); - } - return prev[m]; - } } } diff --git a/src/runtime/Util/Util.cs b/src/runtime/Util/Util.cs index 45ee649a9..2e17911bf 100644 --- a/src/runtime/Util/Util.cs +++ b/src/runtime/Util/Util.cs @@ -336,5 +336,32 @@ public static bool IsInteger(this TypeCode typeCode) return false; } } + + // Case-insensitive Levenshtein distance. + internal static int LevenshteinDistance(string a, string b) + { + a = a.ToLowerInvariant(); + b = b.ToLowerInvariant(); + var n = a.Length; + var m = b.Length; + if (n == 0) return m; + if (m == 0) return n; + + var prev = new int[m + 1]; + var curr = new int[m + 1]; + for (var j = 0; j <= m; j++) prev[j] = j; + + for (var i = 1; i <= n; i++) + { + curr[0] = i; + for (var j = 1; j <= m; j++) + { + var cost = a[i - 1] == b[j - 1] ? 0 : 1; + curr[j] = Math.Min(Math.Min(curr[j - 1] + 1, prev[j] + 1), prev[j - 1] + cost); + } + (prev, curr) = (curr, prev); + } + return prev[m]; + } } } From 1943ff5131f75f720fac49f26545e19215fee02d Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Tue, 11 Aug 2026 17:01:31 -0400 Subject: [PATCH 4/5] Extend the no-match error with the unexpected keyword argument instead of replacing it --- src/runtime/MethodBinder.cs | 49 ++++++++++++++----------------------- tests/test_method.py | 15 ++++++++---- 2 files changed, 28 insertions(+), 36 deletions(-) diff --git a/src/runtime/MethodBinder.cs b/src/runtime/MethodBinder.cs index 710f89cc0..e7b76d6c6 100644 --- a/src/runtime/MethodBinder.cs +++ b/src/runtime/MethodBinder.cs @@ -1016,12 +1016,6 @@ internal virtual NewReference Invoke(BorrowedReference inst, BorrowedReference a // If we already have an exception pending, don't create a new one if (!Exceptions.ErrorOccurred()) { - // Unknown kwarg names get the Python-style error; the generic message below does not echo kwargs. - if (TryRaiseUnexpectedKeywordArgumentError(kw, info, methodinfo)) - { - return default; - } - var value = new StringBuilder("No method matches given arguments"); // Use the snake_case name Python callers use, matching the hinted signatures below. if (methodinfo != null && methodinfo.Length > 0) @@ -1036,6 +1030,10 @@ internal virtual NewReference Invoke(BorrowedReference inst, BorrowedReference a value.Append(": "); AppendArgumentTypes(to: value, args); + // The argument types echo above covers positional args only; name the first + // unknown kwarg (if any) so a misspelled keyword argument is visible. + AppendUnexpectedKeywordArgument(value, kw, info); + // List the candidate overloads so the caller can see what was // expected (e.g. that an int overload exists when a float was // passed). Applies to every "no match" case, not just numeric ones. @@ -1045,7 +1043,12 @@ internal virtual NewReference Invoke(BorrowedReference inst, BorrowedReference a var overloads = MethodSignatureFormatter.FormatOverloads(candidates); if (overloads.Length > 0) { - value.Append(". ").Append(overloads); + // The kwarg hint may already end the sentence with a question mark. + if (value[value.Length - 1] != '?') + { + value.Append('.'); + } + value.Append(' ').Append(overloads); } Exceptions.RaiseTypeError(value.ToString()); @@ -1130,15 +1133,16 @@ internal virtual NewReference Invoke(BorrowedReference inst, BorrowedReference a } /// - /// Raises "got an unexpected keyword argument" and returns true when a kwarg name is - /// accepted by no candidate overload; returns false to let the generic no-match error be raised. + /// Appends "Got an unexpected keyword argument" to the no-match message when a kwarg + /// name is accepted by no candidate overload, with a "Did you mean" suggestion when a + /// similar parameter name exists. Appends nothing when every kwarg name is valid. /// - private bool TryRaiseUnexpectedKeywordArgumentError(BorrowedReference kw, MethodBase info, MethodInfo[] methodinfo) + private void AppendUnexpectedKeywordArgument(StringBuilder to, BorrowedReference kw, MethodBase info) { var kwCount = kw == null ? 0 : (int)Runtime.PyDict_Size(kw); if (kwCount <= 0) { - return false; + return; } // Same candidate set Bind considered; ParameterNames are already in the caller's convention. @@ -1170,32 +1174,15 @@ private bool TryRaiseUnexpectedKeywordArgumentError(BorrowedReference kw, Method if (unexpectedName == null) { - return false; - } - - string methodName = null; - if (methodinfo != null && methodinfo.Length > 0) - { - methodName = MethodSignatureFormatter.SnakeCaseName(methodinfo[0]); - } - else if (list.Count > 0) - { - methodName = MethodSignatureFormatter.SnakeCaseName(list[0].MethodBase); - } - if (string.IsNullOrEmpty(methodName)) - { - return false; + return; } - var message = $"{methodName}() got an unexpected keyword argument '{unexpectedName}'"; + to.Append($". Got an unexpected keyword argument '{unexpectedName}'"); var suggestion = ClosestParameterName(unexpectedName, parameterNames); if (suggestion != null) { - message += $". Did you mean '{suggestion}'?"; + to.Append($". Did you mean '{suggestion}'?"); } - - Exceptions.RaiseTypeError(message); - return true; } /// diff --git a/tests/test_method.py b/tests/test_method.py index 14836c8a3..7190f49f8 100644 --- a/tests/test_method.py +++ b/tests/test_method.py @@ -1108,14 +1108,16 @@ def test_unexpected_keyword_argument_with_suggestion(): with pytest.raises(TypeError) as excinfo: MethodTest.order_like_method("SPY", 10, as_tag="EmergencyFlatten") message = str(excinfo.value) - assert "order_like_method() got an unexpected keyword argument 'as_tag'" in message + assert "No method matches given arguments for order_like_method" in message + assert "Got an unexpected keyword argument 'as_tag'" in message assert "Did you mean 'tag'?" in message # PascalCase call path: parameter names are the original ones. with pytest.raises(TypeError) as excinfo: MethodTest.OrderLikeMethod("SPY", 10, asTag="EmergencyFlatten") message = str(excinfo.value) - assert "order_like_method() got an unexpected keyword argument 'asTag'" in message + assert "No method matches given arguments for order_like_method" in message + assert "Got an unexpected keyword argument 'asTag'" in message assert "Did you mean 'tag'?" in message @@ -1123,7 +1125,8 @@ def test_unexpected_keyword_argument_without_suggestion(): with pytest.raises(TypeError) as excinfo: MethodTest.order_like_method("SPY", 10, completely_unrelated_name=1) message = str(excinfo.value) - assert "order_like_method() got an unexpected keyword argument " \ + assert "No method matches given arguments for order_like_method" in message + assert "Got an unexpected keyword argument " \ "'completely_unrelated_name'" in message assert "Did you mean" not in message @@ -1131,7 +1134,7 @@ def test_unexpected_keyword_argument_without_suggestion(): def test_unexpected_keyword_argument_reports_first_in_call_order(): with pytest.raises(TypeError) as excinfo: MethodTest.order_like_method("SPY", 10, first_bogus=1, second_bogus=2) - assert "got an unexpected keyword argument 'first_bogus'" in str(excinfo.value) + assert "Got an unexpected keyword argument 'first_bogus'" in str(excinfo.value) def test_valid_keyword_arguments_still_bind(): @@ -1143,7 +1146,9 @@ def test_valid_keyword_argument_names_keep_no_match_message(): # 'd' is supplied both positionally and by name: valid names, unbindable call. with pytest.raises(TypeError) as excinfo: MethodTest.DefaultParams(1, 2, 3, 4, d=5) - assert "No method matches given arguments for default_params" in str(excinfo.value) + message = str(excinfo.value) + assert "No method matches given arguments for default_params" in message + assert "unexpected keyword argument" not in message def test_optional_params(): res = MethodTest.OptionalParams(1, 2, 3, 4) From 49dc328eab53c547b71f3d7d3005d50ef210aa27 Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Tue, 11 Aug 2026 17:09:07 -0400 Subject: [PATCH 5/5] Never let bind-failure message construction throw --- src/runtime/MethodBinder.cs | 66 +++++++++++++++++++++---------------- 1 file changed, 38 insertions(+), 28 deletions(-) diff --git a/src/runtime/MethodBinder.cs b/src/runtime/MethodBinder.cs index e7b76d6c6..6d98fe671 100644 --- a/src/runtime/MethodBinder.cs +++ b/src/runtime/MethodBinder.cs @@ -1017,38 +1017,48 @@ internal virtual NewReference Invoke(BorrowedReference inst, BorrowedReference a if (!Exceptions.ErrorOccurred()) { var value = new StringBuilder("No method matches given arguments"); - // Use the snake_case name Python callers use, matching the hinted signatures below. - if (methodinfo != null && methodinfo.Length > 0) + try { - value.Append($" for {MethodSignatureFormatter.SnakeCaseName(methodinfo[0])}"); - } - else if (list.Count > 0) - { - value.Append($" for {MethodSignatureFormatter.SnakeCaseName(list[0].MethodBase)}"); - } + // Use the snake_case name Python callers use, matching the hinted signatures below. + if (methodinfo != null && methodinfo.Length > 0) + { + value.Append($" for {MethodSignatureFormatter.SnakeCaseName(methodinfo[0])}"); + } + else if (list.Count > 0) + { + value.Append($" for {MethodSignatureFormatter.SnakeCaseName(list[0].MethodBase)}"); + } - value.Append(": "); - AppendArgumentTypes(to: value, args); - - // The argument types echo above covers positional args only; name the first - // unknown kwarg (if any) so a misspelled keyword argument is visible. - AppendUnexpectedKeywordArgument(value, kw, info); - - // List the candidate overloads so the caller can see what was - // expected (e.g. that an int overload exists when a float was - // passed). Applies to every "no match" case, not just numeric ones. - var candidates = methodinfo != null && methodinfo.Length > 0 - ? methodinfo.Cast() - : list?.Select(m => m.MethodBase); - var overloads = MethodSignatureFormatter.FormatOverloads(candidates); - if (overloads.Length > 0) - { - // The kwarg hint may already end the sentence with a question mark. - if (value[value.Length - 1] != '?') + value.Append(": "); + AppendArgumentTypes(to: value, args); + + // The argument types echo above covers positional args only; name the first + // unknown kwarg (if any) so a misspelled keyword argument is visible. + AppendUnexpectedKeywordArgument(value, kw, info); + + // List the candidate overloads so the caller can see what was + // expected (e.g. that an int overload exists when a float was + // passed). Applies to every "no match" case, not just numeric ones. + var candidates = methodinfo != null && methodinfo.Length > 0 + ? methodinfo.Cast() + : list?.Select(m => m.MethodBase); + var overloads = MethodSignatureFormatter.FormatOverloads(candidates); + if (overloads.Length > 0) { - value.Append('.'); + // The kwarg hint may already end the sentence with a question mark. + if (value[value.Length - 1] != '?') + { + value.Append('.'); + } + value.Append(' ').Append(overloads); } - value.Append(' ').Append(overloads); + } + catch + { + // The details above are best-effort diagnostics over arbitrary caller + // input; an exception here would escape the tp_call slot into CPython + // and mask the bind failure. Raise with whatever was appended so far. + Exceptions.Clear(); } Exceptions.RaiseTypeError(value.ToString());