Repository: jmcgeheeiv/pyfakefs
Pull request #433 (2018-09-26 05:50:07)

Add side_effect option to fake files
**Background:**
On linux, parts of the file system have other uses than being actual files. One example is "/sys/class/gpio".
To mock the expected behaviour in this example a "side effect" of writing to the files is needed:

1. When writing an int XX to "/sys/class/gpio/export" a new directory is created "/sys/class/gpio/gpioXX" with some files
2. when writing an int XX to "/sys/class/gpio/unexport" the directory "/sys/class/gpio/gpioXX" is removed.

This "side effect" needs to happen in the same way during testing as in real environment to not affect the SUT. For example, if the folder "/sys/class/gpio/gpioXX" is created in the setup of the test the SUT can see that gpioXX is already available and skip writing to "/sys/class/gpio/export".
Another solution could have been polling the files if export has been written to, however this problems as the polling must be much faster than the SUT the be able to cause the side_effect to simulate the real environment.

**Pull request:**
The proposed solution in this pull request is to add a side_effect argument to fake files which can contain a function handle that is invoked, with the file object as argument, after the file is written. This function then causes the "side effects" needed.
The default is to not have any "side effect", preserving legacy.
I also added 2 unittests for the new functionality.


Patch:
diff --git a/.travis.yml b/.travis.yml
index 5b8248d..d0f20b6 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -20,26 +20,29 @@ matrix:
     include:
         - python: 2.7
           env: PYTHON=py27
-        - python: 3.3
-          env: PYTHON=py33
         - python: 3.4
           env: PYTHON=py34
         - python: 3.5
           env: PYTHON=py35
         - python: 3.6
           env: PYTHON=py36
+        - python: 3.7
+          dist: xenial
+          sudo: true
+          env: PYTHON=py37
         - python: pypy2.7-5.8.0
           env: PYTHON=pypy2
         - python: pypy3.5-5.8.0
           env: PYTHON=pypy3
-        - python: 3.7-dev
-          env: PYTHON=py37
         - os: osx
           language: generic
           env: PYTHON=py27
         - os: osx
           language: generic
           env: PYTHON=py36
+        - os: osx
+          language: generic
+          env: PYTHON=py37
 
 install:
   - ./.travis/install.sh
diff --git a/.travis/install.sh b/.travis/install.sh
index e62c01e..0ae52ca 100755
--- a/.travis/install.sh
+++ b/.travis/install.sh
@@ -17,10 +17,6 @@ if [[ $TRAVIS_OS_NAME == 'osx' ]]; then
             curl -O https://bootstrap.pypa.io/get-pip.py
             python get-pip.py --user
             ;;
-        py33)
-            pyenv install 3.3.5
-            pyenv global 3.3.5
-            ;;
         py34)
             pyenv install 3.4.4
             pyenv global 3.4.4
@@ -30,8 +26,12 @@ if [[ $TRAVIS_OS_NAME == 'osx' ]]; then
             pyenv global 3.5.4
             ;;
         py36)
-            pyenv install 3.6.3
-            pyenv global 3.6.3
+            pyenv install 3.6.6
+            pyenv global 3.6.6
+            ;;
+        py37)
+            pyenv install 3.7.0
+            pyenv global 3.7.0
             ;;
         pypy*)
             pyenv install "$PYPY_VERSION"
diff --git a/CHANGES.md b/CHANGES.md
index 1b46e96..2f37957 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -3,9 +3,13 @@ The release versions are PyPi releases.
 
 ## Version 3.5 (as yet unreleased)
 
+This version of pyfakefs does not support Python 3.3. Python 3.3 users shall 
+keep using pyfakefs 3.4.3, or upgrade to a newer Python version.
+
 #### New Features
   * parameter `patch_path` has been removed from `UnitTest` and `Patcher`, 
     the correct patching of `path` imports is now done automatically
