Repository: jmcgeheeiv/pyfakefs
Pull request #183 (2017-05-10 19:25:33)

Copied mox3 stubout into pyfakefs
- removed dependency on mox3
- see #182

jmcgeheeiv: Well done.
mrbean-bremen: Did you close this unintentionally?
jmcgeheeiv: Yes, I merged it.  Was it not ready?

Patch:
diff --git a/.travis.yml b/.travis.yml
index e962bb6..cb323e5 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -21,13 +21,12 @@ python:
   - "3.3"
   - "3.4"
   - "3.5"
-  - "3.6-dev"
+  - "3.6"
   - "nightly" # points to 3.7-dev
   - "pypy"
   - "pypy3"
 
 install:
-  - if [[ $TRAVIS_PYTHON_VERSION == 2.6 ]]; then pip install importlib unittest2; fi
   - pip install -r requirements.txt
   - pip install .
 
diff --git a/CHANGES.md b/CHANGES.md
index 5a28619..f6a8667 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -10,6 +10,9 @@ The release versions are PyPi releases.
     File contents are read from the real file system only when needed ([#170](../../issues/170)).
   * Added the CHANGES.md release notes to the release manifest
 
+#### Infrastructure
+  * `mox3` is no longer required - the relevant part has been integrated into pyfakefs ([#182](../../issues/182))
+  
 #### Fixes
  * `FakeShutilModule.rmtree` failed for directory ending with path separator ([#177](../../issues/177))
  * Case incorrectly handled for added Windows drives 
@@ -39,7 +42,7 @@ The release versions are PyPi releases.
  * support for `os.replace` (Python >= 3.3)
  * `os.access`, `os.chmod`, `os.chown`, `os.stat`, `os.utime`:
    support for `follow_symlinks` argument (Python >= 3.3)
- * support for `os.scandir` (Python >= 3.5)([#119](../../issues/119))
+ * support for `os.scandir` (Python >= 3.5) ([#119](../../issues/119))
  * option to not fake modules named `path` ([#53](../../issues/53))
  * `glob.glob`, `glob.iglob`: support for `recursive` argument (Python >= 3.5) ([#116](../../issues/116))
  * support for `glob.iglob` ([#59](../../issues/59))
@@ -61,15 +64,15 @@ The release versions are PyPi releases.
  * `os.makedirs`: support for `exist_ok` argument (Python >= 3.2) ([#98](../../issues/98))
  * support for fake `io.open()` ([#70](../../issues/70))
  * support for mount points ([#25](../../issues/25))
- * support for hard links
+ * support for hard links ([#75](../../issues/75))
  * support for float times (mtime, ctime)
  * Windows support:
      * support for alternative path separator
      * support for case-insensitive filesystems ([#69](../../issues/69))
      * support for drive letters and UNC paths
  * support for filesystem size ([#86](../../issues/86))
- * `shutil.rmtree`: support for `ignore_errors` and `onerror` arguments
- * support for `os.fsync()` and `os.fdatasync()`
+ * `shutil.rmtree`: support for `ignore_errors` and `onerror` arguments ([#72](../../issues/72))
+ * support for `os.fsync()` and `os.fdatasync()` ([#73](../../issues/73))
  * `os.walk`: Support for `followlinks` argument
  
 #### Fixes
@@ -77,7 +80,6 @@ The release versions are PyPi releases.
  * file permissions on deletion not correctly handled ([#27](../../issues/27))
  * `shutil.copy` error with bytes contents ([#105](../../issues/105))
  * mtime and ctime not updated on content changes
- * Reading from fake block devices doesn't work ([#24](../../issues/24))
 
 ## [Version 2.7](https://pypi.python.org/pypi/pyfakefs/2.7)
 
@@ -95,9 +97,14 @@ The release versions are PyPi releases.
  * better support for Python 3
 
 #### Fixes
- * `chown` incorrectly accepts non-integer uid/gid arguments
+ * `os.utime` fails to traverse symlinks ([#49](../../issues/49))
+ * `chown` incorrectly accepts non-integer uid/gid arguments ([#30](../../issues/30))
+ * Reading from fake block devices doesn't work ([#24](../../issues/24))
+ * `fake_tempfile` is using `AddOpenFile` incorrectly ([#23](../../issues/23))
  * incorrect behavior of `relpath`, `abspath` and `normpath` on Windows.
+ * cygwin wasn't treated as Windows ([#37](../../issues/37))
  * Python 3 `open` in binary mode not working ([#32](../../issues/32))
+ * `os.remove` doesn't work with relative paths ([#31](../../issues/31))
  * `mkstemp` returns no valid file descriptor ([#19](../../issues/19))
  * `open` methods lack `IOError` for prohibited operations ([#18](../../issues/18))
  * incorrectly resolved relative path ([#3](../../issues/3))
diff --git a/README.md b/README.md
index 00848d8..e914a3c 100644
--- a/README.md
+++ b/README.md
@@ -80,8 +80,6 @@ with patch('mymodule.glob', glob):
 ### Compatibility
 pyfakefs works with Python 2.6 and above, on Linux, Windows and OSX (MacOS).
 
-pyfakefs requires [mox3](https://pypi.python.org/pypi/mox3).
-
 pyfakefs works with [PyTest](doc.pytest.org) version 2.8.6 or above.
 
 ### PyPi
diff --git a/docs/usage.rst b/docs/usage.rst
index 34df90a..998f730 100644
--- a/docs/usage.rst
+++ b/docs/usage.rst
@@ -29,7 +29,7 @@ The PyTest plugin provides the ``fs`` fixture for use in your test. For example:
 
 Patch using unittest.mock
 ~~~~~~~~~~~~~~~~~~~~~~~~~
-If you are using other means of testing like `nose <http://nose2.readthedocs.io>`__, you can to do the patching yourself using
+If you are using other means of testing like `nose <http://nose2.readthedocs.io>`__, you can do the patching yourself using
 ``mock.patch()``:
 
 .. code:: python
diff --git a/mox3_stubout_example.py b/mox3_stubout_example.py
new file mode 100644
index 0000000..94e2d98
--- /dev/null
+++ b/mox3_stubout_example.py
@@ -0,0 +1,34 @@
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""
+Example module that is used for testing the functionality of :py:class`pyfakefs.mox_stubout.StubOutForTesting`.
+"""
+import datetime
+import math
+import os
+
+
+def check_if_exists(filepath):
+    return os.path.exists(filepath)
+
+
+def fabs(x):
+    return math.fabs(x)
+
+
+def set_contains(container, element):
+    return element in container
+
+
+def tomorrow():
+    return datetime.date.today() + datetime.timedelta(days=1)
diff --git a/pyfakefs/mox3_stubout.py b/pyfakefs/mox3_stubout.py
new file mode 100644
index 0000000..ab66502
--- /dev/null
+++ b/pyfakefs/mox3_stubout.py
@@ -0,0 +1,160 @@
+# Copyright 2008 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# This is a fork of the pymox library intended to work with Python 3.
+# The file was modified by quermit@gmail.com and dawid.fatyga@gmail.com
+#
+# Previously, pyfakefs used just this file from the mox3 library.
+# However, mox3 will soon be decommissioned, yet standard mock cannot
+# be used because of the problem described in pyfakefs #182 and
+# mock issue 250 (https://github.com/testing-cabal/mock/issues/250).
+# Therefore just this file was forked from mox3 and incorporated
+# into pyfakefs.
+
+import inspect
+
+
+class StubOutForTesting(object):
+    """Sample Usage:
+
+       You want os.path.exists() to always return true during testing.
+
+       stubs = StubOutForTesting()
+       stubs.Set(os.path, 'exists', lambda x: 1)
+           ...
+       stubs.UnsetAll()
+
+       The above changes os.path.exists into a lambda that returns 1.    Once
+       the ... part of the code finishes, the UnsetAll() looks up the old value
+       of os.path.exists and restores it.
+
+    """
+
+    def __init__(self):
+        self.cache = []
+        self.stubs = []
+
+    def __del__(self):
+        self.SmartUnsetAll()
+        self.UnsetAll()
+
+    def SmartSet(self, obj, attr_name, new_attr):
+        """Replace obj.attr_name with new_attr.
+
+        This method is smart and works at the module, class, and instance level
+        while preserving proper inheritance. It will not stub out C types
+        however unless that has been explicitly allowed by the type.
+
+        This method supports the case where attr_name is a staticmethod or a
+        classmethod of obj.
+
+        Notes:
+          - If obj is an instance, then it is its class that will actually be
+            stubbed. Note that the method Set() does not do that: if obj is
+            an instance, it (and not its class) will be stubbed.
+          - The stubbing is using the builtin getattr and setattr. So, the
+            __get__ and __set__ will be called when stubbing (TODO: A better
+            idea would probably be to manipulate obj.__dict__ instead of
+            getattr() and setattr()).
+
+        Raises AttributeError if the attribute cannot be found.
+        """
+        if (inspect.ismodule(obj) or
+                (not inspect.isclass(obj) and attr_name in obj.__dict__)):
+            orig_obj = obj
+            orig_attr = getattr(obj, attr_name)
+
+        else:
+            if not inspect.isclass(obj):
+                mro = list(inspect.getmro(obj.__class__))
+            else:
+                mro = list(inspect.getmro(obj))
+
+            mro.reverse()
+
+            orig_attr = None
+
+            for cls in mro:
+                try:
+                    orig_obj = cls
+                    orig_attr = getattr(obj, attr_name)
+                except AttributeError:
+                    continue
+
+        if orig_attr is None:
+            raise AttributeError("Attribute not found.")
+
+        # Calling getattr() on a staticmethod transforms it to a 'normal'
+        # function. We need to ensure that we put it back as a staticmethod.
+        old_attribute = obj.__dict__.get(attr_name)
+        if (old_attribute is not None
+            and isinstance(old_attribute, staticmethod)):
+            orig_attr = staticmethod(orig_attr)
+
+        self.stubs.append((orig_obj, attr_name, orig_attr))
+        setattr(orig_obj, attr_name, new_attr)
+
+    def SmartUnsetAll(self):
+        """Reverses all the SmartSet() calls.
+
+        Restores things to their original definition. Its okay to call
+        SmartUnsetAll() repeatedly, as later calls have no effect if no
+        SmartSet() calls have been made.
+        """
+        self.stubs.reverse()
+
+        for args in self.stubs:
+            setattr(*args)
+
+        self.stubs = []
+
+    def Set(self, parent, child_name, new_child):
+        """Replace child_name's old definition with new_child.
+
+        Replace definition in the context of the given parent. The parent could
+        be a module when the child is a function at module scope. Or the parent
+        could be a class when a class' method is being replaced. The named
+        child is set to new_child, while the prior definition is saved away
+        for later, when UnsetAll() is called.
+
+        This method supports the case where child_name is a staticmethod or a
+        classmethod of parent.
+        """
+        old_child = getattr(parent, child_name)
+
+        old_attribute = parent.__dict__.get(child_name)
+        if old_attribute is not None:
+            if isinstance(old_attribute, staticmethod):
+                old_child = staticmethod(old_child)
+            elif isinstance(old_attribute, classmethod):
+                old_child = classmethod(old_child.__func__)
+
+        self.cache.append((parent, old_child, child_name))
+        setattr(parent, child_name, new_child)
+
+    def UnsetAll(self):
+        """Reverses all the Set() calls.
+
+        Restores things to their original definition. Its okay to call
+        UnsetAll() repeatedly, as later calls have no effect if no Set()
+        calls have been made.
+        """
+        # Undo calls to Set() in reverse order, in case Set() was called on the
+        # same arguments repeatedly (want the original call to be last one
+        # undone)
+        self.cache.reverse()
+
+        for (parent, old_child, child_name) in self.cache:
+            setattr(parent, child_name, old_child)
+        self.cache = []
diff --git a/requirements.txt b/requirements.txt
index 6fd1f50..bce6319 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,2 +1,2 @@
-mox3>=0.10.0
+unittest2; python_version < '2.7'
 pytest>=2.8.6
diff --git a/setup.py b/setup.py
index 0aae7bf..33371a5 100644
--- a/setup.py
+++ b/setup.py
@@ -22,7 +22,7 @@ from pyfakefs.fake_filesystem import __version__
 import os
 
 NAME = 'pyfakefs'
-REQUIRES = ['mox3']
+REQUIRES = []
 DESCRIPTION = 'pyfakefs implements a fake file system that mocks the Python file system modules.'
 
 URL = "http://pyfakefs.org"
diff --git a/tox.ini b/tox.ini
index 6072003..b6009d9 100644
--- a/tox.ini
+++ b/tox.ini
@@ -7,8 +7,3 @@ passenv = HOME USERPROFILE
 commands=
     python all_tests.py
     py.test pytest_plugin_test.py
-
-[testenv:py26]
-deps=
-    -rrequirements.txt
-    unittest2


Test patch:
diff --git a/all_tests.py b/all_tests.py
index 253f74a..b04cb79 100755
--- a/all_tests.py
+++ b/all_tests.py
@@ -26,6 +26,7 @@ import fake_filesystem_vs_real_test
 import fake_tempfile_test
 import fake_filesystem_unittest_test
 import example_test
+import mox3_stubout_test
 
 if sys.version_info >= (3, 4):
     import fake_pathlib_test
@@ -44,6 +45,7 @@ class AllTests(unittest.TestSuite):
             loader.loadTestsFromModule(fake_filesystem_vs_real_test),
             loader.loadTestsFromModule(fake_filesystem_unittest_test),
             loader.loadTestsFromModule(example_test),
+            loader.loadTestsFromModule(mox3_stubout_test),
         ])
         if sys.version_info >= (3, 4):
             self.addTests([
diff --git a/example_test.py b/example_test.py
index 7a5ef21..a3aa47e 100644
--- a/example_test.py
+++ b/example_test.py
@@ -62,7 +62,7 @@ class TestExample(fake_filesystem_unittest.TestCase):  # pylint: disable=R0904
           :py:class:`pyfakefs.fake_filesystem.FakeFilesystem`.  This is useful \
           for creating test files.
         * Attribute `self.stubs`, an instance of \
-          :py:class:`mox.stubout.StubOutForTesting`.  Use this if you need to
+          :py:class:`pyfakefs.mox3_stubout.StubOutForTesting`.  Use this if you need to
           define additional stubs.
         """
         self.setUpPyfakefs()
diff --git a/mox3_stubout_test.py b/mox3_stubout_test.py
new file mode 100644
index 0000000..3ac7527
--- /dev/null
+++ b/mox3_stubout_test.py
@@ -0,0 +1,136 @@
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Unit tests for mox3_stubout."""
+
+import datetime
+import math
+import os
+from os import path
+
+import sys
+
+if sys.version_info < (2, 7):
+    import unittest2 as unittest
+else:
+    import unittest
+
+import mox3_stubout_example
+from pyfakefs import mox3_stubout
+
+
+class NoPanicMath(object):
+    real_math = math
+
+    @staticmethod
+    def fabs(x):
+        return 42
+
+    def __getattr__(self, name):
+        """Forwards any unfaked calls to the standard module."""
+        return getattr(self.real_math, name)
+
+
+class ExistingPath(object):
+    real_path = path
+
+    @staticmethod
+    def exists(path):
+        return True
+
+    def __getattr__(self, name):
+        """Forwards any unfaked calls to the standard module."""
+        return getattr(self.real_path, name)
+
+
+class GroundhogDate(datetime.date):
+    @classmethod
+    def today(cls):
+        return datetime.date(1993, 2, 2)
+
+
+class StubOutForTestingTest(unittest.TestCase):
+    def setUp(self):
+        super(StubOutForTestingTest, self).setUp()
+        self.stubber = mox3_stubout.StubOutForTesting()
+
+    def testStuboutMethodWithSet(self):
+        non_existing_path = 'non_existing_path'
+        self.assertFalse(mox3_stubout_example.check_if_exists(non_existing_path))
+        self.stubber.Set(os.path, 'exists', lambda x: True)
+        self.assertTrue(mox3_stubout_example.check_if_exists(non_existing_path))
+        self.stubber.UnsetAll()
+        self.assertFalse(mox3_stubout_example.check_if_exists(non_existing_path))
+
+    def testStuboutClassWithSet(self):
+        self.assertGreater(mox3_stubout_example.tomorrow().year, 2000)
+
+        self.stubber.Set(datetime, 'date', GroundhogDate)
+        self.assertEqual(mox3_stubout_example.tomorrow(), datetime.date(1993, 2, 3))
+
+        self.stubber.UnsetAll()
+        self.assertGreater(mox3_stubout_example.tomorrow().year, 2000)
+
+    def testStuboutModuleWithSet(self):
+        self.assertEqual(10, mox3_stubout_example.fabs(-10))
+
+        self.stubber.Set(mox3_stubout_example, 'math', NoPanicMath)
+        self.assertEqual(42, mox3_stubout_example.fabs(-10))
+
+        self.stubber.UnsetAll()
+        self.assertEqual(10, mox3_stubout_example.fabs(-10))
+
+    def testSetRaiseIfUnknownAttribute(self):
+        self.assertRaises(AttributeError, self.stubber.Set, os.path, 'exists_not', lambda x: True)
+        self.assertRaises(AttributeError, self.stubber.Set, datetime, 'tomorrow', GroundhogDate)
+        self.assertRaises(AttributeError, self.stubber.Set, mox3_stubout_example, 'math1', NoPanicMath)
+
+    def testStuboutMethodWithSmartSet(self):
+        non_existing_path = 'non_existing_path'
+        self.stubber.SmartSet(os.path, 'exists', lambda x: True)
+        self.assertTrue(mox3_stubout_example.check_if_exists(non_existing_path))
+        self.stubber.SmartUnsetAll()
+        self.assertFalse(mox3_stubout_example.check_if_exists(non_existing_path))
+
+    def testStuboutClassWithSmartSet(self):
+        self.stubber.SmartSet(datetime, 'date', GroundhogDate)
+        self.assertEqual(mox3_stubout_example.tomorrow(), datetime.date(1993, 2, 3))
+
+        self.stubber.SmartUnsetAll()
+        self.assertGreater(mox3_stubout_example.tomorrow().year, 2000)
+
+    def testStuboutModuleWithSmartSet(self):
+        self.assertEqual(10, mox3_stubout_example.fabs(-10))
+
+        self.stubber.SmartSet(mox3_stubout_example, 'math', NoPanicMath)
+        self.assertEqual(42, mox3_stubout_example.fabs(-10))
+
+        self.stubber.SmartUnsetAll()
+        self.assertEqual(10, mox3_stubout_example.fabs(-10))
+
+    def testStuboutSubModuleWithSmartSet(self):
+        # this one does not work with Set
+        non_existing_path = 'non_existing_path'
+        self.assertFalse(mox3_stubout_example.check_if_exists(non_existing_path))
+        self.stubber.SmartSet(os, 'path', ExistingPath)
+        self.assertTrue(mox3_stubout_example.check_if_exists(non_existing_path))
+        self.stubber.SmartUnsetAll()
+        self.assertFalse(mox3_stubout_example.check_if_exists(non_existing_path))
+
+    def testSmartSetRaiseIfUnknownAttribute(self):
+        self.assertRaises(AttributeError, self.stubber.SmartSet, os.path, 'exists_not', lambda x: True)
+        self.assertRaises(AttributeError, self.stubber.SmartSet, datetime, 'tomorrow', GroundhogDate)
+        self.assertRaises(AttributeError, self.stubber.SmartSet, mox3_stubout_example, 'math1', NoPanicMath)
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/pyfakefs/fake_filesystem_unittest.py b/pyfakefs/fake_filesystem_unittest.py
index cb92ff1..1b08fa1 100644
--- a/pyfakefs/fake_filesystem_unittest.py
+++ b/pyfakefs/fake_filesystem_unittest.py
@@ -43,11 +43,10 @@ import sys
 import doctest
 import inspect
 
-import mox3.stubout
-
 from pyfakefs import fake_filesystem
 from pyfakefs import fake_filesystem_shutil
 from pyfakefs import fake_tempfile
+from pyfakefs import mox3_stubout
 
 if sys.version_info >= (3, 4):
     from pyfakefs import fake_pathlib
@@ -318,7 +317,7 @@ class Patcher(object):
         """Renew the fake file system and set the _isStale flag to `False`."""
         if self._stubs is not None:
             self._stubs.SmartUnsetAll()
-        self._stubs = mox3.stubout.StubOutForTesting()
+        self._stubs = mox3_stubout.StubOutForTesting()
 
         self.fs = fake_filesystem.FakeFilesystem()
         self.fake_os = fake_filesystem.FakeOsModule(self.fs)