@@ -362,6 +362,9 @@ class Patcher:
362362 None , fake_filesystem , fake_filesystem_shutil ,
363363 sys , linecache , tokenize
364364 }
365+ # caches all modules that do not have file system modules or function
366+ # to speed up _find_modules
367+ CACHED_SKIPMODULES = set ()
365368
366369 assert None in SKIPMODULES , ("sys.modules contains 'None' values;"
367370 " must skip them." )
@@ -370,6 +373,11 @@ class Patcher:
370373
371374 SKIPNAMES = {'os' , 'path' , 'io' , 'genericpath' , OS_MODULE , PATH_MODULE }
372375
376+ # hold values from last call - if changed, the cache in
377+ # CACHED_SKIPMODULES has to be invalidated
378+ PATCHED_MODULE_NAMES = {}
379+ PATCH_DEFAULT_ARGS = False
380+
373381 def __init__ (self , additional_skip_names = None ,
374382 modules_to_reload = None , modules_to_patch = None ,
375383 allow_root_user = True , use_known_patches = True ,
@@ -426,6 +434,9 @@ def __init__(self, additional_skip_names=None,
426434 if modules_to_reload is not None :
427435 self .modules_to_reload .extend (modules_to_reload )
428436 self .patch_default_args = patch_default_args
437+ if patch_default_args != self .PATCH_DEFAULT_ARGS :
438+ self .__class__ .PATCH_DEFAULT_ARGS = patch_default_args
439+ self .__class__ .CACHED_SKIPMODULES = set ()
429440
430441 if use_known_patches :
431442 modules_to_patch = modules_to_patch or {}
@@ -436,6 +447,10 @@ def __init__(self, additional_skip_names=None,
436447 if modules_to_patch is not None :
437448 for name , fake_module in modules_to_patch .items ():
438449 self ._fake_module_classes [name ] = fake_module
450+ patched_module_names = set (modules_to_patch )
451+ if patched_module_names != self .PATCHED_MODULE_NAMES :
452+ self .__class__ .PATCHED_MODULE_NAMES = patched_module_names
453+ self .__class__ .CACHED_SKIPMODULES = set ()
439454
440455 self ._fake_module_functions = {}
441456 self ._init_fake_module_functions ()
@@ -455,6 +470,7 @@ def __init__(self, additional_skip_names=None,
455470 # _isStale is set by tearDown(), reset by _refresh()
456471 self ._isStale = True
457472 self ._patching = False
473+ self .found_fs_module = False
458474
459475 def _init_fake_module_classes (self ):
460476 # IMPORTANT TESTING NOTE: Whenever you add a new module below, test
@@ -532,38 +548,50 @@ def __exit__(self, exc_type, exc_val, exc_tb):
532548
533549 def _is_fs_module (self , mod , name , module_names ):
534550 try :
551+ # check for __name__ first and ignore the AttributeException
552+ # if it does not exist - avoids calling expansive ismodule
535553 if mod .__name__ in module_names and inspect .ismodule (mod ):
554+ self .found_fs_module = True
536555 return True
537556 except Exception :
538557 pass
539558 try :
540559 if (name in self ._class_modules and
541560 mod .__module__ in self ._class_modules [name ]):
542- return inspect .isclass (mod )
561+ if inspect .isclass (mod ):
562+ self .found_fs_module = True
563+ return True
564+ return False
543565 except Exception :
544- # handle cases where the class has no __module__
545- # attribute - see #460, and any other exception triggered
546- # by inspect functions
566+ # handle AttributeError and any other exception possibly triggered
567+ # by side effects of inspect methods
547568 return False
548569
549570 def _is_skipped_fs_module (self , mod , name , module_names ):
550571 try :
572+ # check for __name__ first and ignore the AttributeException
573+ # if it does not exist - avoids calling expansive ismodule
551574 return mod .__name__ in module_names and inspect .ismodule (mod )
552575 except Exception :
553- # handle cases where the module has no __name__ or __module__
554- # attribute - see #460, and any other exception triggered
555- # by inspect functions
576+ # handle AttributeError and any other exception possibly triggered
577+ # by side effects of inspect.ismodule
556578 return False
557579
558580 def _is_fs_function (self , fct ):
559581 try :
560- return (fct .__name__ in self ._fake_module_functions and
582+ # check for __name__ first and ignore the AttributeException
583+ # if it does not exist - avoids calling expansive inspect
584+ # methods in most cases
585+ if (fct .__name__ in self ._fake_module_functions and
561586 fct .__module__ in self ._fake_module_functions [
562587 fct .__name__ ] and
563- (inspect .isfunction (fct ) or inspect .isbuiltin (fct )))
588+ (inspect .isfunction (fct ) or inspect .isbuiltin (fct ))):
589+ self .found_fs_module = True
590+ return True
591+ return False
564592 except Exception :
565- # handle cases where the function has no __name__ or __module__
566- # attribute, or any other exception in inspect functions
593+ # handle AttributeError and any other exception possibly triggered
594+ # by side effects of inspect methods
567595 return False
568596
569597 def _def_values (self , item ):
@@ -581,6 +609,7 @@ def _def_values(self, item):
581609 if inspect .isclass (item ):
582610 # check for methods in class
583611 # (nested classes are ignored for now)
612+ # inspect.getmembers is very expansive!
584613 for m in inspect .getmembers (item ,
585614 predicate = inspect .isfunction ):
586615 m = m [1 ]
@@ -602,14 +631,18 @@ def _find_modules(self):
602631 module_names = list (self ._fake_module_classes .keys ()) + [PATH_MODULE ]
603632 for name , module in list (sys .modules .items ()):
604633 try :
605- if module in self .SKIPMODULES or not inspect .ismodule (module ):
634+ if (module in self .CACHED_SKIPMODULES or
635+ module in self .SKIPMODULES or
636+ not inspect .ismodule (module )):
606637 continue
607638 except Exception :
608639 # workaround for some py (part of pytest) versions
609640 # where py.error has no __name__ attribute
610641 # see https://github.com/pytest-dev/py/issues/73
611642 # and any other exception triggered by inspect.ismodule
643+ self .__class__ .SKIPMODULES .add (module )
612644 continue
645+ self .found_fs_module = False
613646 skipped = (any ([sn .startswith (module .__name__ )
614647 for sn in self ._skip_names ]))
615648 module_items = module .__dict__ .copy ().items ()
@@ -639,6 +672,8 @@ def _find_modules(self):
639672 for name , fct in functions .items ():
640673 self ._fct_modules .setdefault (
641674 (name , fct .__name__ , fct .__module__ ), set ()).add (module )
675+ if not self .found_fs_module :
676+ self .__class__ .CACHED_SKIPMODULES .add (module )
642677
643678 def _refresh (self ):
644679 """Renew the fake file system and set the _isStale flag to `False`."""
0 commit comments