17
17
from typing import final
18
18
from typing import Optional
19
19
from typing import overload
20
+ from typing import Protocol
21
+ from typing import runtime_checkable
20
22
from typing import TYPE_CHECKING
21
23
from typing import TypedDict
22
24
from typing import TypeVar
38
40
_HookImplFunction = Callable [..., Union [_T , Generator [None , Result [_T ], None ]]]
39
41
40
42
43
+ @runtime_checkable
44
+ class HookCaller (Protocol ):
45
+ """Protocol defining the interface for hook callers."""
46
+
47
+ @property
48
+ def name (self ) -> str :
49
+ """Name of the hook getting called."""
50
+ ...
51
+
52
+ @property
53
+ def spec (self ) -> HookSpec | None :
54
+ """The hook specification, if any."""
55
+ ...
56
+
57
+ def has_spec (self ) -> bool :
58
+ """Whether this caller has a hook specification."""
59
+ ...
60
+
61
+ def is_historic (self ) -> bool :
62
+ """Whether this caller is historic."""
63
+ ...
64
+
65
+ def get_hookimpls (self ) -> list [HookImpl ]:
66
+ """Get all registered hook implementations for this hook."""
67
+ ...
68
+
69
+ def set_specification (
70
+ self ,
71
+ specmodule_or_class : _Namespace ,
72
+ _spec_opts_or_config : HookspecOpts | HookspecConfiguration | None = None ,
73
+ * ,
74
+ spec_opts : HookspecOpts | None = None ,
75
+ spec_config : HookspecConfiguration | None = None ,
76
+ ) -> None :
77
+ """Set the hook specification."""
78
+ ...
79
+
80
+ def __call__ (self , ** kwargs : object ) -> Any :
81
+ """Call the hook with given kwargs."""
82
+ ...
83
+
84
+ def call_historic (
85
+ self ,
86
+ result_callback : Callable [[Any ], None ] | None = None ,
87
+ kwargs : Mapping [str , object ] | None = None ,
88
+ ) -> None :
89
+ """Call the hook historically."""
90
+ ...
91
+
92
+ def call_extra (
93
+ self , methods : Sequence [Callable [..., object ]], kwargs : Mapping [str , object ]
94
+ ) -> Any :
95
+ """Call the hook with additional methods."""
96
+ ...
97
+
98
+ def __repr__ (self ) -> str :
99
+ """String representation of the hook caller."""
100
+ ...
101
+
102
+
41
103
class HookspecOpts (TypedDict ):
42
104
"""Options for a hook specification."""
43
105
@@ -598,25 +660,6 @@ def _add_hookimpl(self, hookimpl: HookImpl) -> None:
598
660
def __repr__ (self ) -> str :
599
661
return f"<HistoricHookCaller { self .name !r} >"
600
662
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
663
def __call__ (self , ** kwargs : object ) -> Any :
621
664
"""Call the hook.
622
665
@@ -640,7 +683,7 @@ def call_historic(
640
683
from a hook implementation.
641
684
"""
642
685
kwargs = kwargs or {}
643
- self ._verify_all_args_are_provided (kwargs )
686
+ self .spec . verify_all_args_are_provided (kwargs )
644
687
self ._call_history .append ((kwargs , result_callback ))
645
688
# Historizing hooks don't return results.
646
689
# Remember firstresult isn't compatible with historic.
@@ -672,7 +715,7 @@ def _maybe_apply_history(self, method: HookImpl) -> None:
672
715
result_callback (res [0 ])
673
716
674
717
675
- class HookCaller :
718
+ class NormalHookCaller :
676
719
"""A caller of all registered implementations of a hook specification."""
677
720
678
721
__slots__ = (
@@ -793,26 +836,7 @@ def _add_hookimpl(self, hookimpl: HookImpl) -> None:
793
836
self ._hookimpls .insert (i + 1 , hookimpl )
794
837
795
838
def __repr__ (self ) -> str :
796
- return f"<HookCaller { self .name !r} >"
797
-
798
- def _verify_all_args_are_provided (self , kwargs : Mapping [str , object ]) -> None :
799
- # This is written to avoid expensive operations when not needed.
800
- if self .spec :
801
- for argname in self .spec .argnames :
802
- if argname not in kwargs :
803
- notincall = ", " .join (
804
- repr (argname )
805
- for argname in self .spec .argnames
806
- # Avoid self.spec.argnames - kwargs.keys()
807
- # it doesn't preserve order.
808
- if argname not in kwargs .keys ()
809
- )
810
- warnings .warn (
811
- f"Argument(s) { notincall } which are declared in the hookspec "
812
- "cannot be found in this hook call" ,
813
- stacklevel = 2 ,
814
- )
815
- break
839
+ return f"<NormalHookCaller { self .name !r} >"
816
840
817
841
def __call__ (self , ** kwargs : object ) -> Any :
818
842
"""Call the hook.
@@ -823,7 +847,8 @@ def __call__(self, **kwargs: object) -> Any:
823
847
Returns the result(s) of calling all registered plugins, see
824
848
:ref:`calling`.
825
849
"""
826
- self ._verify_all_args_are_provided (kwargs )
850
+ if self .spec :
851
+ self .spec .verify_all_args_are_provided (kwargs )
827
852
firstresult = self .spec .config .firstresult if self .spec else False
828
853
# Copy because plugins may register other plugins during iteration (#438).
829
854
return self ._hookexec (self .name , self ._hookimpls .copy (), kwargs , firstresult )
@@ -849,7 +874,8 @@ def call_extra(
849
874
"""Call the hook with some additional temporarily participating
850
875
methods using the specified ``kwargs`` as call parameters, see
851
876
:ref:`call_extra`."""
852
- self ._verify_all_args_are_provided (kwargs )
877
+ if self .spec :
878
+ self .spec .verify_all_args_are_provided (kwargs )
853
879
config = HookimplConfiguration ()
854
880
hookimpls = self ._hookimpls .copy ()
855
881
for method in methods :
@@ -869,23 +895,13 @@ def call_extra(
869
895
870
896
871
897
# Historical name (pluggy<=1.2), kept for backward compatibility.
872
- _HookCaller = HookCaller
898
+ _HookCaller = NormalHookCaller
873
899
874
900
875
- class _SubsetHookCaller ( HookCaller ) :
901
+ class SubsetHookCaller :
876
902
"""A proxy to another HookCaller which manages calls to all registered
877
903
plugins except the ones from remove_plugins."""
878
904
879
- # This class is unusual: in inhertits from `HookCaller` so all of
880
- # the *code* runs in the class, but it delegates all underlying *data*
881
- # to the original HookCaller.
882
- # `subset_hook_caller` used to be implemented by creating a full-fledged
883
- # HookCaller, copying all hookimpls from the original. This had problems
884
- # with memory leaks (#346) and historic calls (#347), which make a proxy
885
- # approach better.
886
- # An alternative implementation is to use a `_getattr__`/`__getattribute__`
887
- # proxy, however that adds more overhead and is more tricky to implement.
888
-
889
905
__slots__ = (
890
906
"_orig" ,
891
907
"_remove_plugins" ,
@@ -894,24 +910,56 @@ class _SubsetHookCaller(HookCaller):
894
910
def __init__ (self , orig : HookCaller , remove_plugins : Set [_Plugin ]) -> None :
895
911
self ._orig = orig
896
912
self ._remove_plugins = remove_plugins
897
- self .name = orig .name # type: ignore[misc]
898
- self ._hookexec = orig ._hookexec # type: ignore[misc]
899
913
900
- @property # type: ignore[misc]
901
- def _hookimpls (self ) -> list [HookImpl ]:
902
- return [
903
- impl
904
- for impl in self ._orig ._hookimpls
905
- if impl .plugin not in self ._remove_plugins
906
- ]
914
+ @property
915
+ def name (self ) -> str :
916
+ return self ._orig .name
907
917
908
918
@property
909
- def spec (self ) -> HookSpec | None : # type: ignore[override]
919
+ def spec (self ) -> HookSpec | None :
910
920
return self ._orig .spec
911
921
922
+ def has_spec (self ) -> bool :
923
+ return self ._orig .has_spec ()
924
+
912
925
def is_historic (self ) -> bool :
913
926
return self ._orig .is_historic ()
914
927
928
+ def get_hookimpls (self ) -> list [HookImpl ]:
929
+ """Get filtered hook implementations for this hook."""
930
+ return [
931
+ impl
932
+ for impl in self ._orig .get_hookimpls ()
933
+ if impl .plugin not in self ._remove_plugins
934
+ ]
935
+
936
+ def set_specification (
937
+ self ,
938
+ specmodule_or_class : _Namespace ,
939
+ _spec_opts_or_config : HookspecOpts | HookspecConfiguration | None = None ,
940
+ * ,
941
+ spec_opts : HookspecOpts | None = None ,
942
+ spec_config : HookspecConfiguration | None = None ,
943
+ ) -> None :
944
+ """SubsetHookCaller cannot set specifications - they are read-only proxies."""
945
+ raise RuntimeError (
946
+ f"Cannot set specification on SubsetHookCaller { self .name !r} - "
947
+ "it is a read-only proxy to another hook caller"
948
+ )
949
+
950
+ def __call__ (self , ** kwargs : object ) -> Any :
951
+ """Call the hook with filtered implementations."""
952
+ if self .is_historic ():
953
+ raise RuntimeError (
954
+ "Cannot directly call a historic hook - use call_historic instead."
955
+ )
956
+ if self .spec :
957
+ self .spec .verify_all_args_are_provided (kwargs )
958
+ firstresult = self .spec .config .firstresult if self .spec else False
959
+ # Get the hookexec from the original
960
+ hookexec = getattr (self ._orig , "_hookexec" )
961
+ return hookexec (self .name , self .get_hookimpls (), kwargs , firstresult )
962
+
915
963
def call_historic (
916
964
self ,
917
965
result_callback : Callable [[Any ], None ] | None = None ,
@@ -928,22 +976,56 @@ def call_historic(
928
976
929
977
# For subset hook callers, we need to manually handle the history and execution
930
978
kwargs = kwargs or {}
931
- self ._verify_all_args_are_provided (kwargs )
979
+ if self .spec :
980
+ self .spec .verify_all_args_are_provided (kwargs )
932
981
933
982
# If the original is a HistoricHookCaller, add to its history
934
983
if hasattr (self ._orig , "_call_history" ):
935
984
self ._orig ._call_history .append ((kwargs , result_callback ))
936
985
937
986
# Execute with filtered hookimpls
938
- res = self ._hookexec (self .name , self ._hookimpls , kwargs , False )
987
+ hookexec = getattr (self ._orig , "_hookexec" )
988
+ res = hookexec (self .name , self .get_hookimpls (), kwargs , False )
939
989
if result_callback is None :
940
990
return
941
991
if isinstance (res , list ):
942
992
for x in res :
943
993
result_callback (x )
944
994
995
+ def call_extra (
996
+ self , methods : Sequence [Callable [..., object ]], kwargs : Mapping [str , object ]
997
+ ) -> Any :
998
+ """Call the hook with some additional temporarily participating methods."""
999
+ if self .is_historic ():
1000
+ raise RuntimeError (
1001
+ "Cannot call call_extra on a historic hook - use call_historic instead."
1002
+ )
1003
+ if self .spec :
1004
+ self .spec .verify_all_args_are_provided (kwargs )
1005
+ config = HookimplConfiguration ()
1006
+ hookimpls = self .get_hookimpls ()
1007
+ for method in methods :
1008
+ hookimpl = HookImpl (None , "<temp>" , method , config )
1009
+ # Find last non-tryfirst nonwrapper method.
1010
+ i = len (hookimpls ) - 1
1011
+ while i >= 0 and (
1012
+ # Skip wrappers.
1013
+ (hookimpls [i ].hookwrapper or hookimpls [i ].wrapper )
1014
+ # Skip tryfirst nonwrappers.
1015
+ or hookimpls [i ].tryfirst
1016
+ ):
1017
+ i -= 1
1018
+ hookimpls .insert (i + 1 , hookimpl )
1019
+ firstresult = self .spec .config .firstresult if self .spec else False
1020
+ hookexec = getattr (self ._orig , "_hookexec" )
1021
+ return hookexec (self .name , hookimpls , kwargs , firstresult )
1022
+
945
1023
def __repr__ (self ) -> str :
946
- return f"<_SubsetHookCaller { self .name !r} >"
1024
+ return f"<SubsetHookCaller { self .name !r} >"
1025
+
1026
+
1027
+ # Historical name (pluggy<=1.2), kept for backward compatibility.
1028
+ _SubsetHookCaller = SubsetHookCaller
947
1029
948
1030
949
1031
@final
@@ -1028,3 +1110,22 @@ def __init__(
1028
1110
self .config = config
1029
1111
self .warn_on_impl = config .warn_on_impl
1030
1112
self .warn_on_impl_args = config .warn_on_impl_args
1113
+
1114
+ def verify_all_args_are_provided (self , kwargs : Mapping [str , object ]) -> None :
1115
+ """Verify that all required arguments are provided in kwargs."""
1116
+ # This is written to avoid expensive operations when not needed.
1117
+ for argname in self .argnames :
1118
+ if argname not in kwargs :
1119
+ notincall = ", " .join (
1120
+ repr (argname )
1121
+ for argname in self .argnames
1122
+ # Avoid self.argnames - kwargs.keys()
1123
+ # it doesn't preserve order.
1124
+ if argname not in kwargs .keys ()
1125
+ )
1126
+ warnings .warn (
1127
+ f"Argument(s) { notincall } which are declared in the hookspec "
1128
+ "cannot be found in this hook call" ,
1129
+ stacklevel = 3 , # Adjusted for delegation
1130
+ )
1131
+ break
0 commit comments