• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright 2014 Altera Corporation. All Rights Reserved.
2# Copyright 2015-2017 John McGehee
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8#      http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15
16"""This module provides a base class derived from `unittest.TestClass`
17for unit tests using the :py:class:`pyfakefs` module.
18
19`fake_filesystem_unittest.TestCase` searches `sys.modules` for modules
20that import the `os`, `io`, `path` `shutil`, and `pathlib` modules.
21
22The `setUpPyfakefs()` method binds these modules to the corresponding fake
23modules from `pyfakefs`.  Further, the `open()` built-in is bound to a fake
24`open()`.
25
26It is expected that `setUpPyfakefs()` be invoked at the beginning of the
27derived class' `setUp()` method.  There is no need to add anything to the
28derived class' `tearDown()` method.
29
30During the test, everything uses the fake file system and modules.  This means
31that even in your test fixture, familiar functions like `open()` and
32`os.makedirs()` manipulate the fake file system.
33
34Existing unit tests that use the real file system can be retrofitted to use
35pyfakefs by simply changing their base class from `:py:class`unittest.TestCase`
36to `:py:class`pyfakefs.fake_filesystem_unittest.TestCase`.
37"""
38import doctest
39import functools
40import inspect
41import shutil
42import sys
43import tempfile
44import unittest
45import warnings
46
47from pyfakefs.deprecator import Deprecator
48from pyfakefs.fake_filesystem import set_uid, set_gid, reset_ids
49from pyfakefs.helpers import IS_PYPY
50
51try:
52    from importlib.machinery import ModuleSpec
53except ImportError:
54    ModuleSpec = object
55
56from importlib import reload
57
58from pyfakefs import fake_filesystem
59from pyfakefs import fake_filesystem_shutil
60from pyfakefs import mox3_stubout
61from pyfakefs.extra_packages import pathlib, pathlib2, use_scandir
62
63if pathlib:
64    from pyfakefs import fake_pathlib
65
66if use_scandir:
67    from pyfakefs import fake_scandir
68
69OS_MODULE = 'nt' if sys.platform == 'win32' else 'posix'
70PATH_MODULE = 'ntpath' if sys.platform == 'win32' else 'posixpath'
71BUILTIN_MODULE = '__builtin__'
72
73
74def _patchfs(f):
75    """Internally used to be able to use patchfs without parentheses."""
76
77    @functools.wraps(f)
78    def decorated(*args, **kwargs):
79        with Patcher() as p:
80            kwargs['fs'] = p.fs
81            return f(*args, **kwargs)
82
83    return decorated
84
85
86def patchfs(additional_skip_names=None,
87            modules_to_reload=None,
88            modules_to_patch=None,
89            allow_root_user=True):
90    """Convenience decorator to use patcher with additional parameters in a
91    test function.
92
93    Usage::
94
95        @patchfs
96        test_my_function(fs):
97            fs.create_file('foo')
98
99        @patchfs(allow_root_user=False)
100        test_with_patcher_args(fs):
101            os.makedirs('foo/bar')
102    """
103
104    def wrap_patchfs(f):
105        @functools.wraps(f)
106        def wrapped(*args, **kwargs):
107            with Patcher(
108                    additional_skip_names=additional_skip_names,
109                    modules_to_reload=modules_to_reload,
110                    modules_to_patch=modules_to_patch,
111                    allow_root_user=allow_root_user) as p:
112                kwargs['fs'] = p.fs
113                return f(*args, **kwargs)
114
115        return wrapped
116
117    # workaround to be able to use the decorator without using calling syntax
118    # (the default usage without parameters)
119    # if using the decorator without parentheses, the first argument here
120    # will be the wrapped function, so we pass it to the decorator function
121    # that doesn't use arguments
122    if inspect.isfunction(additional_skip_names):
123        return _patchfs(additional_skip_names)
124
125    return wrap_patchfs
126
127
128def load_doctests(loader, tests, ignore, module,
129                  additional_skip_names=None,
130                  modules_to_reload=None,
131                  modules_to_patch=None,
132                  allow_root_user=True):  # pylint: disable=unused-argument
133    """Load the doctest tests for the specified module into unittest.
134        Args:
135            loader, tests, ignore : arguments passed in from `load_tests()`
136            module: module under test
137            remaining args: see :py:class:`TestCase` for an explanation
138
139    File `example_test.py` in the pyfakefs release provides a usage example.
140    """
141    _patcher = Patcher(additional_skip_names=additional_skip_names,
142                       modules_to_reload=modules_to_reload,
143                       modules_to_patch=modules_to_patch,
144                       allow_root_user=allow_root_user)
145    globs = _patcher.replace_globs(vars(module))
146    tests.addTests(doctest.DocTestSuite(module,
147                                        globs=globs,
148                                        setUp=_patcher.setUp,
149                                        tearDown=_patcher.tearDown))
150    return tests
151
152
153class TestCaseMixin:
154    """Test case mixin that automatically replaces file-system related
155    modules by fake implementations.
156
157    Attributes:
158        additional_skip_names: names of modules inside of which no module
159            replacement shall be performed, in addition to the names in
160            :py:attr:`fake_filesystem_unittest.Patcher.SKIPNAMES`.
161            Instead of the module names, the modules themselves may be used.
162        modules_to_reload: A list of modules that need to be reloaded
163            to be patched dynamically; may be needed if the module
164            imports file system modules under an alias
165
166            .. caution:: Reloading modules may have unwanted side effects.
167        modules_to_patch: A dictionary of fake modules mapped to the
168            fully qualified patched module names. Can be used to add patching
169            of modules not provided by `pyfakefs`.
170
171    If you specify some of these attributes here and you have DocTests,
172    consider also specifying the same arguments to :py:func:`load_doctests`.
173
174    Example usage in derived test classes::
175
176        from unittest import TestCase
177        from fake_filesystem_unittest import TestCaseMixin
178
179        class MyTestCase(TestCase, TestCaseMixin):
180            def __init__(self, methodName='runTest'):
181                super(MyTestCase, self).__init__(
182                    methodName=methodName,
183                    additional_skip_names=['posixpath'])
184
185        import sut
186
187        class AnotherTestCase(TestCase, TestCaseMixin):
188            def __init__(self, methodName='runTest'):
189                super(MyTestCase, self).__init__(
190                    methodName=methodName, modules_to_reload=[sut])
191    """
192
193    additional_skip_names = None
194    modules_to_reload = None
195    modules_to_patch = None
196
197    @property
198    def fs(self):
199        return self._stubber.fs
200
201    def setUpPyfakefs(self,
202                      additional_skip_names=None,
203                      modules_to_reload=None,
204                      modules_to_patch=None,
205                      allow_root_user=True):
206        """Bind the file-related modules to the :py:class:`pyfakefs` fake file
207        system instead of the real file system.  Also bind the fake `open()`
208        function.
209
210        Invoke this at the beginning of the `setUp()` method in your unit test
211        class.
212        For the arguments, see the `TestCaseMixin` attribute description.
213        If any of the arguments is not None, it overwrites the settings for
214        the current test case. Settings the arguments here may be a more
215        convenient way to adapt the setting than overwriting `__init__()`.
216        """
217        if additional_skip_names is None:
218            additional_skip_names = self.additional_skip_names
219        if modules_to_reload is None:
220            modules_to_reload = self.modules_to_reload
221        if modules_to_patch is None:
222            modules_to_patch = self.modules_to_patch
223        self._stubber = Patcher(
224            additional_skip_names=additional_skip_names,
225            modules_to_reload=modules_to_reload,
226            modules_to_patch=modules_to_patch,
227            allow_root_user=allow_root_user
228        )
229
230        self._stubber.setUp()
231        self.addCleanup(self._stubber.tearDown)
232
233    def pause(self):
234        """Pause the patching of the file system modules until `resume` is
235        called. After that call, all file system calls are executed in the
236        real file system.
237        Calling pause() twice is silently ignored.
238
239        """
240        self._stubber.pause()
241
242    def resume(self):
243        """Resume the patching of the file system modules if `pause` has
244        been called before. After that call, all file system calls are
245        executed in the fake file system.
246        Does nothing if patching is not paused.
247        """
248        self._stubber.resume()
249
250
251class TestCase(unittest.TestCase, TestCaseMixin):
252    """Test case class that automatically replaces file-system related
253    modules by fake implementations. Inherits :py:class:`TestCaseMixin`.
254
255    The arguments are explained in :py:class:`TestCaseMixin`.
256    """
257
258    def __init__(self, methodName='runTest',
259                 additional_skip_names=None,
260                 modules_to_reload=None,
261                 modules_to_patch=None,
262                 allow_root_user=True):
263        """Creates the test class instance and the patcher used to stub out
264        file system related modules.
265
266        Args:
267            methodName: The name of the test method (same as in
268                unittest.TestCase)
269        """
270        super(TestCase, self).__init__(methodName)
271
272        self.additional_skip_names = additional_skip_names
273        self.modules_to_reload = modules_to_reload
274        self.modules_to_patch = modules_to_patch
275        self.allow_root_user = allow_root_user
276
277    @Deprecator('add_real_file')
278    def copyRealFile(self, real_file_path, fake_file_path=None,
279                     create_missing_dirs=True):
280        """Add the file `real_file_path` in the real file system to the same
281        path in the fake file system.
282
283        **This method is deprecated** in favor of
284        :py:meth:`FakeFilesystem..add_real_file`.
285        `copyRealFile()` is retained with limited functionality for backward
286        compatibility only.
287
288        Args:
289          real_file_path: Path to the file in both the real and fake
290            file systems
291          fake_file_path: Deprecated.  Use the default, which is
292            `real_file_path`.
293            If a value other than `real_file_path` is specified, a `ValueError`
294            exception will be raised.
295          create_missing_dirs: Deprecated.  Use the default, which creates
296            missing directories in the fake file system.  If `False` is
297            specified, a `ValueError` exception is raised.
298
299        Returns:
300          The newly created FakeFile object.
301
302        Raises:
303          OSError: If the file already exists in the fake file system.
304          ValueError: If deprecated argument values are specified.
305
306        See:
307          :py:meth:`FakeFileSystem.add_real_file`
308        """
309        if fake_file_path is not None and real_file_path != fake_file_path:
310            raise ValueError("CopyRealFile() is deprecated and no longer "
311                             "supports different real and fake file paths")
312        if not create_missing_dirs:
313            raise ValueError("CopyRealFile() is deprecated and no longer "
314                             "supports NOT creating missing directories")
315        return self._stubber.fs.add_real_file(real_file_path, read_only=False)
316
317    @DeprecationWarning
318    def tearDownPyfakefs(self):
319        """This method is deprecated and exists only for backward
320        compatibility. It does nothing.
321        """
322        pass
323
324
325class Patcher:
326    """
327    Instantiate a stub creator to bind and un-bind the file-related modules to
328    the :py:mod:`pyfakefs` fake modules.
329
330    The arguments are explained in :py:class:`TestCaseMixin`.
331
332    :py:class:`Patcher` is used in :py:class:`TestCaseMixin`.
333    :py:class:`Patcher` also works as a context manager for other tests::
334
335        with Patcher():
336            doStuff()
337    """
338    '''Stub nothing that is imported within these modules.
339    `sys` is included to prevent `sys.path` from being stubbed with the fake
340    `os.path`.
341    '''
342    SKIPMODULES = {None, fake_filesystem, fake_filesystem_shutil, sys}
343    assert None in SKIPMODULES, ("sys.modules contains 'None' values;"
344                                 " must skip them.")
345
346    IS_WINDOWS = sys.platform in ('win32', 'cygwin')
347
348    SKIPNAMES = {'os', 'path', 'io', 'genericpath', OS_MODULE, PATH_MODULE}
349    if pathlib:
350        SKIPNAMES.add('pathlib')
351    if pathlib2:
352        SKIPNAMES.add('pathlib2')
353
354    def __init__(self, additional_skip_names=None,
355                 modules_to_reload=None, modules_to_patch=None,
356                 allow_root_user=True):
357        """For a description of the arguments, see TestCase.__init__"""
358
359        if not allow_root_user:
360            # set non-root IDs even if the real user is root
361            set_uid(1)
362            set_gid(1)
363
364        self._skipNames = self.SKIPNAMES.copy()
365        # save the original open function for use in pytest plugin
366        self.original_open = open
367        self.fake_open = None
368
369        if additional_skip_names is not None:
370            skip_names = [m.__name__ if inspect.ismodule(m) else m
371                          for m in additional_skip_names]
372            self._skipNames.update(skip_names)
373
374        self._fake_module_classes = {}
375        self._class_modules = {}
376        self._init_fake_module_classes()
377
378        self.modules_to_reload = modules_to_reload or []
379
380        if modules_to_patch is not None:
381            for name, fake_module in modules_to_patch.items():
382                self._fake_module_classes[name] = fake_module
383
384        self._fake_module_functions = {}
385        self._init_fake_module_functions()
386
387        # Attributes set by _refresh()
388        self._modules = {}
389        self._fct_modules = {}
390        self._def_functions = []
391        self._open_functions = {}
392        self._stubs = None
393        self.fs = None
394        self.fake_modules = {}
395        self._dyn_patcher = None
396
397        # _isStale is set by tearDown(), reset by _refresh()
398        self._isStale = True
399        self._patching = False
400
401    def _init_fake_module_classes(self):
402        # IMPORTANT TESTING NOTE: Whenever you add a new module below, test
403        # it by adding an attribute in fixtures/module_with_attributes.py
404        # and a test in fake_filesystem_unittest_test.py, class
405        # TestAttributesWithFakeModuleNames.
406        self._fake_module_classes = {
407            'os': fake_filesystem.FakeOsModule,
408            'shutil': fake_filesystem_shutil.FakeShutilModule,
409            'io': fake_filesystem.FakeIoModule,
410        }
411        if IS_PYPY:
412            # in PyPy io.open, the module is referenced as _io
413            self._fake_module_classes['_io'] = fake_filesystem.FakeIoModule
414
415        # class modules maps class names against a list of modules they can
416        # be contained in - this allows for alternative modules like
417        # `pathlib` and `pathlib2`
418        if pathlib:
419            self._class_modules['Path'] = []
420            if pathlib:
421                self._fake_module_classes[
422                    'pathlib'] = fake_pathlib.FakePathlibModule
423                self._class_modules['Path'].append('pathlib')
424            if pathlib2:
425                self._fake_module_classes[
426                    'pathlib2'] = fake_pathlib.FakePathlibModule
427                self._class_modules['Path'].append('pathlib2')
428            self._fake_module_classes[
429                'Path'] = fake_pathlib.FakePathlibPathModule
430        if use_scandir:
431            self._fake_module_classes[
432                'scandir'] = fake_scandir.FakeScanDirModule
433
434    def _init_fake_module_functions(self):
435        # handle patching function imported separately like
436        # `from os import stat`
437        # each patched function name has to be looked up separately
438        for mod_name, fake_module in self._fake_module_classes.items():
439            if (hasattr(fake_module, 'dir') and
440                    inspect.isfunction(fake_module.dir)):
441                for fct_name in fake_module.dir():
442                    module_attr = (getattr(fake_module, fct_name), mod_name)
443                    self._fake_module_functions.setdefault(
444                        fct_name, {})[mod_name] = module_attr
445                    if mod_name == 'os':
446                        self._fake_module_functions.setdefault(
447                            fct_name, {})[OS_MODULE] = module_attr
448
449        # special handling for functions in os.path
450        fake_module = fake_filesystem.FakePathModule
451        for fct_name in fake_module.dir():
452            module_attr = (getattr(fake_module, fct_name), PATH_MODULE)
453            self._fake_module_functions.setdefault(
454                fct_name, {})['genericpath'] = module_attr
455            self._fake_module_functions.setdefault(
456                fct_name, {})[PATH_MODULE] = module_attr
457
458    def __enter__(self):
459        """Context manager for usage outside of
460        fake_filesystem_unittest.TestCase.
461        Ensure that all patched modules are removed in case of an
462        unhandled exception.
463        """
464        self.setUp()
465        return self
466
467    def __exit__(self, exc_type, exc_val, exc_tb):
468        self.tearDown()
469
470    def _is_fs_module(self, mod, name, module_names):
471        try:
472            return (inspect.ismodule(mod) and
473                    mod.__name__ in module_names
474                    or inspect.isclass(mod) and
475                    mod.__module__ in self._class_modules.get(name, []))
476        except AttributeError:
477            # handle cases where the module has no __name__ or __module__
478            # attribute - see #460
479            return False
480
481    def _is_fs_function(self, fct):
482        try:
483            return ((inspect.isfunction(fct) or
484                     inspect.isbuiltin(fct)) and
485                    fct.__name__ in self._fake_module_functions and
486                    fct.__module__ in self._fake_module_functions[
487                        fct.__name__])
488        except AttributeError:
489            # handle cases where the function has no __name__ or __module__
490            # attribute
491            return False
492
493    def _def_values(self, item):
494        """Find default arguments that are file-system functions to be
495        patched in top-level functions and members of top-level classes."""
496        # check for module-level functions
497        if inspect.isfunction(item):
498            if item.__defaults__:
499                for i, d in enumerate(item.__defaults__):
500                    if self._is_fs_function(d):
501                        yield item, i, d
502        elif inspect.isclass(item):
503            # check for methods in class (nested classes are ignored for now)
504            try:
505                for m in inspect.getmembers(item,
506                                            predicate=inspect.isfunction):
507                    m = m[1]
508                    if m.__defaults__:
509                        for i, d in enumerate(m.__defaults__):
510                            if self._is_fs_function(d):
511                                yield m, i, d
512            except Exception:
513                # Ignore any exception, examples:
514                # ImportError: No module named '_gdbm'
515                # _DontDoThat() (see #523)
516                pass
517
518    def _find_modules(self):
519        """Find and cache all modules that import file system modules.
520        Later, `setUp()` will stub these with the fake file system
521        modules.
522        """
523
524        module_names = list(self._fake_module_classes.keys()) + [PATH_MODULE]
525        for name, module in list(sys.modules.items()):
526            try:
527                if (module in self.SKIPMODULES or
528                        not inspect.ismodule(module) or
529                        module.__name__.split('.')[0] in self._skipNames):
530                    continue
531            except AttributeError:
532                # workaround for some py (part of pytest) versions
533                # where py.error has no __name__ attribute
534                # see https://github.com/pytest-dev/py/issues/73
535                continue
536
537            module_items = module.__dict__.copy().items()
538
539            # suppress specific pytest warning - see #466
540            with warnings.catch_warnings():
541                warnings.filterwarnings(
542                    'ignore',
543                    message='The compiler package is deprecated',
544                    category=DeprecationWarning,
545                    module='py'
546                )
547                modules = {name: mod for name, mod in module_items
548                           if self._is_fs_module(mod, name, module_names)}
549
550            for name, mod in modules.items():
551                self._modules.setdefault(name, set()).add((module,
552                                                           mod.__name__))
553            functions = {name: fct for name, fct in
554                         module_items
555                         if self._is_fs_function(fct)}
556
557            # find default arguments that are file system functions
558            for _, fct in module_items:
559                for f, i, d in self._def_values(fct):
560                    self._def_functions.append((f, i, d))
561
562            for name, fct in functions.items():
563                self._fct_modules.setdefault(
564                    (name, fct.__name__, fct.__module__), set()).add(module)
565
566    def _refresh(self):
567        """Renew the fake file system and set the _isStale flag to `False`."""
568        if self._stubs is not None:
569            self._stubs.smart_unset_all()
570        self._stubs = mox3_stubout.StubOutForTesting()
571
572        self.fs = fake_filesystem.FakeFilesystem(patcher=self)
573        for name in self._fake_module_classes:
574            self.fake_modules[name] = self._fake_module_classes[name](self.fs)
575        self.fake_modules[PATH_MODULE] = self.fake_modules['os'].path
576        self.fake_open = fake_filesystem.FakeFileOpen(self.fs)
577
578        self._isStale = False
579
580    def setUp(self, doctester=None):
581        """Bind the file-related modules to the :py:mod:`pyfakefs` fake
582        modules real ones.  Also bind the fake `file()` and `open()` functions.
583        """
584        self.has_fcopy_file = (sys.platform == 'darwin' and
585                               hasattr(shutil, '_HAS_FCOPYFILE') and
586                               shutil._HAS_FCOPYFILE)
587        if self.has_fcopy_file:
588            shutil._HAS_FCOPYFILE = False
589
590        temp_dir = tempfile.gettempdir()
591        self._find_modules()
592        self._refresh()
593
594        if doctester is not None:
595            doctester.globs = self.replace_globs(doctester.globs)
596
597        self.start_patching()
598
599        # the temp directory is assumed to exist at least in `tempfile1`,
600        # so we create it here for convenience
601        self.fs.create_dir(temp_dir)
602
603    def start_patching(self):
604        if not self._patching:
605            self._patching = True
606
607            for name, modules in self._modules.items():
608                for module, attr in modules:
609                    self._stubs.smart_set(
610                        module, name, self.fake_modules[attr])
611            for (name, ft_name, ft_mod), modules in self._fct_modules.items():
612                method, mod_name = self._fake_module_functions[ft_name][ft_mod]
613                fake_module = self.fake_modules[mod_name]
614                attr = method.__get__(fake_module, fake_module.__class__)
615                for module in modules:
616                    self._stubs.smart_set(module, name, attr)
617
618            for (fct, idx, ft) in self._def_functions:
619                method, mod_name = self._fake_module_functions[
620                    ft.__name__][ft.__module__]
621                fake_module = self.fake_modules[mod_name]
622                attr = method.__get__(fake_module, fake_module.__class__)
623                new_defaults = []
624                for i, d in enumerate(fct.__defaults__):
625                    if i == idx:
626                        new_defaults.append(attr)
627                    else:
628                        new_defaults.append(d)
629                fct.__defaults__ = tuple(new_defaults)
630
631            self._dyn_patcher = DynamicPatcher(self)
632            sys.meta_path.insert(0, self._dyn_patcher)
633            for module in self.modules_to_reload:
634                if module.__name__ in sys.modules:
635                    reload(module)
636
637    def replace_globs(self, globs_):
638        globs = globs_.copy()
639        if self._isStale:
640            self._refresh()
641        for name in self._fake_module_classes:
642            if name in globs:
643                globs[name] = self._fake_module_classes[name](self.fs)
644        return globs
645
646    def tearDown(self, doctester=None):
647        """Clear the fake filesystem bindings created by `setUp()`."""
648        self.stop_patching()
649        if self.has_fcopy_file:
650            shutil._HAS_FCOPYFILE = True
651
652        reset_ids()
653
654    def stop_patching(self):
655        if self._patching:
656            self._isStale = True
657            self._patching = False
658            self._stubs.smart_unset_all()
659            self.unset_defaults()
660            self._dyn_patcher.cleanup()
661            sys.meta_path.pop(0)
662
663    def unset_defaults(self):
664        for (fct, idx, ft) in self._def_functions:
665            new_defaults = []
666            for i, d in enumerate(fct.__defaults__):
667                if i == idx:
668                    new_defaults.append(ft)
669                else:
670                    new_defaults.append(d)
671            fct.__defaults__ = tuple(new_defaults)
672        self._def_functions = []
673
674    def pause(self):
675        """Pause the patching of the file system modules until `resume` is
676        called. After that call, all file system calls are executed in the
677        real file system.
678        Calling pause() twice is silently ignored.
679
680        """
681        self.stop_patching()
682
683    def resume(self):
684        """Resume the patching of the file system modules if `pause` has
685        been called before. After that call, all file system calls are
686        executed in the fake file system.
687        Does nothing if patching is not paused.
688        """
689        self.start_patching()
690
691
692class Pause:
693    """Simple context manager that allows to pause/resume patching the
694    filesystem. Patching is paused in the context manager, and resumed after
695    going out of it's scope.
696    """
697
698    def __init__(self, caller):
699        """Initializes the context manager with the fake filesystem.
700
701        Args:
702            caller: either the FakeFilesystem instance, the Patcher instance
703                or the pyfakefs test case.
704        """
705        if isinstance(caller, (Patcher, TestCaseMixin)):
706            self._fs = caller.fs
707        elif isinstance(caller, fake_filesystem.FakeFilesystem):
708            self._fs = caller
709        else:
710            raise ValueError('Invalid argument - should be of type '
711                             '"fake_filesystem_unittest.Patcher", '
712                             '"fake_filesystem_unittest.TestCase" '
713                             'or "fake_filesystem.FakeFilesystem"')
714
715    def __enter__(self):
716        self._fs.pause()
717        return self._fs
718
719    def __exit__(self, *args):
720        return self._fs.resume()
721
722
723class DynamicPatcher:
724    """A file loader that replaces file system related modules by their
725    fake implementation if they are loaded after calling `setUpPyfakefs()`.
726    Implements the protocol needed for import hooks.
727    """
728
729    def __init__(self, patcher):
730        self._patcher = patcher
731        self.sysmodules = {}
732        self.modules = self._patcher.fake_modules
733        self._loaded_module_names = set()
734
735        # remove all modules that have to be patched from `sys.modules`,
736        # otherwise the find_... methods will not be called
737        for name in self.modules:
738            if self.needs_patch(name) and name in sys.modules:
739                self.sysmodules[name] = sys.modules[name]
740                del sys.modules[name]
741
742        for name, module in self.modules.items():
743            sys.modules[name] = module
744
745    def cleanup(self):
746        for module in self.sysmodules:
747            sys.modules[module] = self.sysmodules[module]
748        for module in self._patcher.modules_to_reload:
749            if module.__name__ in sys.modules:
750                reload(module)
751        reloaded_module_names = [module.__name__
752                                 for module in self._patcher.modules_to_reload]
753        # Dereference all modules loaded during the test so they will reload on
754        # the next use, ensuring that no faked modules are referenced after the
755        # test.
756        for name in self._loaded_module_names:
757            if name in sys.modules and name not in reloaded_module_names:
758                del sys.modules[name]
759
760    def needs_patch(self, name):
761        """Check if the module with the given name shall be replaced."""
762        if name not in self.modules:
763            self._loaded_module_names.add(name)
764            return False
765        if (name in sys.modules and
766                type(sys.modules[name]) == self.modules[name]):
767            return False
768        return True
769
770    def find_spec(self, fullname, path, target=None):
771        """Module finder for Python 3."""
772        if self.needs_patch(fullname):
773            return ModuleSpec(fullname, self)
774
775    def load_module(self, fullname):
776        """Replaces the module by its fake implementation."""
777        sys.modules[fullname] = self.modules[fullname]
778        return self.modules[fullname]
779