@@ -513,6 +513,165 @@ def __getattr__(self, name: str) -> HookCaller: ...
513
513
_CallHistory = list [tuple [Mapping [str , object ], Optional [Callable [[Any ], None ]]]]
514
514
515
515
516
+ class HistoricHookCaller :
517
+ """A caller for historic hook specifications that memorizes and replays calls.
518
+
519
+ Historic hooks memorize every call and replay them on plugins registered
520
+ after the call was made. Historic hooks do not support wrappers.
521
+ """
522
+
523
+ __slots__ = (
524
+ "name" ,
525
+ "spec" ,
526
+ "_hookexec" ,
527
+ "_hookimpls" ,
528
+ "_call_history" ,
529
+ )
530
+
531
+ def __init__ (
532
+ self ,
533
+ name : str ,
534
+ hook_execute : _HookExec ,
535
+ specmodule_or_class : _Namespace ,
536
+ spec_config : HookspecConfiguration ,
537
+ ) -> None :
538
+ """:meta private:"""
539
+ assert spec_config .historic , "HistoricHookCaller requires historic=True"
540
+ #: Name of the hook getting called.
541
+ self .name : Final = name
542
+ self ._hookexec : Final = hook_execute
543
+ # The hookimpls list for historic hooks (no wrappers supported)
544
+ self ._hookimpls : Final [list [HookImpl ]] = []
545
+ self ._call_history : Final [_CallHistory ] = []
546
+ # TODO: Document, or make private.
547
+ self .spec : Final = HookSpec (specmodule_or_class , name , spec_config )
548
+
549
+ def has_spec (self ) -> bool :
550
+ return True # HistoricHookCaller always has a spec
551
+
552
+ def set_specification (
553
+ self ,
554
+ specmodule_or_class : _Namespace ,
555
+ _spec_opts_or_config : HookspecOpts | HookspecConfiguration | None = None ,
556
+ * ,
557
+ spec_opts : HookspecOpts | None = None ,
558
+ spec_config : HookspecConfiguration | None = None ,
559
+ ) -> None :
560
+ """Historic hooks cannot have their specification changed after creation."""
561
+ raise ValueError (
562
+ f"HistoricHookCaller { self .name !r} already has a specification. "
563
+ "Historic hooks cannot have their specification changed."
564
+ )
565
+
566
+ def is_historic (self ) -> bool :
567
+ """Whether this caller is :ref:`historic <historic>`."""
568
+ return True # HistoricHookCaller is always historic
569
+
570
+ def _remove_plugin (self , plugin : _Plugin ) -> None :
571
+ for i , method in enumerate (self ._hookimpls ):
572
+ if method .plugin == plugin :
573
+ del self ._hookimpls [i ]
574
+ return
575
+ raise ValueError (f"plugin { plugin !r} not found" )
576
+
577
+ def get_hookimpls (self ) -> list [HookImpl ]:
578
+ """Get all registered hook implementations for this hook."""
579
+ return self ._hookimpls .copy ()
580
+
581
+ def _add_hookimpl (self , hookimpl : HookImpl ) -> None :
582
+ """Add an implementation to the callback chain."""
583
+ # Historic hooks don't support wrappers - simpler ordering
584
+ if hookimpl .trylast :
585
+ self ._hookimpls .insert (0 , hookimpl )
586
+ elif hookimpl .tryfirst :
587
+ self ._hookimpls .append (hookimpl )
588
+ else :
589
+ # find last non-tryfirst method
590
+ i = len (self ._hookimpls ) - 1
591
+ while i >= 0 and self ._hookimpls [i ].tryfirst :
592
+ i -= 1
593
+ self ._hookimpls .insert (i + 1 , hookimpl )
594
+
595
+ # Apply history to the newly added hookimpl
596
+ self ._maybe_apply_history (hookimpl )
597
+
598
+ def __repr__ (self ) -> str :
599
+ return f"<HistoricHookCaller { self .name !r} >"
600
+
601
+ def _verify_all_args_are_provided (self , kwargs : Mapping [str , object ]) -> None :
602
+ # This is written to avoid expensive operations when not needed.
603
+ if self .spec :
604
+ for argname in self .spec .argnames :
605
+ if argname not in kwargs :
606
+ notincall = ", " .join (
607
+ repr (argname )
608
+ for argname in self .spec .argnames
609
+ # Avoid self.spec.argnames - kwargs.keys()
610
+ # it doesn't preserve order.
611
+ if argname not in kwargs .keys ()
612
+ )
613
+ warnings .warn (
614
+ f"Argument(s) { notincall } which are declared in the hookspec "
615
+ "cannot be found in this hook call" ,
616
+ stacklevel = 2 ,
617
+ )
618
+ break
619
+
620
+ def __call__ (self , ** kwargs : object ) -> Any :
621
+ """Call the hook.
622
+
623
+ Historic hooks cannot be called directly. Use call_historic instead.
624
+ """
625
+ raise RuntimeError (
626
+ "Cannot directly call a historic hook - use call_historic instead."
627
+ )
628
+
629
+ def call_historic (
630
+ self ,
631
+ result_callback : Callable [[Any ], None ] | None = None ,
632
+ kwargs : Mapping [str , object ] | None = None ,
633
+ ) -> None :
634
+ """Call the hook with given ``kwargs`` for all registered plugins and
635
+ for all plugins which will be registered afterwards, see
636
+ :ref:`historic`.
637
+
638
+ :param result_callback:
639
+ If provided, will be called for each non-``None`` result obtained
640
+ from a hook implementation.
641
+ """
642
+ kwargs = kwargs or {}
643
+ self ._verify_all_args_are_provided (kwargs )
644
+ self ._call_history .append ((kwargs , result_callback ))
645
+ # Historizing hooks don't return results.
646
+ # Remember firstresult isn't compatible with historic.
647
+ # Copy because plugins may register other plugins during iteration (#438).
648
+ res = self ._hookexec (self .name , self ._hookimpls .copy (), kwargs , False )
649
+ if result_callback is None :
650
+ return
651
+ if isinstance (res , list ):
652
+ for x in res :
653
+ result_callback (x )
654
+
655
+ def call_extra (
656
+ self , methods : Sequence [Callable [..., object ]], kwargs : Mapping [str , object ]
657
+ ) -> Any :
658
+ """Call the hook with some additional temporarily participating
659
+ methods using the specified ``kwargs`` as call parameters, see
660
+ :ref:`call_extra`."""
661
+ raise RuntimeError (
662
+ "Cannot call call_extra on a historic hook - use call_historic instead."
663
+ )
664
+
665
+ def _maybe_apply_history (self , method : HookImpl ) -> None :
666
+ """Apply call history to a new hookimpl if it is marked as historic."""
667
+ for kwargs , result_callback in self ._call_history :
668
+ res = self ._hookexec (self .name , [method ], kwargs , False )
669
+ if res and result_callback is not None :
670
+ # XXX: remember firstresult isn't compat with historic
671
+ assert isinstance (res , list )
672
+ result_callback (res [0 ])
673
+
674
+
516
675
class HookCaller :
517
676
"""A caller of all registered implementations of a hook specification."""
518
677
@@ -521,7 +680,6 @@ class HookCaller:
521
680
"spec" ,
522
681
"_hookexec" ,
523
682
"_hookimpls" ,
524
- "_call_history" ,
525
683
)
526
684
527
685
def __init__ (
@@ -543,7 +701,6 @@ def __init__(
543
701
# 5. wrappers
544
702
# 6. tryfirst wrappers
545
703
self ._hookimpls : Final [list [HookImpl ]] = []
546
- self ._call_history : _CallHistory | None = None
547
704
# TODO: Document, or make private.
548
705
self .spec : HookSpec | None = None
549
706
if specmodule_or_class is not None :
@@ -589,13 +746,16 @@ def set_specification(
589
746
else :
590
747
raise TypeError ("Must provide either spec_opts or spec_config" )
591
748
592
- self .spec = HookSpec (specmodule_or_class , self .name , final_config )
593
749
if final_config .historic :
594
- self ._call_history = []
750
+ raise ValueError (
751
+ f"HookCaller cannot handle historic hooks. "
752
+ f"Use HistoricHookCaller for { self .name !r} "
753
+ )
754
+ self .spec = HookSpec (specmodule_or_class , self .name , final_config )
595
755
596
756
def is_historic (self ) -> bool :
597
757
"""Whether this caller is :ref:`historic <historic>`."""
598
- return self . _call_history is not None
758
+ return False # HookCaller is never historic
599
759
600
760
def _remove_plugin (self , plugin : _Plugin ) -> None :
601
761
for i , method in enumerate (self ._hookimpls ):
@@ -663,9 +823,6 @@ def __call__(self, **kwargs: object) -> Any:
663
823
Returns the result(s) of calling all registered plugins, see
664
824
:ref:`calling`.
665
825
"""
666
- assert not self .is_historic (), (
667
- "Cannot directly call a historic hook - use call_historic instead."
668
- )
669
826
self ._verify_all_args_are_provided (kwargs )
670
827
firstresult = self .spec .config .firstresult if self .spec else False
671
828
# Copy because plugins may register other plugins during iteration (#438).
@@ -680,33 +837,18 @@ def call_historic(
680
837
for all plugins which will be registered afterwards, see
681
838
:ref:`historic`.
682
839
683
- :param result_callback:
684
- If provided, will be called for each non-``None`` result obtained
685
- from a hook implementation.
840
+ This method should not be called on non-historic hooks.
686
841
"""
687
- assert self ._call_history is not None
688
- kwargs = kwargs or {}
689
- self ._verify_all_args_are_provided (kwargs )
690
- self ._call_history .append ((kwargs , result_callback ))
691
- # Historizing hooks don't return results.
692
- # Remember firstresult isn't compatible with historic.
693
- # Copy because plugins may register other plugins during iteration (#438).
694
- res = self ._hookexec (self .name , self ._hookimpls .copy (), kwargs , False )
695
- if result_callback is None :
696
- return
697
- if isinstance (res , list ):
698
- for x in res :
699
- result_callback (x )
842
+ raise AssertionError (
843
+ f"Hook { self .name !r} is not historic - cannot call call_historic"
844
+ )
700
845
701
846
def call_extra (
702
847
self , methods : Sequence [Callable [..., object ]], kwargs : Mapping [str , object ]
703
848
) -> Any :
704
849
"""Call the hook with some additional temporarily participating
705
850
methods using the specified ``kwargs`` as call parameters, see
706
851
:ref:`call_extra`."""
707
- assert not self .is_historic (), (
708
- "Cannot directly call a historic hook - use call_historic instead."
709
- )
710
852
self ._verify_all_args_are_provided (kwargs )
711
853
config = HookimplConfiguration ()
712
854
hookimpls = self ._hookimpls .copy ()
@@ -725,17 +867,6 @@ def call_extra(
725
867
firstresult = self .spec .config .firstresult if self .spec else False
726
868
return self ._hookexec (self .name , hookimpls , kwargs , firstresult )
727
869
728
- def _maybe_apply_history (self , method : HookImpl ) -> None :
729
- """Apply call history to a new hookimpl if it is marked as historic."""
730
- if self .is_historic ():
731
- assert self ._call_history is not None
732
- for kwargs , result_callback in self ._call_history :
733
- res = self ._hookexec (self .name , [method ], kwargs , False )
734
- if res and result_callback is not None :
735
- # XXX: remember firstresult isn't compat with historic
736
- assert isinstance (res , list )
737
- result_callback (res [0 ])
738
-
739
870
740
871
# Historical name (pluggy<=1.2), kept for backward compatibility.
741
872
_HookCaller = HookCaller
@@ -778,9 +909,38 @@ def _hookimpls(self) -> list[HookImpl]:
778
909
def spec (self ) -> HookSpec | None : # type: ignore[override]
779
910
return self ._orig .spec
780
911
781
- @property
782
- def _call_history (self ) -> _CallHistory | None : # type: ignore[override]
783
- return self ._orig ._call_history
912
+ def is_historic (self ) -> bool :
913
+ return self ._orig .is_historic ()
914
+
915
+ def call_historic (
916
+ self ,
917
+ result_callback : Callable [[Any ], None ] | None = None ,
918
+ kwargs : Mapping [str , object ] | None = None ,
919
+ ) -> None :
920
+ """Call the hook with given ``kwargs`` for all registered plugins and
921
+ for all plugins which will be registered afterwards, see
922
+ :ref:`historic`.
923
+ """
924
+ if not self .is_historic ():
925
+ raise AssertionError (
926
+ f"Hook { self .name !r} is not historic - cannot call call_historic"
927
+ )
928
+
929
+ # For subset hook callers, we need to manually handle the history and execution
930
+ kwargs = kwargs or {}
931
+ self ._verify_all_args_are_provided (kwargs )
932
+
933
+ # If the original is a HistoricHookCaller, add to its history
934
+ if hasattr (self ._orig , "_call_history" ):
935
+ self ._orig ._call_history .append ((kwargs , result_callback ))
936
+
937
+ # Execute with filtered hookimpls
938
+ res = self ._hookexec (self .name , self ._hookimpls , kwargs , False )
939
+ if result_callback is None :
940
+ return
941
+ if isinstance (res , list ):
942
+ for x in res :
943
+ result_callback (x )
784
944
785
945
def __repr__ (self ) -> str :
786
946
return f"<_SubsetHookCaller { self .name !r} >"
0 commit comments