@@ -1062,11 +1062,139 @@ def annotations_to_string(annotations):
10621062 }
10631063
10641064
1065+ def _get_annotations_for_partialmethod (partialmethod_obj , format ):
1066+ """Get annotations for a functools.partialmethod object.
1067+
1068+ Returns annotations for the wrapped function, but only for parameters
1069+ that haven't been bound by the partial application. The first parameter
1070+ (usually 'self' or 'cls') is kept since partialmethod is unbound.
1071+ """
1072+ import inspect
1073+
1074+ # Get the wrapped function
1075+ func = partialmethod_obj .func
1076+
1077+ # Get annotations from the wrapped function
1078+ func_annotations = get_annotations (func , format = format )
1079+
1080+ if not func_annotations :
1081+ return {}
1082+
1083+ # For partialmethod, we need to simulate the signature calculation
1084+ # The first parameter (self/cls) should remain, but bound args should be removed
1085+ try :
1086+ # Get the function signature
1087+ func_sig = inspect .signature (func )
1088+ func_params = list (func_sig .parameters .keys ())
1089+
1090+ if not func_params :
1091+ return func_annotations
1092+
1093+ # Calculate which parameters are bound by the partialmethod
1094+ partial_args = partialmethod_obj .args or ()
1095+ partial_keywords = partialmethod_obj .keywords or {}
1096+
1097+ # Build new annotations dict
1098+ new_annotations = {}
1099+
1100+ # Keep return annotation if present
1101+ if 'return' in func_annotations :
1102+ new_annotations ['return' ] = func_annotations ['return' ]
1103+
1104+ # The first parameter (self/cls) is always kept for unbound partialmethod
1105+ first_param = func_params [0 ]
1106+ if first_param in func_annotations :
1107+ new_annotations [first_param ] = func_annotations [first_param ]
1108+
1109+ # For partialmethod, positional args bind to parameters AFTER the first one
1110+ # So if func is (self, a, b, c) and partialmethod.args=(1,)
1111+ # Then 'self' stays, 'a' is bound, 'b' and 'c' remain
1112+
1113+ remaining_params = func_params [1 :]
1114+ num_positional_bound = len (partial_args )
1115+
1116+ for i , param_name in enumerate (remaining_params ):
1117+ # Skip if this param is bound positionally
1118+ if i < num_positional_bound :
1119+ continue
1120+
1121+ # For keyword binding: keep the annotation (keyword sets default, doesn't remove param)
1122+ if param_name in partial_keywords :
1123+ if param_name in func_annotations :
1124+ new_annotations [param_name ] = func_annotations [param_name ]
1125+ continue
1126+
1127+ # This parameter is not bound, keep its annotation
1128+ if param_name in func_annotations :
1129+ new_annotations [param_name ] = func_annotations [param_name ]
1130+
1131+ return new_annotations
1132+
1133+ except (ValueError , TypeError ):
1134+ # If we can't process, return the original annotations
1135+ return func_annotations
1136+
1137+
1138+ def _get_annotations_for_partial (partial_obj , format ):
1139+ """Get annotations for a functools.partial object.
1140+
1141+ Returns annotations for the wrapped function, but only for parameters
1142+ that haven't been bound by the partial application.
1143+ """
1144+ import inspect
1145+
1146+ # Get the wrapped function
1147+ func = partial_obj .func
1148+
1149+ # Get annotations from the wrapped function
1150+ func_annotations = get_annotations (func , format = format )
1151+
1152+ if not func_annotations :
1153+ return {}
1154+
1155+ # Get the signature to determine which parameters are bound
1156+ try :
1157+ sig = inspect .signature (partial_obj )
1158+ except (ValueError , TypeError ):
1159+ # If we can't get signature, return empty dict
1160+ return {}
1161+
1162+ # Build new annotations dict with only unbound parameters
1163+ new_annotations = {}
1164+
1165+ # Keep return annotation if present
1166+ if 'return' in func_annotations :
1167+ new_annotations ['return' ] = func_annotations ['return' ]
1168+
1169+ # Only include annotations for parameters that still exist in partial's signature
1170+ for param_name in sig .parameters :
1171+ if param_name in func_annotations :
1172+ new_annotations [param_name ] = func_annotations [param_name ]
1173+
1174+ return new_annotations
1175+
1176+
10651177def _get_and_call_annotate (obj , format ):
10661178 """Get the __annotate__ function and call it.
10671179
10681180 May not return a fresh dictionary.
10691181 """
1182+ import functools
1183+
1184+ # Handle functools.partialmethod objects (unbound)
1185+ # Check for __partialmethod__ attribute first
1186+ try :
1187+ partialmethod = obj .__partialmethod__
1188+ except AttributeError :
1189+ pass
1190+ else :
1191+ if isinstance (partialmethod , functools .partialmethod ):
1192+ return _get_annotations_for_partialmethod (partialmethod , format )
1193+
1194+ # Handle functools.partial objects
1195+ if isinstance (obj , functools .partial ):
1196+ return _get_annotations_for_partial (obj , format )
1197+
10701198 annotate = getattr (obj , "__annotate__" , None )
10711199 if annotate is not None :
10721200 ann = call_annotate_function (annotate , format , owner = obj )
@@ -1084,6 +1212,21 @@ def _get_dunder_annotations(obj):
10841212
10851213 Does not return a fresh dictionary.
10861214 """
1215+ # Check for functools.partialmethod - skip __annotations__ and use __annotate__ path
1216+ import functools
1217+ try :
1218+ partialmethod = obj .__partialmethod__
1219+ if isinstance (partialmethod , functools .partialmethod ):
1220+ # Return None to trigger _get_and_call_annotate
1221+ return None
1222+ except AttributeError :
1223+ pass
1224+
1225+ # Check for functools.partial - skip __annotations__ and use __annotate__ path
1226+ if isinstance (obj , functools .partial ):
1227+ # Return None to trigger _get_and_call_annotate
1228+ return None
1229+
10871230 # This special case is needed to support types defined under
10881231 # from __future__ import annotations, where accessing the __annotations__
10891232 # attribute directly might return annotations for the wrong class.
0 commit comments