6
6
import warnings
7
7
from types import ModuleType
8
8
from typing import (
9
+ AbstractSet ,
9
10
Any ,
10
11
Callable ,
11
12
Generator ,
@@ -286,8 +287,7 @@ class _HookCaller:
286
287
"name" ,
287
288
"spec" ,
288
289
"_hookexec" ,
289
- "_wrappers" ,
290
- "_nonwrappers" ,
290
+ "_hookimpls" ,
291
291
"_call_history" ,
292
292
)
293
293
@@ -300,8 +300,7 @@ def __init__(
300
300
) -> None :
301
301
self .name : "Final" = name
302
302
self ._hookexec : "Final" = hook_execute
303
- self ._wrappers : "Final[List[HookImpl]]" = []
304
- self ._nonwrappers : "Final[List[HookImpl]]" = []
303
+ self ._hookimpls : "Final[List[HookImpl]]" = []
305
304
self ._call_history : Optional [_CallHistory ] = None
306
305
self .spec : Optional [HookSpec ] = None
307
306
if specmodule_or_class is not None :
@@ -325,38 +324,38 @@ def is_historic(self) -> bool:
325
324
return self ._call_history is not None
326
325
327
326
def _remove_plugin (self , plugin : _Plugin ) -> None :
328
- def remove (wrappers : List [HookImpl ]) -> Optional [bool ]:
329
- for i , method in enumerate (wrappers ):
330
- if method .plugin == plugin :
331
- del wrappers [i ]
332
- return True
333
- return None
334
-
335
- if remove (self ._wrappers ) is None :
336
- if remove (self ._nonwrappers ) is None :
337
- raise ValueError (f"plugin { plugin !r} not found" )
327
+ for i , method in enumerate (self ._hookimpls ):
328
+ if method .plugin == plugin :
329
+ del self ._hookimpls [i ]
330
+ return
331
+ raise ValueError (f"plugin { plugin !r} not found" )
338
332
339
333
def get_hookimpls (self ) -> List ["HookImpl" ]:
340
- # Order is important for _hookexec
341
- return self ._nonwrappers + self ._wrappers
334
+ return self ._hookimpls .copy ()
342
335
343
336
def _add_hookimpl (self , hookimpl : "HookImpl" ) -> None :
344
337
"""Add an implementation to the callback chain."""
338
+ for i , method in enumerate (self ._hookimpls ):
339
+ if method .hookwrapper :
340
+ splitpoint = i
341
+ break
342
+ else :
343
+ splitpoint = len (self ._hookimpls )
345
344
if hookimpl .hookwrapper :
346
- methods = self ._wrappers
345
+ start , end = splitpoint , len ( self ._hookimpls )
347
346
else :
348
- methods = self . _nonwrappers
347
+ start , end = 0 , splitpoint
349
348
350
349
if hookimpl .trylast :
351
- methods . insert (0 , hookimpl )
350
+ self . _hookimpls . insert (start , hookimpl )
352
351
elif hookimpl .tryfirst :
353
- methods . append ( hookimpl )
352
+ self . _hookimpls . insert ( end , hookimpl )
354
353
else :
355
354
# find last non-tryfirst method
356
- i = len ( methods ) - 1
357
- while i >= 0 and methods [i ].tryfirst :
355
+ i = end - 1
356
+ while i >= start and self . _hookimpls [i ].tryfirst :
358
357
i -= 1
359
- methods .insert (i + 1 , hookimpl )
358
+ self . _hookimpls .insert (i + 1 , hookimpl )
360
359
361
360
def __repr__ (self ) -> str :
362
361
return f"<_HookCaller { self .name !r} >"
@@ -387,7 +386,7 @@ def __call__(self, *args: object, **kwargs: object) -> Any:
387
386
), "Cannot directly call a historic hook - use call_historic instead."
388
387
self ._verify_all_args_are_provided (kwargs )
389
388
firstresult = self .spec .opts .get ("firstresult" , False ) if self .spec else False
390
- return self ._hookexec (self .name , self .get_hookimpls () , kwargs , firstresult )
389
+ return self ._hookexec (self .name , self ._hookimpls , kwargs , firstresult )
391
390
392
391
def call_historic (
393
392
self ,
@@ -406,7 +405,7 @@ def call_historic(
406
405
self ._call_history .append ((kwargs , result_callback ))
407
406
# Historizing hooks don't return results.
408
407
# Remember firstresult isn't compatible with historic.
409
- res = self ._hookexec (self .name , self .get_hookimpls () , kwargs , False )
408
+ res = self ._hookexec (self .name , self ._hookimpls , kwargs , False )
410
409
if result_callback is None :
411
410
return
412
411
if isinstance (res , list ):
@@ -429,13 +428,12 @@ def call_extra(
429
428
"tryfirst" : False ,
430
429
"specname" : None ,
431
430
}
432
- hookimpls = self .get_hookimpls ()
431
+ hookimpls = self ._hookimpls . copy ()
433
432
for method in methods :
434
433
hookimpl = HookImpl (None , "<temp>" , method , opts )
435
434
# Find last non-tryfirst nonwrapper method.
436
435
i = len (hookimpls ) - 1
437
- until = len (self ._nonwrappers )
438
- while i >= until and hookimpls [i ].tryfirst :
436
+ while i >= 0 and hookimpls [i ].tryfirst and not hookimpls [i ].hookwrapper :
439
437
i -= 1
440
438
hookimpls .insert (i + 1 , hookimpl )
441
439
firstresult = self .spec .opts .get ("firstresult" , False ) if self .spec else False
@@ -453,6 +451,53 @@ def _maybe_apply_history(self, method: "HookImpl") -> None:
453
451
result_callback (res [0 ])
454
452
455
453
454
+ class _SubsetHookCaller (_HookCaller ):
455
+ """A proxy to another HookCaller which manages calls to all registered
456
+ plugins except the ones from remove_plugins."""
457
+
458
+ # This class is unusual: in inhertits from `_HookCaller` so all of
459
+ # the *code* runs in the class, but it delegates all underlying *data*
460
+ # to the original HookCaller.
461
+ # `subset_hook_caller` used to be implemented by creating a full-fledged
462
+ # HookCaller, copying all hookimpls from the original. This had problems
463
+ # with memory leaks (#346) and historic calls (#347), which make a proxy
464
+ # approach better.
465
+ # An alternative implementation is to use a `_getattr__`/`__getattribute__`
466
+ # proxy, however that adds more overhead and is more tricky to implement.
467
+
468
+ __slots__ = (
469
+ "_orig" ,
470
+ "_remove_plugins" ,
471
+ "name" ,
472
+ "_hookexec" ,
473
+ )
474
+
475
+ def __init__ (self , orig : _HookCaller , remove_plugins : AbstractSet [_Plugin ]) -> None :
476
+ self ._orig = orig
477
+ self ._remove_plugins = remove_plugins
478
+ self .name = orig .name # type: ignore[misc]
479
+ self ._hookexec = orig ._hookexec # type: ignore[misc]
480
+
481
+ @property # type: ignore[misc]
482
+ def _hookimpls (self ) -> List ["HookImpl" ]: # type: ignore[override]
483
+ return [
484
+ impl
485
+ for impl in self ._orig ._hookimpls
486
+ if impl .plugin not in self ._remove_plugins
487
+ ]
488
+
489
+ @property
490
+ def spec (self ) -> Optional ["HookSpec" ]: # type: ignore[override]
491
+ return self ._orig .spec
492
+
493
+ @property
494
+ def _call_history (self ) -> Optional [_CallHistory ]: # type: ignore[override]
495
+ return self ._orig ._call_history
496
+
497
+ def __repr__ (self ) -> str :
498
+ return f"<_SubsetHookCaller { self .name !r} >"
499
+
500
+
456
501
class HookImpl :
457
502
__slots__ = (
458
503
"function" ,
0 commit comments