Skip to content

Commit 541cd09

Browse files
committed
Add option to switch off cache
- just in case the cache will have unwanted side-effects - also add a clear_cache method - run performance test in CI builds for reference
1 parent 6646762 commit 541cd09

File tree

6 files changed

+76
-34
lines changed

6 files changed

+76
-34
lines changed

.github/workflows/pythonpackage.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,11 @@ jobs:
6363
export PY_VERSION=${{ matrix.python-version }}
6464
$GITHUB_WORKSPACE/.github/workflows/run_pytest.sh
6565
shell: bash
66+
- name: Run performance tests
67+
run: |
68+
python -m pyfakefs.tests.performance_test SetupPerformanceTest
69+
python -m pyfakefs.tests.performance_test SetupNoCachePerformanceTest
70+
shell: bash
6671

6772
dockertests:
6873
runs-on: ubuntu-latest

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,11 @@
1212

1313
# pytest
1414
.cache/
15+
.pytest_cache/
1516

1617
# autodoc created by sphinx
1718
gh-pages/
1819

1920
# Distribution creation
2021
dist/
22+
build/

docs/usage.rst

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -498,6 +498,21 @@ search for this kind of default arguments and patch them automatically.
498498
You could also use the ``modules_to_reload`` option with the module that
499499
contains the default argument instead, if you want to avoid the overhead.
500500

501+
use_cache
502+
.........
503+
If True (default), non-patched modules are cached between tests for performance
504+
reasons. As this is a new feature, this argument allows to turn it off in case
505+
it causes any problems. Note that this parameter may be removed in a later
506+
version. If you want to clear the cache just for a specific test, you can call
507+
``clear_cache`` on the ``Patcher`` or the ``fake_filesystem`` instance:
508+
509+
.. code:: python
510+
511+
def test_something(fs):
512+
fs.clear_cache()
513+
...
514+
515+
501516
Using convenience methods
502517
-------------------------
503518
While ``pyfakefs`` can be used just with the standard Python file system

pyfakefs/fake_filesystem.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -974,6 +974,11 @@ def resume(self):
974974
'system object created by a Patcher object')
975975
self.patcher.resume()
976976

977+
def clear_cache(self):
978+
"""Clear the cache of non-patched modules."""
979+
if self.patcher:
980+
self.patcher.clear_cache()
981+
977982
def line_separator(self):
978983
return '\r\n' if self.is_windows_fs else '\n'
979984

pyfakefs/fake_filesystem_unittest.py

Lines changed: 38 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,6 @@
6666
from pyfakefs import mox3_stubout
6767
from pyfakefs.extra_packages import pathlib, pathlib2, use_scandir
6868

69-
7069
if use_scandir:
7170
from pyfakefs import fake_scandir
7271

@@ -81,7 +80,8 @@ def patchfs(_func=None, *,
8180
allow_root_user=True,
8281
use_known_patches=True,
8382
patch_open_code=PatchMode.OFF,
84-
patch_default_args=False):
83+
patch_default_args=False,
84+
use_cache=True):
8585
"""Convenience decorator to use patcher with additional parameters in a
8686
test function.
8787
@@ -106,7 +106,8 @@ def wrapped(*args, **kwargs):
106106
allow_root_user=allow_root_user,
107107
use_known_patches=use_known_patches,
108108
patch_open_code=patch_open_code,
109-
patch_default_args=False) as p:
109+
patch_default_args=patch_default_args,
110+
use_cache=use_cache) as p:
110111
args = list(args)
111112
args.append(p.fs)
112113
return f(*args, **kwargs)
@@ -213,7 +214,8 @@ def setUpPyfakefs(self,
213214
allow_root_user=True,
214215
use_known_patches=True,
215216
patch_open_code=PatchMode.OFF,
216-
patch_default_args=False):
217+
patch_default_args=False,
218+
use_cache=True):
217219
"""Bind the file-related modules to the :py:class:`pyfakefs` fake file
218220
system instead of the real file system. Also bind the fake `open()`
219221
function.
@@ -238,7 +240,8 @@ def setUpPyfakefs(self,
238240
allow_root_user=allow_root_user,
239241
use_known_patches=use_known_patches,
240242
patch_open_code=patch_open_code,
241-
patch_default_args=patch_default_args
243+
patch_default_args=patch_default_args,
244+
use_cache=use_cache
242245
)
243246

244247
self._stubber.setUp()
@@ -382,7 +385,8 @@ def __init__(self, additional_skip_names=None,
382385
modules_to_reload=None, modules_to_patch=None,
383386
allow_root_user=True, use_known_patches=True,
384387
patch_open_code=PatchMode.OFF,
385-
patch_default_args=False):
388+
patch_default_args=False,
389+
use_cache=True):
386390
"""
387391
Args:
388392
additional_skip_names: names of modules inside of which no module
@@ -406,6 +410,13 @@ def __init__(self, additional_skip_names=None,
406410
patch_open_code: If True, `io.open_code` is patched. The default
407411
is not to patch it, as it mostly is used to load compiled
408412
modules that are not in the fake file system.
413+
patch_default_args: If True, default arguments are checked for
414+
file system functions, which are patched. This check is
415+
expansive, so it is off by default.
416+
use_cache: If True (default), non-patched modules are cached
417+
between tests for performance reasons. As this is a new
418+
feature, this argument allows to turn it off in case it
419+
causes any problems.
409420
"""
410421

