Python Programming, news on the Voidspace Python Projects and all things techie.
Simple mocking of open as a context manager
Using open as a context manager is a great way to ensure your file handles are closed properly and is becoming common:
with open('/some/path', 'w') as f: f.write('something')
The issue is that even if you mock out the call to open it is the returned object that is used as a context manager (and has __enter__ and __exit__ called).
Using MagicMock from the mock library, we can mock out context managers very simply. However, mocking open is fiddly enough that a helper function is useful. Here mock_open creates and configures a MagicMock that behaves as a file context manager.
from mock import inPy3k, MagicMock if inPy3k: file_spec = ['_CHUNK_SIZE', '__enter__', '__eq__', '__exit__', '__format__', '__ge__', '__gt__', '__hash__', '__iter__', '__le__', '__lt__', '__ne__', '__next__', '__repr__', '__str__', '_checkClosed', '_checkReadable', '_checkSeekable', '_checkWritable', 'buffer', 'close', 'closed', 'detach', 'encoding', 'errors', 'fileno', 'flush', 'isatty', 'line_buffering', 'mode', 'name', 'newlines', 'peek', 'raw', 'read', 'read1', 'readable', 'readinto', 'readline', 'readlines', 'seek', 'seekable', 'tell', 'truncate', 'writable', 'write', 'writelines'] else: file_spec = file def mock_open(mock=None, data=None): if mock is None: mock = MagicMock(spec=file_spec) handle = MagicMock(spec=file_spec) handle.write.return_value = None if data is None: handle.__enter__.return_value = handle else: handle.__enter__.return_value = data mock.return_value = handle return mock
>>> m = mock_open() >>> with patch('__main__.open', m, create=True): ... with open('foo', 'w') as h: ... h.write('some stuff') ... >>> m.assert_called_once_with('foo', 'w') >>> m.mock_calls [call('foo', 'w'), call().__enter__(), call().write('some stuff'), call().__exit__(None, None, None)] >>> handle = m() >>> handle.write.assert_called_once_with('some stuff')
And for reading files, using a StringIO to represent the file handle:
>>> from StringIO import StringIO >>> m = mock_open(data=StringIO('foo bar baz')) >>> with patch('__main__.open', m, create=True): ... with open('foo') as h: ... result = h.read() ... >>> m.assert_called_once_with('foo') >>> assert result == 'foo bar baz'
Note that the StringIO will only be used for the data if open is used as a context manager. If you just configure and use mocks they will work whichever way open is used.
This helper function will be built into mock 0.9.
Mocks with some attributes not present
Mock objects, from the mock library, create attributes on demand. This allows them to pretend to be objects of any type.
What mock isn't so good at is pretending not to have attributes. You may want a mock object to return False to a hasattr call, or raise an AttributeError when an attribute is fetched. You can do this by providing an object as a spec for a mock, but that isn't always convenient.
Below is a subclass of Mock that allows you to "block" attributes by deleting them. Once deleted, accessing an attribute will raise an AttributeError.
from mock import Mock deleted = object() missing = object() class DeletingMock(Mock): def __delattr__(self, attr): if attr in self.__dict__: return super(DeletingMock, self).__delattr__(attr) obj = self._mock_children.get(attr, missing) if obj is deleted: raise AttributeError(attr) if obj is not missing: del self._mock_children[attr] self._mock_children[attr] = deleted def __getattr__(self, attr): result = super(DeletingMock, self).__getattr__(attr) if result is deleted: raise AttributeError(attr) return result
>>> mock = DeletingMock() >>> hasattr(mock, 'm') True >>> del mock.m >>> hasattr(mock, 'm') False >>> del mock.f >>> mock.f Traceback (most recent call last): ... AttributeError: f
This functionality will probably be built into 0.9, or whichever version of mock comes after 0.8...
mock 0.8rc2: new release and development docs
I've pushed out a new release of mock. This fixes an inconsistency in the create_autospec api I discovered whilst working on the docs (yes I've really been working on the docs), and a fix for a bug with using ANY. (Thanks to Tom Davis for reporting this.)
You can download mock 0.8rc2 or install it with:
pip install -U mock==dev
There are two changes in 0.8rc2:
- Removed the configure keyword argument to create_autospec and allow arbitrary keyword arguments (for the Mock constructor) instead
- Fixed ANY equality with some types in assert_called_with calls
Unfortunately, if you were using the configure argument to create_autospec then you'll have to change code when you update to 0.8rc2, sorry. The good news is that there's now some documentation for the new features - including for create_autospec.
I've now switched mock to using one of the standard Sphinx themes, and the development documentation (updated automatically on every commit) is available at mock.readthedocs.org.
This work is licensed under a Creative Commons Attribution-Share Alike 2.0 License.