Skip to content

Commit ed9b31b

Browse files
committed
fix review idea
Signed-off-by: Manjusaka <me@manjusaka.me>
1 parent 95a0522 commit ed9b31b

File tree

3 files changed

+150
-145
lines changed

3 files changed

+150
-145
lines changed

Lib/annotationlib.py

Lines changed: 0 additions & 145 deletions
Original file line numberDiff line numberDiff line change
@@ -1062,141 +1062,11 @@ 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 in proper order
1098-
# (parameters first, then return)
1099-
new_annotations = {}
1100-
1101-
# The first parameter (self/cls) is always kept for unbound partialmethod
1102-
first_param = func_params[0]
1103-
if first_param in func_annotations:
1104-
new_annotations[first_param] = func_annotations[first_param]
1105-
1106-
# For partialmethod, positional args bind to parameters AFTER the first one
1107-
# So if func is (self, a, b, c) and partialmethod.args=(1,)
1108-
# Then 'self' stays, 'a' is bound, 'b' and 'c' remain
1109-
1110-
remaining_params = func_params[1:]
1111-
num_positional_bound = len(partial_args)
1112-
1113-
for i, param_name in enumerate(remaining_params):
1114-
# Skip if this param is bound positionally
1115-
if i < num_positional_bound:
1116-
continue
1117-
1118-
# For keyword binding: keep the annotation (keyword sets default, doesn't remove param)
1119-
if param_name in partial_keywords:
1120-
if param_name in func_annotations:
1121-
new_annotations[param_name] = func_annotations[param_name]
1122-
continue
1123-
1124-
# This parameter is not bound, keep its annotation
1125-
if param_name in func_annotations:
1126-
new_annotations[param_name] = func_annotations[param_name]
1127-
1128-
# Add return annotation at the end
1129-
if 'return' in func_annotations:
1130-
new_annotations['return'] = func_annotations['return']
1131-
1132-
return new_annotations
1133-
1134-
except (ValueError, TypeError):
1135-
# If we can't process, return the original annotations
1136-
return func_annotations
1137-
1138-
1139-
def _get_annotations_for_partial(partial_obj, format):
1140-
"""Get annotations for a functools.partial object.
1141-
1142-
Returns annotations for the wrapped function, but only for parameters
1143-
that haven't been bound by the partial application.
1144-
"""
1145-
import inspect
1146-
1147-
# Get the wrapped function
1148-
func = partial_obj.func
1149-
1150-
# Get annotations from the wrapped function
1151-
func_annotations = get_annotations(func, format=format)
1152-
1153-
if not func_annotations:
1154-
return {}
1155-
1156-
# Get the signature to determine which parameters are bound
1157-
try:
1158-
sig = inspect.signature(partial_obj)
1159-
except (ValueError, TypeError):
1160-
# If we can't get signature, return empty dict
1161-
return {}
1162-
1163-
# Build new annotations dict with only unbound parameters
1164-
# (parameters first, then return)
1165-
new_annotations = {}
1166-
1167-
# Only include annotations for parameters that still exist in partial's signature
1168-
for param_name in sig.parameters:
1169-
if param_name in func_annotations:
1170-
new_annotations[param_name] = func_annotations[param_name]
1171-
1172-
# Add return annotation at the end
1173-
if 'return' in func_annotations:
1174-
new_annotations['return'] = func_annotations['return']
1175-
1176-
return new_annotations
1177-
1178-
11791065
def _get_and_call_annotate(obj, format):
11801066
"""Get the __annotate__ function and call it.
11811067
11821068
May not return a fresh dictionary.
11831069
"""
1184-
import functools
1185-
1186-
# Handle functools.partialmethod objects (unbound)
1187-
# Check for __partialmethod__ attribute first
1188-
try:
1189-
partialmethod = obj.__partialmethod__
1190-
except AttributeError:
1191-
pass
1192-
else:
1193-
if isinstance(partialmethod, functools.partialmethod):
1194-
return _get_annotations_for_partialmethod(partialmethod, format)
1195-
1196-
# Handle functools.partial objects
1197-
if isinstance(obj, functools.partial):
1198-
return _get_annotations_for_partial(obj, format)
1199-
12001070
annotate = getattr(obj, "__annotate__", None)
12011071
if annotate is not None:
12021072
ann = call_annotate_function(annotate, format, owner=obj)
@@ -1214,21 +1084,6 @@ def _get_dunder_annotations(obj):
12141084
12151085
Does not return a fresh dictionary.
12161086
"""
1217-
# Check for functools.partialmethod - skip __annotations__ and use __annotate__ path
1218-
import functools
1219-
try:
1220-
partialmethod = obj.__partialmethod__
1221-
if isinstance(partialmethod, functools.partialmethod):
1222-
# Return None to trigger _get_and_call_annotate
1223-
return None
1224-
except AttributeError:
1225-
pass
1226-
1227-
# Check for functools.partial - skip __annotations__ and use __annotate__ path
1228-
if isinstance(obj, functools.partial):
1229-
# Return None to trigger _get_and_call_annotate
1230-
return None
1231-
12321087
# This special case is needed to support types defined under
12331088
# from __future__ import annotations, where accessing the __annotations__
12341089
# attribute directly might return annotations for the wrong class.

Lib/functools.py

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -467,6 +467,8 @@ def _method(cls_or_self, /, *args, **keywords):
467467
return self.func(cls_or_self, *pto_args, *args, **keywords)
468468
_method.__isabstractmethod__ = self.__isabstractmethod__
469469
_method.__partialmethod__ = self
470+
# Set __annotate__ to delegate to the partialmethod's __annotate__
471+
_method.__annotate__ = self.__annotate__
470472
return _method
471473

472474
def __get__(self, obj, cls=None):
@@ -492,6 +494,10 @@ def __get__(self, obj, cls=None):
492494
def __isabstractmethod__(self):
493495
return getattr(self.func, "__isabstractmethod__", False)
494496

497+
def __annotate__(self, format):
498+
"""Return annotations for the partial method."""
499+
return _partialmethod_annotate(self, format)
500+
495501
__class_getitem__ = classmethod(GenericAlias)
496502

497503

@@ -513,6 +519,123 @@ def _unwrap_partialmethod(func):
513519
func = _unwrap_partial(func)
514520
return func
515521

522+
def _partial_annotate(partial_obj, format):
523+
"""Helper function to compute annotations for a partial object.
524+
525+
This is called by the __annotate__ descriptor defined in C.
526+
Returns annotations for the wrapped function, but only for parameters
527+
that haven't been bound by the partial application.
528+
"""
529+
import inspect
530+
from annotationlib import get_annotations
531+
532+
# Get the wrapped function
533+
func = partial_obj.func
534+
535+
# Get annotations from the wrapped function
536+
func_annotations = get_annotations(func, format=format)
537+
538+
if not func_annotations:
539+
return {}
540+
541+
# Get the signature to determine which parameters are bound
542+
try:
543+
sig = inspect.signature(partial_obj)
544+
except (ValueError, TypeError):
545+
# If we can't get signature, return empty dict
546+
return {}
547+
548+
# Build new annotations dict with only unbound parameters
549+
# (parameters first, then return)
550+
new_annotations = {}
551+
552+
# Only include annotations for parameters that still exist in partial's signature
553+
for param_name in sig.parameters:
554+
if param_name in func_annotations:
555+
new_annotations[param_name] = func_annotations[param_name]
556+
557+
# Add return annotation at the end
558+
if 'return' in func_annotations:
559+
new_annotations['return'] = func_annotations['return']
560+
561+
return new_annotations
562+
563+
def _partialmethod_annotate(partialmethod_obj, format):
564+
"""Helper function to compute annotations for a partialmethod object.
565+
566+
This is called when accessing annotations on an unbound partialmethod
567+
(via the __partialmethod__ attribute).
568+
Returns annotations for the wrapped function, but only for parameters
569+
that haven't been bound by the partial application. The first parameter
570+
(usually 'self' or 'cls') is kept since partialmethod is unbound.
571+
"""
572+
import inspect
573+
from annotationlib import get_annotations
574+
575+
# Get the wrapped function
576+
func = partialmethod_obj.func
577+
578+
# Get annotations from the wrapped function
579+
func_annotations = get_annotations(func, format=format)
580+
581+
if not func_annotations:
582+
return {}
583+
584+
# For partialmethod, we need to simulate the signature calculation
585+
# The first parameter (self/cls) should remain, but bound args should be removed
586+
try:
587+
# Get the function signature
588+
func_sig = inspect.signature(func)
589+
func_params = list(func_sig.parameters.keys())
590+
591+
if not func_params:
592+
return func_annotations
593+
594+
# Calculate which parameters are bound by the partialmethod
595+
partial_args = partialmethod_obj.args or ()
596+
partial_keywords = partialmethod_obj.keywords or {}
597+
598+
# Build new annotations dict in proper order
599+
# (parameters first, then return)
600+
new_annotations = {}
601+
602+
# The first parameter (self/cls) is always kept for unbound partialmethod
603+
first_param = func_params[0]
604+
if first_param in func_annotations:
605+
new_annotations[first_param] = func_annotations[first_param]
606+
607+
# For partialmethod, positional args bind to parameters AFTER the first one
608+
# So if func is (self, a, b, c) and partialmethod.args=(1,)
609+
# Then 'self' stays, 'a' is bound, 'b' and 'c' remain
610+
611+
remaining_params = func_params[1:]
612+
num_positional_bound = len(partial_args)
613+
614+
for i, param_name in enumerate(remaining_params):
615+
# Skip if this param is bound positionally
616+
if i < num_positional_bound:
617+
continue
618+
619+
# For keyword binding: keep the annotation (keyword sets default, doesn't remove param)
620+
if param_name in partial_keywords:
621+
if param_name in func_annotations:
622+
new_annotations[param_name] = func_annotations[param_name]
623+
continue
624+
625+
# This parameter is not bound, keep its annotation
626+
if param_name in func_annotations:
627+
new_annotations[param_name] = func_annotations[param_name]
628+
629+
# Add return annotation at the end
630+
if 'return' in func_annotations:
631+
new_annotations['return'] = func_annotations['return']
632+
633+
return new_annotations
634+
635+
except (ValueError, TypeError):
636+
# If we can't process, return the original annotations
637+
return func_annotations
638+
516639
################################################################################
517640
### LRU Cache function decorator
518641
################################################################################

Modules/_functoolsmodule.c

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,32 @@ partial_descr_get(PyObject *self, PyObject *obj, PyObject *type)
360360
return PyMethod_New(self, obj);
361361
}
362362

363+
static PyObject *
364+
partial_annotate(PyObject *self, PyObject *format_obj)
365+
{
366+
/* Delegate to Python functools._partial_annotate helper */
367+
PyObject *functools = NULL, *helper = NULL, *result = NULL;
368+
369+
/* Import functools module */
370+
functools = PyImport_ImportModule("functools");
371+
if (functools == NULL) {
372+
return NULL;
373+
}
374+
375+
/* Get the _partial_annotate function */
376+
helper = PyObject_GetAttrString(functools, "_partial_annotate");
377+
Py_DECREF(functools);
378+
if (helper == NULL) {
379+
return NULL;
380+
}
381+
382+
/* Call _partial_annotate(self, format) */
383+
result = PyObject_CallFunctionObjArgs(helper, self, format_obj, NULL);
384+
Py_DECREF(helper);
385+
386+
return result;
387+
}
388+
363389
static PyObject *
364390
partial_vectorcall(PyObject *self, PyObject *const *args,
365391
size_t nargsf, PyObject *kwnames)
@@ -832,6 +858,7 @@ partial_setstate(PyObject *self, PyObject *state)
832858
static PyMethodDef partial_methods[] = {
833859
{"__reduce__", partial_reduce, METH_NOARGS},
834860
{"__setstate__", partial_setstate, METH_O},
861+
{"__annotate__", partial_annotate, METH_O},
835862
{"__class_getitem__", Py_GenericAlias,
836863
METH_O|METH_CLASS, PyDoc_STR("See PEP 585")},
837864
{NULL, NULL} /* sentinel */

0 commit comments

Comments
 (0)