Skip to content

Commit 3190c7e

Browse files
committed
Add caching for modules without file system functions
- avoids to parse these modules in each test - halves setup times in local tests
1 parent 9e6317b commit 3190c7e

File tree

2 files changed

+48
-13
lines changed

2 files changed

+48
-13
lines changed

CHANGES.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ The released versions correspond to PyPi releases.
1313
default to avoid a large performance impact. An additional parameter
1414
`patch_default_args` has been added that switches this behavior on
1515
(see [#567](../../issues/567)).
16-
* Some setup performance improvements have been added
16+
* Added some performance improvements in the test setup
1717

1818
## [Version 4.2.1](https://pypi.python.org/pypi/pyfakefs/4.2.1) (2020-11-02)
1919

pyfakefs/fake_filesystem_unittest.py

Lines changed: 47 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)