@@ -155,47 +155,77 @@ def register(self, plugin: _Plugin, name: str | None = None) -> str | None:
155
155
156
156
# register matching hook implementations of the plugin
157
157
for name in dir (plugin ):
158
- hookimpl_opts = self .parse_hookimpl_opts (plugin , name )
159
- if hookimpl_opts is not None :
160
- normalize_hookimpl_opts (hookimpl_opts )
158
+ hookimpl_config = self ._parse_hookimpl (plugin , name )
159
+ if hookimpl_config is not None :
161
160
method : _HookImplFunction [object ] = getattr (plugin , name )
162
- hookimpl_config = HookimplConfiguration .from_opts (hookimpl_opts )
163
161
hookimpl = HookImpl (plugin , plugin_name , method , hookimpl_config )
164
- name = hookimpl_opts . get ( " specname" ) or name
165
- hook : HookCaller | None = getattr (self .hook , name , None )
162
+ hook_name = hookimpl_config . specname or name
163
+ hook : HookCaller | None = getattr (self .hook , hook_name , None )
166
164
if hook is None :
167
- hook = HookCaller (name , self ._hookexec )
168
- setattr (self .hook , name , hook )
165
+ hook = HookCaller (hook_name , self ._hookexec )
166
+ setattr (self .hook , hook_name , hook )
169
167
elif hook .has_spec ():
170
168
self ._verify_hook (hook , hookimpl )
171
169
hook ._maybe_apply_history (hookimpl )
172
170
hook ._add_hookimpl (hookimpl )
173
171
return plugin_name
174
172
173
+ def _parse_hookimpl (self , plugin : _Plugin , name : str ) -> HookimplConfiguration | None :
174
+ """Internal method to parse hook implementation configuration.
175
+
176
+ This method uses the new HookimplConfiguration type internally.
177
+ Falls back to the legacy parse_hookimpl_opts method for compatibility.
178
+
179
+ :param plugin: The plugin object to inspect
180
+ :param name: The attribute name to check for hook implementation
181
+ :returns: HookimplConfiguration if found, None otherwise
182
+ """
183
+ try :
184
+ method : object = getattr (plugin , name )
185
+ except Exception : # pragma: no cover
186
+ return None
187
+
188
+ if not inspect .isroutine (method ):
189
+ return None
190
+
191
+ try :
192
+ # Try to get hook implementation configuration directly
193
+ impl_attr = getattr (method , self .project_name + "_impl" , None )
194
+ except Exception : # pragma: no cover
195
+ impl_attr = None
196
+
197
+ if impl_attr is not None :
198
+ # Check if it's already a HookimplConfiguration (new style)
199
+ if isinstance (impl_attr , HookimplConfiguration ):
200
+ return impl_attr
201
+ # Handle legacy dict-based configuration
202
+ elif isinstance (impl_attr , dict ):
203
+ return HookimplConfiguration .from_opts (impl_attr )
204
+
205
+ # Fall back to legacy parse_hookimpl_opts for compatibility (e.g. pytest override)
206
+ legacy_opts = self .parse_hookimpl_opts (plugin , name )
207
+ if legacy_opts is not None :
208
+ normalize_hookimpl_opts (legacy_opts )
209
+ return HookimplConfiguration .from_opts (legacy_opts )
210
+
211
+ return None
212
+
175
213
def parse_hookimpl_opts (self , plugin : _Plugin , name : str ) -> HookimplOpts | None :
176
214
"""Try to obtain a hook implementation from an item with the given name
177
215
in the given plugin which is being searched for hook impls.
178
216
179
217
:returns:
180
218
The parsed hookimpl options, or None to skip the given item.
181
219
182
- This method can be overridden by ``PluginManager`` subclasses to
183
- customize how hook implementation are picked up. By default, returns the
184
- options for items decorated with :class:`HookimplMarker`.
220
+ .. deprecated::
221
+ Customizing hook implementation parsing by overriding this method is
222
+ deprecated. This method is only kept as a compatibility shim for
223
+ legacy projects like pytest. New code should use the standard
224
+ :class:`HookimplMarker` decorators.
185
225
"""
186
- method : object = getattr (plugin , name )
187
- if not inspect .isroutine (method ):
188
- return None
189
- try :
190
- res : HookimplOpts | None = getattr (
191
- method , self .project_name + "_impl" , None
192
- )
193
- except Exception : # pragma: no cover
194
- res = {} # type: ignore[assignment] #pragma: no cover
195
- if res is not None and not isinstance (res , dict ):
196
- # false positive
197
- res = None # type:ignore[unreachable] #pragma: no cover
198
- return res
226
+ # Compatibility shim - only overridden by legacy projects like pytest
227
+ # Modern hook implementations are handled by _parse_hookimpl
228
+ return None
199
229
200
230
def unregister (
201
231
self , plugin : _Plugin | None = None , name : str | None = None
0 commit comments