Browse files

Add a mock.patch-compatible wrapper for options objects.

  • Loading branch information...
1 parent 8c5ad60 commit e4ebf8be144006f91e9395cda4aac72702f8ffd0 @bdarnell bdarnell committed Dec 1, 2012
Showing with 114 additions and 2 deletions.
  1. +1 −0 maint/requirements.txt
  2. +43 −0 tornado/options.py
  3. +51 −0 tornado/test/options_test.py
  4. +16 −2 tox.ini
  5. +3 −0 website/sphinx/releases/next.rst
View
1 maint/requirements.txt
@@ -3,6 +3,7 @@
# Tornado's optional dependencies
Twisted==12.2.0
futures==2.1.3
+mock==1.0.1
pycurl==7.19.0
# Other useful tools
View
43 tornado/options.py
@@ -256,6 +256,49 @@ def run_parse_callbacks(self):
for callback in self._parse_callbacks:
callback()
+ def mockable(self):
+ """Returns a wrapper around self that is compatible with `mock.patch`.
+
+ The `mock.patch` function (included in the standard library
+ `unittest.mock` package since Python 3.3, or in the
+ third-party `mock` package for older versions of Python) is
+ incompatible with objects like ``options`` that override
+ ``__getattr__`` and ``__setattr__``. This function returns an
+ object that can be used with `mock.patch.object` to modify
+ option values::
+
+ with mock.patch.object(options.mockable(), 'name', value):
+ assert options.name == value
+ """
+ return _Mockable(self)
+
+class _Mockable(object):
+ """`mock.patch` compatible wrapper for `OptionParser`.
+
+ As of ``mock`` version 1.0.1, when an object uses ``__getattr__``
+ hooks instead of ``__dict__``, ``patch.__exit__`` tries to delete
+ the attribute it set instead of setting a new one (assuming that
+ the object does not catpure ``__setattr__``, so the patch
+ created a new attribute in ``__dict__``).
+
+ _Mockable's getattr and setattr pass through to the underlying
+ OptionParser, and delattr undoes the effect of a previous setattr.
+ """
+ def __init__(self, options):
+ # Modify __dict__ directly to bypass __setattr__
+ self.__dict__['_options'] = options
+ self.__dict__['_originals'] = {}
+
+ def __getattr__(self, name):
+ return getattr(self._options, name)
+
+ def __setattr__(self, name, value):
+ assert name not in self._originals, "don't reuse mockable objects"
+ self._originals[name] = getattr(self._options, name)
+ setattr(self._options, name, value)
+
+ def __delattr__(self, name):
+ setattr(self._options, name, self._originals.pop(name))
class _Option(object):
def __init__(self, name, default=None, type=basestring, help=None,
View
51 tornado/test/options_test.py
@@ -10,6 +10,14 @@
except ImportError:
from io import StringIO # python 3
+try:
+ from unittest import mock # python 3.3
+except ImportError:
+ try:
+ import mock # third-party mock package
+ except ImportError:
+ mock = None
+
class OptionsTest(unittest.TestCase):
def test_parse_command_line(self):
options = OptionParser()
@@ -71,3 +79,46 @@ def test_subcommand(self):
sub_options.parse_command_line(["subcommand", "--verbose"])
finally:
sys.stderr = orig_stderr
+
+ def test_setattr(self):
+ options = OptionParser()
+ options.define('foo', default=1, type=int)
+ options.foo = 2
+ self.assertEqual(options.foo, 2)
+
+ def test_setattr_type_check(self):
+ # setattr requires that options be the right type and doesn't
+ # parse from string formats.
+ options = OptionParser()
+ options.define('foo', default=1, type=int)
+ with self.assertRaises(Error):
+ options.foo = '2'
+
+ def test_setattr_with_callback(self):
+ values = []
+ options = OptionParser()
+ options.define('foo', default=1, type=int, callback=values.append)
+ options.foo = 2
+ self.assertEqual(values, [2])
+
+ @unittest.skipIf(mock is None, 'mock package not present')
+ def test_mock_patch(self):
+ # ensure that our setattr hooks don't interfere with mock.patch
+ options = OptionParser()
+ options.define('foo', default=1)
+ options.parse_command_line(['main.py', '--foo=2'])
+ self.assertEqual(options.foo, 2)
+
+ with mock.patch.object(options.mockable(), 'foo', 3):
+ self.assertEqual(options.foo, 3)
+ self.assertEqual(options.foo, 2)
+
+ # Try nested patches mixed with explicit sets
+ with mock.patch.object(options.mockable(), 'foo', 4):
+ self.assertEqual(options.foo, 4)
+ options.foo = 5
+ self.assertEqual(options.foo, 5)
+ with mock.patch.object(options.mockable(), 'foo', 6):
+ self.assertEqual(options.foo, 6)
+ self.assertEqual(options.foo, 5)
+ self.assertEqual(options.foo, 2)
View
18 tox.ini
@@ -11,7 +11,7 @@
[tox]
# "-full" variants include optional dependencies, to ensure
# that things work both in a bare install and with all the extras.
-envlist = py27-full, py27-curl, py25-full, py32, pypy, py25, py26, py26-full, py27, py32-utf8, py33, py27-opt, py32-opt, pypy-full, py27-select, py27-monotonic, py33-monotonic, py27-twisted
+envlist = py27-full, py27-curl, py25-full, py32-full, pypy, py25, py26, py26-full, py27, py32, py32-utf8, py33, py27-opt, py32-opt, pypy-full, py27-select, py27-monotonic, py33-monotonic, py27-twisted
[testenv]
commands = python -m tornado.test.runtests {posargs:}
@@ -34,6 +34,7 @@ deps =
basepython = python2.5
deps =
futures
+ mock
pycurl
simplejson
# twisted is dropping python 2.5 support in 12.2.0
@@ -52,6 +53,7 @@ deps = unittest2
basepython = python2.6
deps =
futures
+ mock
pycurl
twisted==11.0.0
unittest2
@@ -60,6 +62,7 @@ deps =
basepython = python2.7
deps =
futures
+ mock
pycurl
twisted>=12.0.0
@@ -70,6 +73,7 @@ deps =
basepython = python2.7
deps =
futures
+ mock
pycurl
twisted>=11.1.0
commands = python -m tornado.test.runtests --httpclient=tornado.curl_httpclient.CurlAsyncHTTPClient {posargs:}
@@ -81,6 +85,7 @@ commands = python -m tornado.test.runtests --httpclient=tornado.curl_httpclient.
basepython = python2.7
deps =
futures
+ mock
pycurl
twisted>=12.0.0
commands = python -m tornado.test.runtests --ioloop=tornado.platform.select.SelectIOLoop {posargs:}
@@ -89,6 +94,7 @@ commands = python -m tornado.test.runtests --ioloop=tornado.platform.select.Sele
basepython = python2.7
deps =
futures
+ mock
pycurl
twisted>=12.2.0
commands = python -m tornado.test.runtests --ioloop=tornado.platform.twisted.TwistedIOLoop {posargs:}
@@ -99,6 +105,7 @@ basepython = python2.7
deps =
http://pypi.python.org/packages/source/M/Monotime/Monotime-1.0.tar.gz
futures
+ mock
pycurl
twisted
commands = python -m tornado.test.runtests --ioloop_time_monotonic {posargs:}
@@ -113,6 +120,7 @@ commands = python -m tornado.test.runtests --ioloop_time_monotonic {posargs:}
basepython = pypy
deps =
futures
+ mock
# In python 3, opening files in text mode uses a system-dependent encoding by
# default. Run the tests with "C" (ascii) and "utf-8" locales to ensure
@@ -128,7 +136,10 @@ commands = python -bb -m tornado.test.runtests {posargs:}
basepython = python3.2
setenv = LANG=en_US.utf-8
-# No py32-full yet: none of our dependencies currently work on python3.
+[testenv:py32-full]
+basepython = python3.2
+deps =
+ mock
[testenv:py33]
# tox doesn't yet know "py33" by default
@@ -146,10 +157,13 @@ commands = python -m tornado.test.runtests --ioloop_time_monotonic {posargs:}
basepython = python2.7
deps =
futures
+ mock
pycurl
twisted>=12.0.0
commands = python -O -m tornado.test.runtests {posargs:}
[testenv:py32-opt]
basepython = python3.2
commands = python -O -m tornado.test.runtests {posargs:}
+deps =
+ mock
View
3 website/sphinx/releases/next.rst
@@ -186,3 +186,6 @@ In progress
* `tornado.web.ErrorHandler` no longer requires XSRF tokens on ``POST``
requests, so posts to an unknown url will always return 404 instead of
complaining about XSRF tokens.
+* `tornado.options.options` (and `OptionParser` instances generally) now
+ have a `mockable()` method that returns a wrapper object compatible with
+ `mock.patch`.

0 comments on commit e4ebf8b

Please sign in to comment.