@@ -155,47 +155,80 @@ 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 (
174
+ self , plugin : _Plugin , name : str
175
+ ) -> HookimplConfiguration | None :
176
+ """Internal method to parse hook implementation configuration.
177
+
178
+ This method uses the new HookimplConfiguration type internally.
179
+ Falls back to the legacy parse_hookimpl_opts method for compatibility.
180
+
181
+ :param plugin: The plugin object to inspect
182
+ :param name: The attribute name to check for hook implementation
183
+ :returns: HookimplConfiguration if found, None otherwise
184
+ """
185
+ try :
186
+ method : object = getattr (plugin , name )
187
+ except Exception : # pragma: no cover
188
+ return None
189
+
190
+ if not inspect .isroutine (method ):
191
+ return None
192
+
193
+ try :
194
+ # Try to get hook implementation configuration directly
195
+ impl_attr = getattr (method , self .project_name + "_impl" , None )
196
+ except Exception : # pragma: no cover
197
+ impl_attr = None
198
+
199
+ if impl_attr is not None :
200
+ # Check if it's already a HookimplConfiguration (new style)
201
+ if isinstance (impl_attr , HookimplConfiguration ):
202
+ return impl_attr
203
+ # Handle legacy dict-based configuration
204
+ elif isinstance (impl_attr , dict ):
205
+ return HookimplConfiguration .from_opts (impl_attr ) # type: ignore
206
+
207
+ # Fall back to legacy parse_hookimpl_opts for compatibility
208
+ # (e.g. pytest override)
209
+ legacy_opts = self .parse_hookimpl_opts (plugin , name )
210
+ if legacy_opts is not None :
211
+ normalize_hookimpl_opts (legacy_opts )
212
+ return HookimplConfiguration .from_opts (legacy_opts )
213
+
214
+ return None
215
+
175
216
def parse_hookimpl_opts (self , plugin : _Plugin , name : str ) -> HookimplOpts | None :
176
217
"""Try to obtain a hook implementation from an item with the given name
177
218
in the given plugin which is being searched for hook impls.
178
219
179
220
:returns:
180
221
The parsed hookimpl options, or None to skip the given item.
181
222
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`.
223
+ .. deprecated::
224
+ Customizing hook implementation parsing by overriding this method is
225
+ deprecated. This method is only kept as a compatibility shim for
226
+ legacy projects like pytest. New code should use the standard
227
+ :class:`HookimplMarker` decorators.
185
228
"""
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
229
+ # Compatibility shim - only overridden by legacy projects like pytest
230
+ # Modern hook implementations are handled by _parse_hookimpl
231
+ return None
199
232
200
233
def unregister (
201
234
self , plugin : _Plugin | None = None , name : str | None = None
0 commit comments