From afc98d4e4eab7d66b09483fb3da1ac30e766d026 Mon Sep 17 00:00:00 2001
From: Michael Foord <michael@voidspace.org.uk>
Date: Tue, 13 Mar 2012 16:38:50 -0700
Subject: [PATCH] Adding mock_open helper

---
 docs/changelog.txt   |  3 +++
 mock.py              | 39 +++++++++++++++++++++++++++++++++--
 tests/testhelpers.py | 48 ++++++++++++++++++++++++++++++++++++++++++--
 tests/testmock.py    | 12 +++++++++++
 4 files changed, 98 insertions(+), 4 deletions(-)

diff --git a/docs/changelog.txt b/docs/changelog.txt
index 90c1ed3..015eda0 100644
--- a/docs/changelog.txt
+++ b/docs/changelog.txt
@@ -10,6 +10,9 @@ CHANGELOG
 The standard library version!
 
 * `mocksignature`, along with the `mocksignature` argument to `patch`, removed
+* Support for deleting attributes (accessing deleted attributes will raise an
+  `AttributeError`)
+* Added the `mock_open` helper function for mocking open as a context manager
 
 
 2012/02/13 Version 0.8.0
diff --git a/mock.py b/mock.py
index 640203b..b6ef6f2 100644
--- a/mock.py
+++ b/mock.py
@@ -25,6 +25,7 @@ __all__ = (
     'FILTER_DIR',
     'NonCallableMock',
     'NonCallableMagicMock',
+    'mock_open',
 )
 
 
@@ -335,6 +336,8 @@ class _Sentinel(object):
 sentinel = _Sentinel()
 
 DEFAULT = sentinel.DEFAULT
+_missing = sentinel.MISSING
+_deleted = sentinel.DELETED
 
 
 class OldStyleClass:
@@ -630,7 +633,9 @@ class NonCallableMock(Base):
             raise AttributeError(name)
 
         result = self._mock_children.get(name)
-        if result is None:
+        if result is _deleted:
+            raise AttributeError(name)
+        elif result is None:
             wraps = None
             if self._mock_wraps is not None:
                 # XXXX should we get the attribute without triggering code
@@ -757,7 +762,16 @@ class NonCallableMock(Base):
                 # not set on the instance itself
                 return
 
-        return object.__delattr__(self, name)
+        if name in self.__dict__:
+            object.__delattr__(self, name)
+
+        obj = self._mock_children.get(name, _missing)
+        if obj is _deleted:
+            raise AttributeError(name)
+        if obj is not _missing:
+            del self._mock_children[name]
+        self._mock_children[name] = _deleted
+
 
 
     def _format_mock_call_signature(self, args, kwargs):
@@ -2201,3 +2215,24 @@ FunctionAttributes = set([
     'func_globals',
     'func_name',
 ])
+
+if inPy3k:
+    import _io
+    file_spec = list(set(dir(_io.TextIOWrapper)).union(set(dir(_io.BytesIO))))
+else:
+    file_spec = file
+
+
+def mock_open(mock=None, read_data=None):
+    if mock is None:
+        mock = MagicMock(spec=file_spec)
+
+    handle = MagicMock(spec=file_spec)
+    handle.write.return_value = None
+    handle.__enter__.return_value = handle
+
+    if read_data is not None:
+        handle.read.return_value = read_data
+
+    mock.return_value = handle
+    return mock
diff --git a/tests/testhelpers.py b/tests/testhelpers.py
index 1b3dd2f..1b04c88 100644
--- a/tests/testhelpers.py
+++ b/tests/testhelpers.py
@@ -5,8 +5,8 @@
 from tests.support import unittest2, inPy3k
 
 from mock import (
-    call, _Call, create_autospec,
-    MagicMock, Mock, ANY, _CallList
+    call, _Call, create_autospec, MagicMock,
+    Mock, ANY, _CallList, mock_open, patch
 )
 
 from datetime import datetime
@@ -852,5 +852,49 @@ class TestCallList(unittest2.TestCase):
         self.assertEqual(str(mock.mock_calls), expected)
 
 
+
+class TestMockOpen(unittest2.TestCase):
+
+    def test_mock_open(self):
+        mock = mock_open()
+        with patch('%s.open' % __name__, mock, create=True) as patched:
+            self.assertIs(patched, mock)
+            open('foo')
+
+        mock.assert_called_once_with('foo')
+
+
+    def test_mock_open_context_manager(self):
+        mock = mock_open()
+        handle = mock.return_value
+        with patch('%s.open' % __name__, mock, create=True):
+            with open('foo') as f:
+                f.read()
+
+        expected_calls = [call('foo'), call().__enter__(), call().read(),
+                          call().__exit__(None, None, None)]
+        self.assertEqual(mock.mock_calls, expected_calls)
+        self.assertIs(f, handle)
+
+
+    def test_explicit_mock(self):
+        mock = MagicMock()
+        mock_open(mock)
+
+        with patch('%s.open' % __name__, mock, create=True) as patched:
+            self.assertIs(patched, mock)
+            open('foo')
+
+        mock.assert_called_once_with('foo')
+
+
+    def test_read_data(self):
+        mock = mock_open(read_data='foo')
+        with patch('%s.open' % __name__, mock, create=True):
+            h = open('bar')
+            result = h.read()
+
+        self.assertEqual(result, 'foo')
+
 if __name__ == '__main__':
     unittest2.main()
diff --git a/tests/testmock.py b/tests/testmock.py
index 886f9bf..9e9c4a1 100644
--- a/tests/testmock.py
+++ b/tests/testmock.py
@@ -1291,5 +1291,17 @@ class MockTest(unittest2.TestCase):
                 self.assertEqual(m.mock_calls, call().foo().call_list())
 
 
+    def test_attribute_deletion(self):
+        for mock in Mock(), MagicMock():
+            self.assertTrue(hasattr(mock, 'm'))
+
+            del mock.m
+            self.assertFalse(hasattr(mock, 'm'))
+
+            del mock.f
+            self.assertFalse(hasattr(mock, 'f'))
+            self.assertRaises(AttributeError, getattr, mock, 'f')
+
+
 if __name__ == '__main__':
     unittest2.main()
-- 
GitLab