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