@@ -1016,6 +1016,14 @@ internal virtual NewReference Invoke(BorrowedReference inst, BorrowedReference a
10161016 // If we already have an exception pending, don't create a new one
10171017 if ( ! Exceptions . ErrorOccurred ( ) )
10181018 {
1019+ // A keyword argument whose name no candidate overload accepts gets the
1020+ // Python-native "unexpected keyword argument" error: the generic no-match
1021+ // message below does not echo kwargs, leaving the actual mistake invisible.
1022+ if ( TryRaiseUnexpectedKeywordArgumentError ( kw , info , methodinfo ) )
1023+ {
1024+ return default ;
1025+ }
1026+
10191027 var value = new StringBuilder ( "No method matches given arguments" ) ;
10201028 // Use the snake_case name Python callers use, matching the hinted signatures below.
10211029 if ( methodinfo != null && methodinfo . Length > 0 )
@@ -1123,6 +1131,134 @@ internal virtual NewReference Invoke(BorrowedReference inst, BorrowedReference a
11231131 return Converter . ToPython ( result , returnType ) ;
11241132 }
11251133
1134+ /// <summary>
1135+ /// When a bind failure involves a keyword argument whose name no candidate overload
1136+ /// accepts, raises the Python-style "got an unexpected keyword argument" TypeError
1137+ /// (with a "Did you mean" hint when a similarly-named parameter exists) and returns
1138+ /// true. Returns false when every kwarg name is accepted by at least one overload,
1139+ /// so the generic no-match error is raised instead.
1140+ /// </summary>
1141+ private bool TryRaiseUnexpectedKeywordArgumentError ( BorrowedReference kw , MethodBase info , MethodInfo [ ] methodinfo )
1142+ {
1143+ var kwCount = kw == null ? 0 : ( int ) Runtime . PyDict_Size ( kw ) ;
1144+ if ( kwCount <= 0 )
1145+ {
1146+ return false ;
1147+ }
1148+
1149+ // The same candidate set Bind considered: parameter names are snake_case for
1150+ // snake_case-registered methods and original for the original ones, matching
1151+ // the names the caller can actually use.
1152+ var methods = info == null
1153+ ? GetMethods ( )
1154+ : new List < MethodInformation > ( 1 ) { new MethodInformation ( info , true ) } ;
1155+ var parameterNames = new HashSet < string > ( StringComparer . Ordinal ) ;
1156+ foreach ( var method in methods )
1157+ {
1158+ foreach ( var parameterName in method . ParameterNames )
1159+ {
1160+ parameterNames . Add ( parameterName ) ;
1161+ }
1162+ }
1163+
1164+ // Report the first unknown kwarg in call order, like CPython does.
1165+ string unexpectedName = null ;
1166+ using ( var keyList = Runtime . PyDict_Keys ( kw ) )
1167+ {
1168+ for ( var i = 0 ; i < kwCount && unexpectedName == null ; i ++ )
1169+ {
1170+ var name = Runtime . GetManagedString ( Runtime . PyList_GetItem ( keyList . Borrow ( ) , i ) ) ;
1171+ if ( name != null && ! parameterNames . Contains ( name ) )
1172+ {
1173+ unexpectedName = name ;
1174+ }
1175+ }
1176+ }
1177+
1178+ if ( unexpectedName == null )
1179+ {
1180+ return false ;
1181+ }
1182+
1183+ string methodName = null ;
1184+ if ( methodinfo != null && methodinfo . Length > 0 )
1185+ {
1186+ methodName = MethodSignatureFormatter . SnakeCaseName ( methodinfo [ 0 ] ) ;
1187+ }
1188+ else if ( list . Count > 0 )
1189+ {
1190+ methodName = MethodSignatureFormatter . SnakeCaseName ( list [ 0 ] . MethodBase ) ;
1191+ }
1192+ if ( string . IsNullOrEmpty ( methodName ) )
1193+ {
1194+ return false ;
1195+ }
1196+
1197+ var message = $ "{ methodName } () got an unexpected keyword argument '{ unexpectedName } '";
1198+ var suggestion = ClosestParameterName ( unexpectedName , parameterNames ) ;
1199+ if ( suggestion != null )
1200+ {
1201+ message += $ ". Did you mean '{ suggestion } '?";
1202+ }
1203+
1204+ Exceptions . RaiseTypeError ( message ) ;
1205+ return true ;
1206+ }
1207+
1208+ /// <summary>
1209+ /// The candidate parameter name closest to the unexpected kwarg name, or null when
1210+ /// none is similar enough to suggest. A candidate is considered when it is within
1211+ /// a small edit distance of the name, or when one contains the other (e.g. 'as_tag'
1212+ /// suggests 'tag'); containment requires 3+ characters so tiny names don't match.
1213+ /// </summary>
1214+ private static string ClosestParameterName ( string name , HashSet < string > parameterNames )
1215+ {
1216+ const int MinContainmentLength = 3 ;
1217+ var threshold = Math . Max ( 2 , name . Length / 3 ) ;
1218+ string best = null ;
1219+ var bestDistance = int . MaxValue ;
1220+ foreach ( var candidate in parameterNames )
1221+ {
1222+ var distance = KeywordEditDistance ( name , candidate ) ;
1223+ var related = distance <= threshold
1224+ || ( candidate . Length >= MinContainmentLength && name . Length >= MinContainmentLength
1225+ && ( candidate . IndexOf ( name , StringComparison . OrdinalIgnoreCase ) >= 0
1226+ || name . IndexOf ( candidate , StringComparison . OrdinalIgnoreCase ) >= 0 ) ) ;
1227+ if ( related && ( distance < bestDistance
1228+ || ( distance == bestDistance && string . CompareOrdinal ( candidate , best ) < 0 ) ) )
1229+ {
1230+ bestDistance = distance ;
1231+ best = candidate ;
1232+ }
1233+ }
1234+ return best ;
1235+ }
1236+
1237+ // Case-insensitive Levenshtein distance, local to keyword suggestions.
1238+ private static int KeywordEditDistance ( string a , string b )
1239+ {
1240+ a = a . ToLowerInvariant ( ) ;
1241+ b = b . ToLowerInvariant ( ) ;
1242+ if ( a . Length == 0 ) return b . Length ;
1243+ if ( b . Length == 0 ) return a . Length ;
1244+
1245+ var prev = new int [ b . Length + 1 ] ;
1246+ var curr = new int [ b . Length + 1 ] ;
1247+ for ( var j = 0 ; j <= b . Length ; j ++ ) prev [ j ] = j ;
1248+
1249+ for ( var i = 1 ; i <= a . Length ; i ++ )
1250+ {
1251+ curr [ 0 ] = i ;
1252+ for ( var j = 1 ; j <= b . Length ; j ++ )
1253+ {
1254+ var cost = a [ i - 1 ] == b [ j - 1 ] ? 0 : 1 ;
1255+ curr [ j ] = Math . Min ( Math . Min ( curr [ j - 1 ] + 1 , prev [ j ] + 1 ) , prev [ j - 1 ] + cost ) ;
1256+ }
1257+ ( prev , curr ) = ( curr , prev ) ;
1258+ }
1259+ return prev [ b . Length ] ;
1260+ }
1261+
11261262 /// <summary>
11271263 /// Utility class to store the information about a <see cref="MethodBase"/>
11281264 /// </summary>
0 commit comments