+  * `UnitTest` /`Patcher` arguments can now also be set in `setUpPyfakefs()`
   * added pathlib2 support ([#408](../../issues/408)) ([#422](../../issues/422))
   * added some support for extended filesystem attributes under Linux 
   ([#423](../../issues/423)) 
diff --git a/README.md b/README.md
index c7098af..a04a693 100644
--- a/README.md
+++ b/README.md
@@ -40,7 +40,8 @@ using convenience functions.
 ## Installation
 
 ### Compatibility
-pyfakefs works with CPython 2.7, 3.3 and above, on Linux, Windows and OSX (MacOS), and with PyPy2 and PyPy3.
+pyfakefs works with CPython 2.7, 3.4 and above, on Linux, Windows and OSX 
+(MacOS), and with PyPy2 and PyPy3.
 
 pyfakefs works with [PyTest](http://doc.pytest.org) version 2.8.6 or above.
 
@@ -59,10 +60,10 @@ For example, pyfakefs will not work with [`lxml`](http://lxml.de/).  In this cas
 ### Continuous integration
 
 pyfakefs is currently automatically tested:
-* On Linux, with Python 2.7, 3.3 and above using [Travis](https://travis-ci.org/jmcgeheeiv/pyfakefs)
-* On MacOS, with Python 2.7 and 3.6, also using [Travis](https://travis-ci.org/jmcgeheeiv/pyfakefs).
+* On Linux, with Python 2.7, and 3.4 to 3.7, using [Travis](https://travis-ci.org/jmcgeheeiv/pyfakefs)
+* On MacOS, with Python 2.7, 3.6 and 3.7, also using [Travis](https://travis-ci.org/jmcgeheeiv/pyfakefs).
   The Linux/MacOS build is currently [![Build Status](https://travis-ci.org/jmcgeheeiv/pyfakefs.svg)](https://travis-ci.org/jmcgeheeiv/pyfakefs).
-* On Windows, with Python 2.7, 3.4 and above using [Appveyor](https://ci.appveyor.com/project/jmcgeheeiv/pyfakefs).
+* On Windows, with Python 2.7, and 3.4 to 3.7 using [Appveyor](https://ci.appveyor.com/project/jmcgeheeiv/pyfakefs).
   The Windows build is currently [![Build status](https://ci.appveyor.com/api/projects/status/4o8j21ufuo056873/branch/master?svg=true)](https://ci.appveyor.com/project/jmcgeheeiv/pyfakefs/branch/master).
 
 ### Running pyfakefs unit tests
diff --git a/appveyor.yml b/appveyor.yml
index c790992..5778719 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -5,6 +5,7 @@ environment:
     - PYTHON: "C:\\Python34-x64"
     - PYTHON: "C:\\Python35-x64"
     - PYTHON: "C:\\Python36-x64"
+    - PYTHON: "C:\\Python37-x64"
 
 install:
   - "%PYTHON%\\python.exe -m pip install -r requirements.txt"
diff --git a/docs/intro.rst b/docs/intro.rst
index c6c4ebe..9161b6c 100644
--- a/docs/intro.rst
+++ b/docs/intro.rst
@@ -5,7 +5,8 @@ Introduction
 Using pyfakefs, your tests operate on a fake file system in memory without touching the real disk.
 The software under test requires no modification to work with pyfakefs.
 
-pyfakefs works with CPython 2.7, 3.3 and above, on Linux, Windows and OSX (MacOS), and with PyPy2 and PyPy3.
+pyfakefs works with CPython 2.7, 3.4 and above, on Linux, Windows and OSX
+(MacOS), and with PyPy2 and PyPy3.
 
 pyfakefs works with `PyTest <doc.pytest.org>`__ version 2.8.6 or above.
 
diff --git a/docs/usage.rst b/docs/usage.rst
index dd6777c..6bbb4b1 100644
--- a/docs/usage.rst
+++ b/docs/usage.rst
@@ -92,6 +92,9 @@ Customizing Patcher and TestCase
 Both ``fake_filesystem_unittest.Patcher`` and ``fake_filesystem_unittest.TestCase``
 provide a few additional arguments for fine-tuning. These are only needed if
 patching does not work for some module.
+In case of ``fake_filesystem_unittest.TestCase``, these arguments can either
+ be set in the TestCase instance initialization, or in the call of
+``setUpPyfakefs()``.
 
 .. note:: If you need these arguments in ``PyTest``, you have to
   use ``Patcher`` directly instead of the ``fs`` fixture. Alternatively,
diff --git a/pyfakefs/fake_filesystem.py b/pyfakefs/fake_filesystem.py
index 29ee3d1..6e05f29 100644
--- a/pyfakefs/fake_filesystem.py
+++ b/pyfakefs/fake_filesystem.py
@@ -207,7 +207,8 @@ class FakeFile(object):
     )
 
     def __init__(self, name, st_mode=S_IFREG | PERM_DEF_FILE,
-                 contents=None, filesystem=None, encoding=None, errors=None):
+                 contents=None, filesystem=None, encoding=None, errors=None,
+                 side_effect=None):
         """
         Args:
             name: Name of the file/directory, without parent path information
@@ -220,12 +221,14 @@ class FakeFile(object):
             encoding: If contents is a unicode string, the encoding used
                 for serialization.
             errors: The error mode used for encoding/decoding errors.
+            side_effect: function handle that is executed when file is written,
+                must accept the file object as an argument.
         """
         # to be backwards compatible regarding argument order, we raise on None
         if filesystem is None:
             raise ValueError('filesystem shall not be None')
         self.filesystem = filesystem
-
+        self._side_effect = side_effect
         self.name = name
         self.stat_result = FakeStatResult(
             filesystem.is_windows_fs, time.time())
@@ -357,6 +360,7 @@ class FakeFile(object):
 
     def set_contents(self, contents, encoding=None):
         """Sets the file contents and size and increases the modification time.
+        Also executes the side_effects if available.
 
         Args:
           contents: (str, bytes, unicode) new content of file.
@@ -373,6 +377,8 @@ class FakeFile(object):
         current_time = time.time()
         self.st_ctime = current_time
         self.st_mtime = current_time
+        if self._side_effect is not None:
+            self._side_effect(self)
 
     @property
     def size(self):
@@ -520,7 +526,7 @@ class FakeFileFromRealFile(FakeFile):
     The contents of the file are read on demand only.
     """
 
-    def __init__(self, file_path, filesystem):
+    def __init__(self, file_path, filesystem, side_effect=None):
         """
         Args:
             file_path: Path to the existing file.
@@ -531,7 +537,8 @@ class FakeFileFromRealFile(FakeFile):
             OSError: if the file already exists in the fake file system.
         """
         super(FakeFileFromRealFile, self).__init__(
-            name=os.path.basename(file_path), filesystem=filesystem)
+            name=os.path.basename(file_path), filesystem=filesystem,
+            side_effect=side_effect)
         self.contents_read = False
 
     @property
@@ -2252,7 +2259,8 @@ class FakeFilesystem(object):
 
     def create_file(self, file_path, st_mode=S_IFREG | PERM_DEF_FILE,
                     contents='', st_size=None, create_missing_dirs=True,
-                    apply_umask=False, encoding=None, errors=None):
+                    apply_umask=False, encoding=None, errors=None,
+                    side_effect=None):
         """Create `file_path`, including all the parent directories along
         the way.
 
@@ -2272,6 +2280,8 @@ class FakeFilesystem(object):
             encoding: If `contents` is a unicode string, the encoding used
                 for serialization.
             errors: The error mode used for encoding/decoding errors.
+            side_effect: function handle that is executed when file is written,
+                must accept the file object as an argument.
 
         Returns:
             The newly created FakeFile object.
@@ -2282,7 +2292,7 @@ class FakeFilesystem(object):
         """
         return self.create_file_internally(
             file_path, st_mode, contents, st_size, create_missing_dirs,
-            apply_umask, encoding, errors)
+            apply_umask, encoding, errors, side_effect=side_effect)
 
     def add_real_file(self, source_path, read_only=True, target_path=None):
         """Create `file_path`, including all the parent directories along the
@@ -2411,7 +2421,8 @@ class FakeFilesystem(object):
                                contents='', st_size=None,
                                create_missing_dirs=True,
                                apply_umask=False, encoding=None, errors=None,
-                               read_from_real_fs=False, raw_io=False):
+                               read_from_real_fs=False, raw_io=False,
+                               side_effect=None):
         """Internal fake file creator that supports both normal fake files
         and fake files based on real files.
 
@@ -2432,6 +2443,8 @@ class FakeFilesystem(object):
             read_from_real_fs: if True, the contents are read from the real
                 file system on demand.
             raw_io: `True` if called from low-level API (`os.open`)
+            side_effect: function handle that is executed when file is written,
+                must accept the file object as an argument.
         """
         error_fct = self.raise_os_error if raw_io else self.raise_io_error
         file_path = self.make_string_path(file_path)
@@ -2455,10 +2468,12 @@ class FakeFilesystem(object):
         if apply_umask:
             st_mode &= ~self.umask
         if read_from_real_fs:
-            file_object = FakeFileFromRealFile(file_path, filesystem=self)
+            file_object = FakeFileFromRealFile(file_path, filesystem=self,
+                                               side_effect=side_effect)
         else:
             file_object = FakeFile(new_file, st_mode, filesystem=self,
-                                   encoding=encoding, errors=errors)
+                                   encoding=encoding, errors=errors,
+                                   side_effect=side_effect)
 
         self._last_ino += 1
         file_object.st_ino = self._last_ino
diff --git a/setup.py b/setup.py
index c3f9a20..a0a6a79 100644
--- a/setup.py
+++ b/setup.py
@@ -40,7 +40,6 @@ CLASSIFIERS = [
     'License :: OSI Approved :: Apache Software License',
     'Programming Language :: Python',
     'Programming Language :: Python :: 2.7',
-    'Programming Language :: Python :: 3.3',
     'Programming Language :: Python :: 3.4',
     'Programming Language :: Python :: 3.5',
     'Programming Language :: Python :: 3.6',


Test patch:
diff --git a/pyfakefs/fake_filesystem_unittest.py b/pyfakefs/fake_filesystem_unittest.py
index aaaf841..19c153d 100644
--- a/pyfakefs/fake_filesystem_unittest.py
+++ b/pyfakefs/fake_filesystem_unittest.py
@@ -154,19 +154,35 @@ class TestCaseMixin(object):
     def fs(self):
         return self._stubber.fs
 
-    def setUpPyfakefs(self):
+    def setUpPyfakefs(self,
+                      additional_skip_names=None,
+                      use_dynamic_patch=None,
+                      modules_to_reload=None,
+                      modules_to_patch=None):
         """Bind the file-related modules to the :py:class:`pyfakefs` fake file
         system instead of the real file system.  Also bind the fake `open()`
         function, and on Python 2, the `file()` function.
 
         Invoke this at the beginning of the `setUp()` method in your unit test
         class.
+        For the arguments, see the `TestCaseMixin` attribute description.
+        If any of the arguments is not None, it overwrites the settings for
+        the current test case. Settings the arguments here may be a more
+        convenient way to adapt the setting than overwriting `__init__()`.
         """
+        if additional_skip_names is None:
+            additional_skip_names = self.additional_skip_names
+        if use_dynamic_patch is None:
+            use_dynamic_patch = self.use_dynamic_patch
+        if modules_to_reload is None:
+            modules_to_reload = self.modules_to_reload
+        if modules_to_patch is None:
+            modules_to_patch = self.modules_to_patch
         self._stubber = Patcher(
-            additional_skip_names=self.additional_skip_names,
-            use_dynamic_patch=self.use_dynamic_patch,
-            modules_to_reload=self.modules_to_reload,
-            modules_to_patch=self.modules_to_patch)
+            additional_skip_names=additional_skip_names,
+            use_dynamic_patch=use_dynamic_patch,
+            modules_to_reload=modules_to_reload,
+            modules_to_patch=modules_to_patch)
 
         self._stubber.setUp()
         self.addCleanup(self._stubber.tearDown)
diff --git a/pyfakefs/tests/fake_filesystem_test.py b/pyfakefs/tests/fake_filesystem_test.py
index 3ee5448..22b08ca 100644
--- a/pyfakefs/tests/fake_filesystem_test.py
+++ b/pyfakefs/tests/fake_filesystem_test.py
@@ -2035,5 +2035,36 @@ class RealFileSystemAccessTest(TestCase):
         self.check_writable_file(fake_file, real_file_path)
 
 
+class FileSideEffectTests(TestCase):
+    def side_effect(self):
+        test_case = self
+        test_case.side_effect_called = False
+
+        def __side_effect(file_object):
+            test_case.side_effect_called = True
+            test_case.side_effect_file_object_content = file_object.contents
+        return __side_effect
+
+    def setUp(self):
+        # use the real path separator to work with the real file system
+        self.filesystem = fake_filesystem.FakeFilesystem()
+        self.filesystem.create_file('/a/b/file_one',
+                                    side_effect=self.side_effect())
+
+    def test_side_effect_called(self):
+        fake_open = fake_filesystem.FakeFileOpen(self.filesystem)
+        self.side_effect_called = False
+        with fake_open('/a/b/file_one', 'w') as handle:
+            handle.write('foo')
+        self.assertTrue(self.side_effect_called)
+
+    def test_side_effect_file_object(self):
+        fake_open = fake_filesystem.FakeFileOpen(self.filesystem)
+        self.side_effect_called = False
+        with fake_open('/a/b/file_one', 'w') as handle:
+            handle.write('foo')
+        self.assertEquals(self.side_effect_file_object_content, 'foo')
+
+
 if __name__ == '__main__':
     unittest.main()
diff --git a/pyfakefs/tests/fake_filesystem_unittest_test.py b/pyfakefs/tests/fake_filesystem_unittest_test.py
index 212b3c9..e378636 100644
--- a/pyfakefs/tests/fake_filesystem_unittest_test.py
+++ b/pyfakefs/tests/fake_filesystem_unittest_test.py
@@ -137,14 +137,23 @@ class TestPyfakefsUnittest(TestPyfakefsUnittestBase):  # pylint: disable=R0904
             self.assertTrue(self.fs.exists('/fake_file.txt'))
 
 
-class TestImportAsOtherName(fake_filesystem_unittest.TestCase):
-    def __init__(self, methodName='RunTest'):
-        modules_to_load = [pyfakefs.tests.import_as_example]
-        super(TestImportAsOtherName, self).__init__(
-            methodName, modules_to_reload=modules_to_load)
+class TestImportAsOtherNameInit(fake_filesystem_unittest.TestCase):
+    def setUp(self):
+        self.setUpPyfakefs(
+            modules_to_reload=[pyfakefs.tests.import_as_example])
 
+    def test_file_exists(self):
+        file_path = '/foo/bar/baz'
+        self.fs.create_file(file_path)
+        self.assertTrue(self.fs.exists(file_path))
+        self.assertTrue(
+            pyfakefs.tests.import_as_example.check_if_exists(file_path))
+
+
+class TestImportAsOtherNameSetup(fake_filesystem_unittest.TestCase):
     def setUp(self):
-        self.setUpPyfakefs()
+        self.setUpPyfakefs(
+            modules_to_reload=[pyfakefs.tests.import_as_example])
 
     def test_file_exists(self):
         file_path = '/foo/bar/baz'
@@ -180,10 +189,6 @@ class TestPathNotPatchedIfNotOsPath(TestPyfakefsUnittestBase):
        An own path module (in this case an alias to math) can be imported
        and used.
     """
-
-    def __init__(self, methodName='runTest'):
-        super(TestPathNotPatchedIfNotOsPath, self).__init__(methodName)
-
     def test_own_path_module(self):
         self.assertEqual(2, path.floor(2.5))