@@ -365,6 +365,152 @@ def _partial_repr(self):
365365 args .extend (f"{ k } ={ v !r} " for k , v in self .keywords .items ())
366366 return f"{ module } .{ qualname } ({ ', ' .join (args )} )"
367367
368+
369+ ################################################################################
370+ ### _partial_annotate() - compute annotations for partial objects
371+ ################################################################################
372+
373+ def _partial_annotate (partial_obj , format ):
374+ """Helper function to compute annotations for a partial object.
375+
376+ This is called by the __annotate__ descriptor defined in C.
377+ Returns annotations for the wrapped function, but only for parameters
378+ that haven't been bound by the partial application.
379+ """
380+ import inspect
381+ from annotationlib import get_annotations
382+
383+ # Get annotations from the wrapped function
384+ func_annotations = get_annotations (partial_obj .func , format = format )
385+
386+ if not func_annotations :
387+ return {}
388+
389+ # Get the signature to determine which parameters are bound
390+ try :
391+ sig = inspect .signature (partial_obj , annotation_format = format )
392+ except (ValueError , TypeError ) as e :
393+ # If we can't get signature, we can't reliably determine which
394+ # parameters are bound. Raise an error rather than returning
395+ # incorrect annotations.
396+ raise TypeError (
397+ f"Cannot compute annotations for { partial_obj !r} : "
398+ f"unable to determine signature"
399+ ) from e
400+
401+ # Build new annotations dict with only unbound parameters
402+ # (parameters first, then return)
403+ new_annotations = {}
404+
405+ # Only include annotations for parameters that still exist in partial's signature
406+ for param_name in sig .parameters :
407+ if param_name in func_annotations :
408+ new_annotations [param_name ] = func_annotations [param_name ]
409+
410+ # Add return annotation at the end
411+ if 'return' in func_annotations :
412+ new_annotations ['return' ] = func_annotations ['return' ]
413+
414+ return new_annotations
415+
416+
417+ ################################################################################
418+ ### _partialmethod_annotate() - compute annotations for partialmethod objects
419+ ################################################################################
420+
421+ def _partialmethod_annotate (partialmethod_obj , format ):
422+ """Helper function to compute annotations for a partialmethod object.
423+
424+ This is called when accessing annotations on an unbound partialmethod
425+ (via the __partialmethod__ attribute).
426+ Returns annotations for the wrapped function, but only for parameters
427+ that haven't been bound by the partial application. The first parameter
428+ (usually 'self' or 'cls') is kept since partialmethod is unbound.
429+ """
430+ import inspect
431+ from annotationlib import get_annotations
432+
433+ # Get annotations from the wrapped function
434+ func_annotations = get_annotations (partialmethod_obj .func , format = format )
435+
436+ if not func_annotations :
437+ return {}
438+
439+ # For partialmethod, we need to simulate the signature calculation
440+ # The first parameter (self/cls) should remain, but bound args should be removed
441+ try :
442+ # Get the function signature
443+ func_sig = inspect .signature (partialmethod_obj .func , annotation_format = format )
444+ func_params = list (func_sig .parameters .keys ())
445+
446+ if not func_params :
447+ return func_annotations
448+
449+ # Calculate which parameters are bound by the partialmethod
450+ partial_args = partialmethod_obj .args or ()
451+ partial_keywords = partialmethod_obj .keywords or {}
452+
453+ # Build new annotations dict in proper order
454+ # (parameters first, then return)
455+ new_annotations = {}
456+
457+ # The first parameter (self/cls) is always kept for unbound partialmethod
458+ first_param = func_params [0 ]
459+ if first_param in func_annotations :
460+ new_annotations [first_param ] = func_annotations [first_param ]
461+
462+ # For partialmethod, positional args bind to parameters AFTER the first one
463+ # So if func is (self, a, b, c) and partialmethod.args=(1,)
464+ # Then 'self' stays, 'a' is bound, 'b' and 'c' remain
465+
466+ # We need to account for Placeholders which create "holes"
467+ # For example: partialmethod(func, 1, Placeholder, 3) binds 'a' and 'c' but not 'b'
468+
469+ remaining_params = func_params [1 :]
470+
471+ # Track which positions are filled by Placeholder
472+ placeholder_positions = set ()
473+ for i , arg in enumerate (partial_args ):
474+ if arg is Placeholder :
475+ placeholder_positions .add (i )
476+
477+ # Number of non-Placeholder positional args
478+ # This doesn't directly tell us which params are bound due to Placeholders
479+
480+ for i , param_name in enumerate (remaining_params ):
481+ # Check if this position has a Placeholder
482+ if i in placeholder_positions :
483+ # This parameter is deferred by Placeholder, keep it
484+ if param_name in func_annotations :
485+ new_annotations [param_name ] = func_annotations [param_name ]
486+ continue
487+
488+ # Check if this position is beyond the partial_args
489+ if i >= len (partial_args ):
490+ # This parameter is not bound at all, keep it
491+ if param_name in func_annotations :
492+ new_annotations [param_name ] = func_annotations [param_name ]
493+ continue
494+
495+ # Otherwise, this position is bound (not a Placeholder and within bounds)
496+ # Skip it
497+
498+ # Add return annotation at the end
499+ if 'return' in func_annotations :
500+ new_annotations ['return' ] = func_annotations ['return' ]
501+
502+ return new_annotations
503+
504+ except (ValueError , TypeError ) as e :
505+ # If we can't process the signature, we can't reliably determine
506+ # which parameters are bound. Raise an error rather than returning
507+ # incorrect annotations (which would include bound parameters).
508+ raise TypeError (
509+ f"Cannot compute annotations for { partialmethod_obj !r} : "
510+ f"unable to determine which parameters are bound"
511+ ) from e
512+
513+
368514# Purely functional, no descriptor behaviour
369515class partial :
370516 """New function with partial application of the given arguments
@@ -499,8 +645,6 @@ def __isabstractmethod__(self):
499645 __class_getitem__ = classmethod (GenericAlias )
500646
501647
502- # Helper functions
503-
504648def _unwrap_partial (func ):
505649 while isinstance (func , partial ):
506650 func = func .func
@@ -517,140 +661,6 @@ def _unwrap_partialmethod(func):
517661 func = _unwrap_partial (func )
518662 return func
519663
520- def _partial_annotate (partial_obj , format ):
521- """Helper function to compute annotations for a partial object.
522-
523- This is called by the __annotate__ descriptor defined in C.
524- Returns annotations for the wrapped function, but only for parameters
525- that haven't been bound by the partial application.
526- """
527- import inspect
528- from annotationlib import get_annotations
529-
530- # Get annotations from the wrapped function
531- func_annotations = get_annotations (partial_obj .func , format = format )
532-
533- if not func_annotations :
534- return {}
535-
536- # Get the signature to determine which parameters are bound
537- try :
538- sig = inspect .signature (partial_obj , annotation_format = format )
539- except (ValueError , TypeError ) as e :
540- # If we can't get signature, we can't reliably determine which
541- # parameters are bound. Raise an error rather than returning
542- # incorrect annotations.
543- raise TypeError (
544- f"Cannot compute annotations for { partial_obj !r} : "
545- f"unable to determine signature"
546- ) from e
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 annotations from the wrapped function
576- func_annotations = get_annotations (partialmethod_obj .func , format = format )
577-
578- if not func_annotations :
579- return {}
580-
581- # For partialmethod, we need to simulate the signature calculation
582- # The first parameter (self/cls) should remain, but bound args should be removed
583- try :
584- # Get the function signature
585- func_sig = inspect .signature (partialmethod_obj .func , annotation_format = format )
586- func_params = list (func_sig .parameters .keys ())
587-
588- if not func_params :
589- return func_annotations
590-
591- # Calculate which parameters are bound by the partialmethod
592- partial_args = partialmethod_obj .args or ()
593- partial_keywords = partialmethod_obj .keywords or {}
594-
595- # Build new annotations dict in proper order
596- # (parameters first, then return)
597- new_annotations = {}
598-
599- # The first parameter (self/cls) is always kept for unbound partialmethod
600- first_param = func_params [0 ]
601- if first_param in func_annotations :
602- new_annotations [first_param ] = func_annotations [first_param ]
603-
604- # For partialmethod, positional args bind to parameters AFTER the first one
605- # So if func is (self, a, b, c) and partialmethod.args=(1,)
606- # Then 'self' stays, 'a' is bound, 'b' and 'c' remain
607-
608- # We need to account for Placeholders which create "holes"
609- # For example: partialmethod(func, 1, Placeholder, 3) binds 'a' and 'c' but not 'b'
610-
611- remaining_params = func_params [1 :]
612-
613- # Track which positions are filled by Placeholder
614- placeholder_positions = set ()
615- for i , arg in enumerate (partial_args ):
616- if arg is Placeholder :
617- placeholder_positions .add (i )
618-
619- # Number of non-Placeholder positional args
620- # This doesn't directly tell us which params are bound due to Placeholders
621-
622- for i , param_name in enumerate (remaining_params ):
623- # Check if this position has a Placeholder
624- if i in placeholder_positions :
625- # This parameter is deferred by Placeholder, keep it
626- if param_name in func_annotations :
627- new_annotations [param_name ] = func_annotations [param_name ]
628- continue
629-
630- # Check if this position is beyond the partial_args
631- if i >= len (partial_args ):
632- # This parameter is not bound at all, keep it
633- if param_name in func_annotations :
634- new_annotations [param_name ] = func_annotations [param_name ]
635- continue
636-
637- # Otherwise, this position is bound (not a Placeholder and within bounds)
638- # Skip it
639-
640- # Add return annotation at the end
641- if 'return' in func_annotations :
642- new_annotations ['return' ] = func_annotations ['return' ]
643-
644- return new_annotations
645-
646- except (ValueError , TypeError ) as e :
647- # If we can't process the signature, we can't reliably determine
648- # which parameters are bound. Raise an error rather than returning
649- # incorrect annotations (which would include bound parameters).
650- raise TypeError (
651- f"Cannot compute annotations for { partialmethod_obj !r} : "
652- f"unable to determine which parameters are bound"
653- ) from e
654664
655665################################################################################
656666### LRU Cache function decorator
0 commit comments