@@ -230,8 +230,11 @@ def call_back(self, module: ModuleType) -> None:
230230 # loader type
231231 module .register_loader_type (_ImportHookChainedLoader , module .DefaultProvider )
232232
233- for callback in self .callbacks .values ():
234- callback (module )
233+ for key , callback in self .callbacks .items ():
234+ try :
235+ callback (module )
236+ except Exception :
237+ log .exception ("Exception ignored in after_import hook %r for module %s" , key , module .__name__ )
235238
236239 def load_module (self , fullname : str ) -> t .Optional [ModuleType ]:
237240 if self .loader is None :
@@ -260,20 +263,10 @@ def _create_module(self, spec):
260263 def _find_first_hook (
261264 self , module : ModuleType , hooks_attr : str
262265 ) -> t .Optional [t .Callable [[t .Any , ModuleType ], None ]]:
263- for _ in sys .meta_path :
264- if isinstance (_ , ModuleWatchdog ):
265- try :
266- for (
267- cond ,
268- hook ,
269- ) in getattr (_ , hooks_attr , []):
270- if (isinstance (cond , str ) and cond == module .__name__ ) or (
271- callable (cond ) and cond (module .__name__ )
272- ):
273- return hook
274- except Exception :
275- log .debug ("Exception happened while processing %s" , hooks_attr , exc_info = True )
276- return None
266+ universal = _UniversalModuleWatchdog ._instance
267+ if universal is None :
268+ return None
269+ return universal ._find_first_hook (module , hooks_attr )
277270
278271 def _find_first_exception_hook (self , module : ModuleType ) -> t .Optional [t .Callable [[t .Any , ModuleType ], None ]]:
279272 return self ._find_first_hook (module , "_import_exception_hooks" )
@@ -330,15 +323,21 @@ def get_code(_loader, fullname):
330323 log .exception ("Failed to call back on module %s" , module )
331324
332325
333- class BaseModuleWatchdog ( abc . ABC ) :
334- """Base module watchdog.
326+ class _UniversalModuleWatchdog :
327+ """The single finder ever inserted into ``sys.meta_path`` for module watchdog purposes .
335328
336- Invokes ``after_import`` every time a new module is imported.
329+ Concrete ``BaseModuleWatchdog`` subclasses register their instances here as
330+ virtual participants, instead of each inserting itself into ``sys.meta_path``.
331+ This collapses what would otherwise be O(N) redundant, mutually-recursive
332+ ``find_spec`` scans (one per installed watchdog, each re-scanning the rest of
333+ ``sys.meta_path`` to find the real underlying finder) into a single O(1) lookup
334+ per import, regardless of how many watchdogs are installed.
337335 """
338336
339- _instance : t .Optional ["BaseModuleWatchdog " ] = None
337+ _instance : t .Optional ["_UniversalModuleWatchdog " ] = None
340338
341339 def __init__ (self ) -> None :
340+ self ._watchdogs : list ["BaseModuleWatchdog" ] = []
342341 self ._finding : set [str ] = set ()
343342
344343 # DEV: pkg_resources support to prevent errors such as
@@ -363,31 +362,14 @@ def __init__(self) -> None:
363362 else :
364363 log .warning ("Cannot ensure correct support with pkg_resources" )
365364
366- def _add_to_meta_path (self ) -> None :
367- sys .meta_path .insert (0 , self ) # type: ignore[arg-type]
368-
369- @classmethod
370- def _find_in_meta_path (cls ) -> t .Optional [int ]:
371- for i , meta_path in enumerate (sys .meta_path ):
372- if type (meta_path ) is cls :
373- return i
374- return None
375-
376- @classmethod
377- def _remove_from_meta_path (cls ) -> None :
378- i = cls ._find_in_meta_path ()
379-
380- if i is None :
381- log .warning ("%s is not installed" , cls .__name__ )
382- return
383-
384- sys .meta_path .pop (i )
365+ def register (self , participant : "BaseModuleWatchdog" ) -> None :
366+ self ._watchdogs .append (participant )
385367
386- def after_import (self , module : ModuleType ) -> None :
387- raise NotImplementedError ()
388-
389- def transform ( self , code : CodeType , _module : ModuleType ) -> CodeType :
390- return code
368+ def unregister (self , participant : "BaseModuleWatchdog" ) -> None :
369+ try :
370+ self . _watchdogs . remove ( participant )
371+ except ValueError :
372+ pass
391373
392374 def find_module (self , fullname : str , path : t .Optional [str ] = None ) -> t .Optional ["Loader" ]:
393375 if fullname in self ._finding :
@@ -404,8 +386,9 @@ def find_module(self, fullname: str, path: t.Optional[str] = None) -> t.Optional
404386 else original_loader
405387 )
406388
407- loader .add_callback (type (self ), self .after_import )
408- loader .add_transformer (type (self ), self .transform )
389+ for watchdog in list (self ._watchdogs ):
390+ loader .add_callback (type (watchdog ), watchdog .after_import )
391+ loader .add_transformer (type (watchdog ), watchdog .transform )
409392
410393 return t .cast ("Loader" , loader )
411394
@@ -452,21 +435,83 @@ def find_spec(
452435 if not isinstance (loader , _ImportHookChainedLoader ):
453436 spec .loader = t .cast ("Loader" , _ImportHookChainedLoader (loader , spec ))
454437
455- t .cast (_ImportHookChainedLoader , spec .loader ).add_callback (type (self ), self .after_import )
456- t .cast (_ImportHookChainedLoader , spec .loader ).add_transformer (type (self ), self .transform )
438+ for watchdog in list (self ._watchdogs ):
439+ t .cast (_ImportHookChainedLoader , spec .loader ).add_callback (type (watchdog ), watchdog .after_import )
440+ t .cast (_ImportHookChainedLoader , spec .loader ).add_transformer (type (watchdog ), watchdog .transform )
457441
458442 return spec
459443
460444 finally :
461445 self ._finding .remove (fullname )
462446
447+ def _find_first_hook (
448+ self , module : ModuleType , hooks_attr : str
449+ ) -> t .Optional [t .Callable [[t .Any , ModuleType ], None ]]:
450+ for watchdog in list (self ._watchdogs ):
451+ try :
452+ for (
453+ cond ,
454+ hook ,
455+ ) in getattr (watchdog , hooks_attr , []):
456+ if (isinstance (cond , str ) and cond == module .__name__ ) or (
457+ callable (cond ) and cond (module .__name__ )
458+ ):
459+ return hook
460+ except Exception :
461+ log .debug ("Exception happened while processing %s" , hooks_attr , exc_info = True )
462+ return None
463+
464+ @classmethod
465+ def _register (cls , participant : "BaseModuleWatchdog" ) -> None :
466+ if cls ._instance is None :
467+ cls ._instance = cls ()
468+ else :
469+ # DEV: Some other finder (eg: pytest's assertion rewriter) may have
470+ # inserted itself ahead of us in the meantime. Each individual watchdog
471+ # used to re-insert itself at position 0 on every install(), so match
472+ # that behavior by moving the shared instance back to the front on every
473+ # new registration, not just the very first one.
474+ try :
475+ sys .meta_path .remove (cls ._instance ) # type: ignore[arg-type]
476+ except ValueError :
477+ pass
478+ sys .meta_path .insert (0 , cls ._instance ) # type: ignore[arg-type]
479+ cls ._instance .register (participant )
480+
481+ @classmethod
482+ def _unregister (cls , participant : "BaseModuleWatchdog" ) -> None :
483+ if cls ._instance is None :
484+ return
485+ cls ._instance .unregister (participant )
486+ if not cls ._instance ._watchdogs :
487+ try :
488+ sys .meta_path .remove (cls ._instance ) # type: ignore[arg-type]
489+ except ValueError :
490+ log .warning ("%s is not installed" , cls .__name__ )
491+ cls ._instance = None
492+
493+
494+ class BaseModuleWatchdog (abc .ABC ):
495+ """Base module watchdog.
496+
497+ Invokes ``after_import`` every time a new module is imported.
498+ """
499+
500+ _instance : t .Optional ["BaseModuleWatchdog" ] = None
501+
502+ def after_import (self , module : ModuleType ) -> None :
503+ raise NotImplementedError ()
504+
505+ def transform (self , code : CodeType , _module : ModuleType ) -> CodeType :
506+ return code
507+
463508 @classmethod
464509 def install (cls ) -> None :
465510 """Install the module watchdog."""
466511 if cls .is_installed ():
467512 return
468513 cls ._instance = cls ()
469- cls ._instance . _add_to_meta_path ( )
514+ _UniversalModuleWatchdog . _register ( cls ._instance )
470515 log .debug ("%s installed" , cls )
471516
472517 @classmethod
@@ -484,7 +529,7 @@ def uninstall(cls) -> None:
484529 if not cls .is_installed ():
485530 return
486531
487- cls . _remove_from_meta_path ( )
532+ _UniversalModuleWatchdog . _unregister ( t . cast ( "BaseModuleWatchdog" , cls . _instance ) )
488533
489534 cls ._instance = None
490535
0 commit comments