411422
if not allow_root_user:
@@ -434,9 +445,10 @@ def __init__(self, additional_skip_names=None,
434445
if modules_to_reload is not None:
435446
self.modules_to_reload.extend(modules_to_reload)
436447
self.patch_default_args = patch_default_args
448+
self.use_cache = use_cache
437449
if patch_default_args != self.PATCH_DEFAULT_ARGS:
438450
self.__class__.PATCH_DEFAULT_ARGS = patch_default_args
439-
self.__class__.CACHED_SKIPMODULES = set()
451+
self.clear_cache()
440452

441453
if use_known_patches:
442454
modules_to_patch = modules_to_patch or {}
@@ -450,7 +462,7 @@ def __init__(self, additional_skip_names=None,
450462
patched_module_names = set(modules_to_patch)
451463
if patched_module_names != self.PATCHED_MODULE_NAMES:
452464
self.__class__.PATCHED_MODULE_NAMES = patched_module_names
453-
self.__class__.CACHED_SKIPMODULES = set()
465+
self.clear_cache()
454466

455467
self._fake_module_functions = {}
456468
self._init_fake_module_functions()
@@ -472,6 +484,10 @@ def __init__(self, additional_skip_names=None,
472484
self._patching = False
473485
self.found_fs_module = False
474486

487+
def clear_cache(self):
488+
"""Clear the cache of non-patched modules."""
489+
self.__class__.CACHED_SKIPMODULES = set()
490+
475491
def _init_fake_module_classes(self):
476492
# IMPORTANT TESTING NOTE: Whenever you add a new module below, test
477493
# it by adding an attribute in fixtures/module_with_attributes.py
@@ -567,16 +583,6 @@ def _is_fs_module(self, mod, name, module_names):
567583
# by side effects of inspect methods
568584
return False
569585

570-
def _is_skipped_fs_module(self, mod, name, module_names):
571-
try:
572-
# check for __name__ first and ignore the AttributeException
573-
# if it does not exist - avoids calling expansive ismodule
574-
return mod.__name__ in module_names and inspect.ismodule(mod)
575-
except Exception:
576-
# handle AttributeError and any other exception possibly triggered
577-
# by side effects of inspect.ismodule
578-
return False
579-
580586
def _is_fs_function(self, fct):
581587
try:
582588
# check for __name__ first and ignore the AttributeException
@@ -623,6 +629,11 @@ def _def_values(self, item):
623629
# _DontDoThat() (see #523)
624630
pass
625631

632+
def _find_def_values(self, module_items):
633+
for _, fct in module_items:
634+
for f, i, d in self._def_values(fct):
635+
self._def_functions.append((f, i, d))
636+
626637
def _find_modules(self):
627638
"""Find and cache all modules that import file system modules.
628639
Later, `setUp()` will stub these with the fake file system
@@ -631,7 +642,7 @@ def _find_modules(self):
631642
module_names = list(self._fake_module_classes.keys()) + [PATH_MODULE]
632643
for name, module in list(sys.modules.items()):
633644
try:
634-
if (module in self.CACHED_SKIPMODULES or
645+
if (self.use_cache and module in self.CACHED_SKIPMODULES or
635646
module in self.SKIPMODULES or
636647
not inspect.ismodule(module)):
637648
continue
@@ -640,7 +651,8 @@ def _find_modules(self):
640651
# where py.error has no __name__ attribute
641652
# see https://github.com/pytest-dev/py/issues/73
642653
# and any other exception triggered by inspect.ismodule
643-
self.__class__.SKIPMODULES.add(module)
654+
if self.use_cache:
655+
self.__class__.CACHED_SKIPMODULES.add(module)
644656
continue
645657
self.found_fs_module = False
646658
skipped = (any([sn.startswith(module.__name__)
@@ -663,16 +675,15 @@ def _find_modules(self):
663675
module_items
664676
if self._is_fs_function(fct)}
665677

666-
# find default arguments that are file system functions
667-
if self.patch_default_args:
668-
for _, fct in module_items:
669-
for f, i, d in self._def_values(fct):
670-
self._def_functions.append((f, i, d))
671-
672678
for name, fct in functions.items():
673679
self._fct_modules.setdefault(
674680
(name, fct.__name__, fct.__module__), set()).add(module)
675-
if not self.found_fs_module:
681+
682+
# find default arguments that are file system functions
683+
if self.patch_default_args:
684+
self._find_def_values(module_items)
685+
686+
if not self.found_fs_module and self.use_cache:
676687
self.__class__.CACHED_SKIPMODULES.add(module)
677688

678689
def _refresh(self):

pyfakefs/tests/performance_test.py

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,21 +16,25 @@
1616
from pyfakefs.fake_filesystem_unittest import TestCase
1717

1818

19-
class PerformanceTest(TestCase):
19+
class SetupPerformanceTest(TestCase):
2020
def setUp(self) -> None:
2121
self.setUpPyfakefs()
2222

2323

24-
def test(self):
25-
path = "foo/bar"
26-
self.fs.create_file(path, contents="test")
27-
with open(path) as f:
28-
assert f.read() == "test"
24+
class SetupNoCachePerformanceTest(TestCase):
25+
def setUp(self) -> None:
26+
self.setUpPyfakefs(use_cache=False)
27+
28+
29+
def test_setup(self):
30+
pass
2931

3032

3133
for n in range(100):
3234
test_name = "test_" + str(n)
33-
setattr(PerformanceTest, test_name, test)
35+
setattr(SetupPerformanceTest, test_name, test_setup)
36+
test_name = "test_nocache" + str(n)
37+
setattr(SetupNoCachePerformanceTest, test_name, test_setup)
3438

3539
if __name__ == "__main__":
3640
unittest.main()

0 commit comments

Comments
 (0)