11from __future__ import annotations
22
3+ from fnmatch import fnmatchcase
34from functools import wraps
45from typing import TYPE_CHECKING , Annotated , Any , Literal , TypeAlias , get_args
56
2728VALID_SCAN_HOOKS = set (get_args (ScanHookName ))
2829
2930
31+ def _matches_scan_name (scan_name : str | None , patterns : list [str ] | None ) -> bool :
32+ if not patterns :
33+ return True
34+ if scan_name is None :
35+ return False
36+ return any (fnmatchcase (scan_name , pattern ) for pattern in patterns )
37+
38+
39+ def _get_hook_method_name (
40+ hook_name : str ,
41+ hook_info : dict [str , str | dict [str , str | list [str ]] | list [str | dict [str , str | list [str ]]]],
42+ hook_type : str ,
43+ scan_name : str | None ,
44+ ) -> str | None :
45+ hook_config = hook_info .get (hook_type )
46+ if hook_config is None :
47+ return None
48+ if isinstance (hook_config , list ):
49+ matched_method_names = []
50+ for config in hook_config :
51+ if isinstance (config , str ):
52+ matched_method_names .append (config )
53+ continue
54+ if _matches_scan_name (scan_name , config .get ("scan_names" )):
55+ matched_method_names .append (config ["method_name" ])
56+ if len (matched_method_names ) > 1 :
57+ raise ValueError (
58+ f"Multiple scan modifier implementations matched hook '{ hook_name } ' "
59+ f"for lifecycle '{ hook_type } ' and scan '{ scan_name } '"
60+ )
61+ return matched_method_names [0 ] if matched_method_names else None
62+ if isinstance (hook_config , str ):
63+ return hook_config
64+ if not _matches_scan_name (scan_name , hook_config .get ("scan_names" )):
65+ return None
66+ return hook_config ["method_name" ]
67+
68+
3069def scan_hook (func ):
3170 """
3271 Decorator for scan hooks. It registers the decorated method as a scan hook and thus allows
@@ -46,62 +85,82 @@ def wrapper(self, *args, **kwargs):
4685 return func (self , * args , ** kwargs )
4786
4887 hook_info = self ._scan_modifier_hooks [func .__name__ ]
49- if "before" in hook_info :
50- before_method = getattr (self ._scan_modifier , hook_info ["before" ])
88+ scan_name = getattr (
89+ getattr (self , "scan_info" , None ), "scan_name" , getattr (self , "scan_name" , None )
90+ )
91+
92+ before_method_name = _get_hook_method_name (func .__name__ , hook_info , "before" , scan_name )
93+ if before_method_name is not None :
94+ before_method = getattr (self ._scan_modifier , before_method_name )
5195 before_method (* args , ** kwargs )
5296
53- if "replace" in hook_info :
54- replace_method = getattr (self ._scan_modifier , hook_info ["replace" ])
97+ replace_method_name = _get_hook_method_name (func .__name__ , hook_info , "replace" , scan_name )
98+ if replace_method_name is not None :
99+ replace_method = getattr (self ._scan_modifier , replace_method_name )
55100 replace_method (* args , ** kwargs )
56101 else :
57102 func (self , * args , ** kwargs )
58103
59- if "after" in hook_info :
60- after_method = getattr (self ._scan_modifier , hook_info ["after" ])
104+ after_method_name = _get_hook_method_name (func .__name__ , hook_info , "after" , scan_name )
105+ if after_method_name is not None :
106+ after_method = getattr (self ._scan_modifier , after_method_name )
61107 after_method (* args , ** kwargs )
62108
63109 return
64110
65111 # pylint: disable=protected-access
66112 wrapper ._scan_hook_info = {"method_name" : func .__name__ } # type: ignore
113+ wrapper ._scan_hook_original = func # type: ignore[attr-defined]
67114
68115 return wrapper
69116
70117
71118def scan_hook_impl (
72- hook_name : ScanHookName , hook_type : Literal ["before" , "after" , "replace" ] = "before"
119+ hook_name : ScanHookName ,
120+ hook_type : Literal ["before" , "after" , "replace" ] = "before" ,
121+ scan_names : list [str ] | None = None ,
73122):
74123 """
75124 Decorator for scan hook implementations. It registers the decorated method as an implementation of the specified scan hook.
76125 The hook_name must refer to an existing scan hook.
77126 The hook_type should be one of the following: "before", "after" or "replace".
127+ The optional scan_names list can be used to restrict the implementation to matching scan names.
128+ Wildcards are supported using shell-style patterns such as ``*_line_scan``.
78129 This allows the scan modifier to specify whether the decorated method should be executed before, after or instead of the original scan hook method.
79130 """
80131 if hook_name not in VALID_SCAN_HOOKS :
81132 raise ValueError (f"Invalid scan hook: { hook_name } " )
82133 if hook_type not in {"before" , "after" , "replace" }:
83134 raise ValueError (f"Invalid scan hook type: { hook_type } " )
135+ if scan_names is not None and not isinstance (scan_names , list ):
136+ raise ValueError ("scan_names must be a list of scan name patterns" )
84137
85138 def decorator (func ):
86139 @wraps (func )
87140 def wrapper (self , * args , ** kwargs ):
88141 return func (self , * args , ** kwargs )
89142
90143 # pylint: disable=protected-access
91- wrapper ._scan_hook_impl_info = {"hook_name" : hook_name , "hook_type" : hook_type } # type: ignore
144+ wrapper ._scan_hook_impl_info = {
145+ "hook_name" : hook_name ,
146+ "hook_type" : hook_type ,
147+ "scan_names" : scan_names ,
148+ } # type: ignore
92149
93150 return wrapper
94151
95152 return decorator
96153
97154
98- def get_scan_hooks_impl (cls ) -> dict [str , dict [str , str ]]:
155+ def get_scan_hooks_impl (
156+ cls ,
157+ ) -> dict [
158+ str , dict [str , str | dict [str , str | list [str ]] | list [str | dict [str , str | list [str ]]]]
159+ ]:
99160 """
100161 Get the scan hooks implemented by the given class. It returns
101162 a dictionary mapping the original scan hook names to the corresponding method names and hook types in the scan modifier.
102163
103- Raises:
104- ValueError: If the class implements multiple hooks for the same hook_type (before, after, replace) for the same scan hook.
105164 """
106165 hooks = {}
107166 for attr_name in dir (cls ):
@@ -112,11 +171,19 @@ def get_scan_hooks_impl(cls) -> dict[str, dict[str, str]]:
112171 hook_type = info ["hook_type" ]
113172 if hook_name not in hooks :
114173 hooks [hook_name ] = {}
115- if hook_type in hooks [hook_name ]:
116- raise ValueError (
117- f"Multiple implementations for the same hook type '{ hook_type } ' for the scan hook '{ hook_name } ' in class '{ cls .__name__ } '"
118- )
119- hooks [hook_name ][hook_type ] = attr_name
174+ scan_names = info .get ("scan_names" )
175+ hook_config : str | dict [str , str | list [str ]]
176+ if scan_names is None :
177+ hook_config = attr_name
178+ else :
179+ hook_config = {"method_name" : attr_name , "scan_names" : scan_names }
180+ existing_hook_config = hooks [hook_name ].get (hook_type )
181+ if existing_hook_config is None :
182+ hooks [hook_name ][hook_type ] = hook_config
183+ elif isinstance (existing_hook_config , list ):
184+ existing_hook_config .append (hook_config )
185+ else :
186+ hooks [hook_name ][hook_type ] = [existing_hook_config , hook_config ]
120187 return hooks
121188
122189
@@ -225,3 +292,27 @@ def device_is_available(self, device: list[str] | str, check_enabled: bool = Tru
225292 if check_enabled and not self .dev [dev_name ].enabled :
226293 return False
227294 return True
295+
296+ def call_original (self , hook_name : ScanHookName , * args , ** kwargs ):
297+ """
298+ Call the scan's original hook implementation directly, bypassing scan modifier dispatch.
299+
300+ Args:
301+ hook_name (ScanHookName): Name of the original scan hook to call.
302+ *args: Positional arguments forwarded to the original hook.
303+ **kwargs: Keyword arguments forwarded to the original hook.
304+
305+ Returns:
306+ Any: The return value of the original hook implementation.
307+
308+ Raises:
309+ AttributeError: If the scan does not expose an original implementation for the hook.
310+ """
311+ original_hooks = getattr (self .scan , "_scan_original_hooks" , {})
312+ try :
313+ original_hook = original_hooks [hook_name ]
314+ except KeyError as exc :
315+ raise AttributeError (
316+ f"Scan { type (self .scan ).__name__ !r} does not expose an original hook for { hook_name !r} "
317+ ) from exc
318+ return original_hook (* args , ** kwargs )
0 commit comments