@@ -75,6 +75,62 @@ class HookimplOpts(TypedDict):
75
75
specname : str | None
76
76
77
77
78
+ @final
79
+ class HookspecConfiguration :
80
+ """Configuration class for hook specifications.
81
+
82
+ This class is intended to replace HookspecOpts in future versions.
83
+ It provides a more structured and extensible way to configure hook specifications.
84
+ """
85
+
86
+ __slots__ = (
87
+ "firstresult" ,
88
+ "historic" ,
89
+ "warn_on_impl" ,
90
+ "warn_on_impl_args" ,
91
+ )
92
+
93
+ def __init__ (
94
+ self ,
95
+ firstresult : bool = False ,
96
+ historic : bool = False ,
97
+ warn_on_impl : Warning | None = None ,
98
+ warn_on_impl_args : Mapping [str , Warning ] | None = None ,
99
+ ) -> None :
100
+ """Initialize hook specification configuration.
101
+
102
+ :param firstresult:
103
+ Whether the hook is :ref:`first result only <firstresult>`.
104
+ :param historic:
105
+ Whether the hook is :ref:`historic <historic>`.
106
+ :param warn_on_impl:
107
+ Whether the hook :ref:`warns when implemented <warn_on_impl>`.
108
+ :param warn_on_impl_args:
109
+ Whether the hook warns when :ref:`certain arguments are requested
110
+ <warn_on_impl>`.
111
+ """
112
+ if historic and firstresult :
113
+ raise ValueError ("cannot have a historic firstresult hook" )
114
+ #: Whether the hook is :ref:`first result only <firstresult>`.
115
+ self .firstresult : Final = firstresult
116
+ #: Whether the hook is :ref:`historic <historic>`.
117
+ self .historic : Final = historic
118
+ #: Whether the hook :ref:`warns when implemented <warn_on_impl>`.
119
+ self .warn_on_impl : Final = warn_on_impl
120
+ #: Whether the hook warns when :ref:`certain arguments are requested
121
+ #: <warn_on_impl>`.
122
+ self .warn_on_impl_args : Final = warn_on_impl_args
123
+
124
+ def __repr__ (self ) -> str :
125
+ attrs = []
126
+ for slot in self .__slots__ :
127
+ value = getattr (self , slot )
128
+ if value :
129
+ attrs .append (f"{ slot } ={ value !r} " )
130
+ attrs_str = ", " .join (attrs )
131
+ return f"HookspecConfiguration({ attrs_str } )"
132
+
133
+
78
134
@final
79
135
class HookimplConfiguration :
80
136
"""Configuration class for hook implementations.
@@ -226,22 +282,31 @@ def __call__( # noqa: F811
226
282
"""
227
283
228
284
def setattr_hookspec_opts (func : _F ) -> _F :
229
- if historic and firstresult :
230
- raise ValueError ("cannot have a historic firstresult hook" )
231
- opts : HookspecOpts = {
232
- "firstresult" : firstresult ,
233
- "historic" : historic ,
234
- "warn_on_impl" : warn_on_impl ,
235
- "warn_on_impl_args" : warn_on_impl_args ,
236
- }
237
- setattr (func , self .project_name + "_spec" , opts )
285
+ config = HookspecConfiguration (
286
+ firstresult = firstresult ,
287
+ historic = historic ,
288
+ warn_on_impl = warn_on_impl ,
289
+ warn_on_impl_args = warn_on_impl_args ,
290
+ )
291
+ setattr (func , self .project_name + "_spec" , config )
238
292
return func
239
293
240
294
if function is not None :
241
295
return setattr_hookspec_opts (function )
242
296
else :
243
297
return setattr_hookspec_opts
244
298
299
+ def _get_hookconfig (self , func : Callable [..., object ]) -> HookspecConfiguration :
300
+ """Extract hook specification configuration from a decorated function.
301
+
302
+ :param func: A function decorated with this HookspecMarker
303
+ :return: HookspecConfiguration object containing the hook specification options
304
+ :raises AttributeError: If the function is not decorated with this marker
305
+ """
306
+ attr_name = self .project_name + "_spec"
307
+ config : HookspecConfiguration = getattr (func , attr_name )
308
+ return config
309
+
245
310
246
311
@final
247
312
class HookimplMarker :
@@ -486,7 +551,7 @@ def __init__(
486
551
name : str ,
487
552
hook_execute : _HookExec ,
488
553
specmodule_or_class : _Namespace | None = None ,
489
- spec_opts : HookspecOpts | None = None ,
554
+ spec_config : HookspecConfiguration | None = None ,
490
555
) -> None :
491
556
""":meta private:"""
492
557
#: Name of the hook getting called.
@@ -504,8 +569,8 @@ def __init__(
504
569
# TODO: Document, or make private.
505
570
self .spec : HookSpec | None = None
506
571
if specmodule_or_class is not None :
507
- assert spec_opts is not None
508
- self .set_specification (specmodule_or_class , spec_opts )
572
+ assert spec_config is not None
573
+ self .set_specification (specmodule_or_class , spec_config = spec_config )
509
574
510
575
# TODO: Document, or make private.
511
576
def has_spec (self ) -> bool :
@@ -515,15 +580,39 @@ def has_spec(self) -> bool:
515
580
def set_specification (
516
581
self ,
517
582
specmodule_or_class : _Namespace ,
518
- spec_opts : HookspecOpts ,
583
+ _spec_opts_or_config : HookspecOpts | HookspecConfiguration | None = None ,
584
+ * ,
585
+ spec_opts : HookspecOpts | None = None ,
586
+ spec_config : HookspecConfiguration | None = None ,
519
587
) -> None :
520
588
if self .spec is not None :
521
589
raise ValueError (
522
590
f"Hook { self .spec .name !r} is already registered "
523
591
f"within namespace { self .spec .namespace } "
524
592
)
525
- self .spec = HookSpec (specmodule_or_class , self .name , spec_opts )
526
- if spec_opts .get ("historic" ):
593
+
594
+ # Handle the dual parameter - set the appropriate typed parameter
595
+ if _spec_opts_or_config is not None :
596
+ assert spec_opts is None and spec_config is None , (
597
+ "Cannot provide both positional and keyword spec arguments"
598
+ )
599
+
600
+ if isinstance (_spec_opts_or_config , dict ):
601
+ spec_opts = _spec_opts_or_config
602
+ else :
603
+ spec_config = _spec_opts_or_config
604
+
605
+ # Require exactly one of the typed parameters to be set
606
+ if spec_opts is not None :
607
+ assert spec_config is None , "Cannot provide both spec_opts and spec_config"
608
+ final_config = HookspecConfiguration (** spec_opts )
609
+ elif spec_config is not None :
610
+ final_config = spec_config
611
+ else :
612
+ raise TypeError ("Must provide either spec_opts or spec_config" )
613
+
614
+ self .spec = HookSpec (specmodule_or_class , self .name , final_config )
615
+ if final_config .historic :
527
616
self ._call_history = []
528
617
529
618
def is_historic (self ) -> bool :
@@ -600,7 +689,7 @@ def __call__(self, **kwargs: object) -> Any:
600
689
"Cannot directly call a historic hook - use call_historic instead."
601
690
)
602
691
self ._verify_all_args_are_provided (kwargs )
603
- firstresult = self .spec .opts . get ( " firstresult" , False ) if self .spec else False
692
+ firstresult = self .spec .config . firstresult if self .spec else False
604
693
# Copy because plugins may register other plugins during iteration (#438).
605
694
return self ._hookexec (self .name , self ._hookimpls .copy (), kwargs , firstresult )
606
695
@@ -655,7 +744,7 @@ def call_extra(
655
744
):
656
745
i -= 1
657
746
hookimpls .insert (i + 1 , hookimpl )
658
- firstresult = self .spec .opts . get ( " firstresult" , False ) if self .spec else False
747
+ firstresult = self .spec .config . firstresult if self .spec else False
659
748
return self ._hookexec (self .name , hookimpls , kwargs , firstresult )
660
749
661
750
def _maybe_apply_history (self , method : HookImpl ) -> None :
@@ -786,16 +875,18 @@ class HookSpec:
786
875
"name" ,
787
876
"argnames" ,
788
877
"kwargnames" ,
789
- "opts " ,
878
+ "config " ,
790
879
"warn_on_impl" ,
791
880
"warn_on_impl_args" ,
792
881
)
793
882
794
- def __init__ (self , namespace : _Namespace , name : str , opts : HookspecOpts ) -> None :
883
+ def __init__ (
884
+ self , namespace : _Namespace , name : str , config : HookspecConfiguration
885
+ ) -> None :
795
886
self .namespace = namespace
796
887
self .function : Callable [..., object ] = getattr (namespace , name )
797
888
self .name = name
798
889
self .argnames , self .kwargnames = varnames (self .function )
799
- self .opts = opts
800
- self .warn_on_impl = opts . get ( " warn_on_impl" )
801
- self .warn_on_impl_args = opts . get ( " warn_on_impl_args" )
890
+ self .config = config
891
+ self .warn_on_impl = config . warn_on_impl
892
+ self .warn_on_impl_args = config . warn_on_impl_args
0 commit comments