From: Ben Darnell Date: Sat, 1 Dec 2012 20:50:40 +0000 (-0500) Subject: Add a mock.patch-compatible wrapper for options objects. X-Git-Tag: v3.0.0~209 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=e4ebf8be144006f91e9395cda4aac72702f8ffd0;p=thirdparty%2Ftornado.git Add a mock.patch-compatible wrapper for options objects. --- diff --git a/maint/requirements.txt b/maint/requirements.txt index 7ec3c490e..7e0b1ab47 100644 --- a/maint/requirements.txt +++ b/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 diff --git a/tornado/options.py b/tornado/options.py index 83c3bac9f..ac1f07c19 100644 --- a/tornado/options.py +++ b/tornado/options.py @@ -256,6 +256,49 @@ class OptionParser(object): 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, diff --git a/tornado/test/options_test.py b/tornado/test/options_test.py index d29a813b5..c774dd8ff 100644 --- a/tornado/test/options_test.py +++ b/tornado/test/options_test.py @@ -10,6 +10,14 @@ try: 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 @@ class OptionsTest(unittest.TestCase): 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) diff --git a/tox.ini b/tox.ini index 4367b5725..714981e60 100644 --- a/tox.ini +++ b/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,6 +157,7 @@ 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:} @@ -153,3 +165,5 @@ commands = python -O -m tornado.test.runtests {posargs:} [testenv:py32-opt] basepython = python3.2 commands = python -O -m tornado.test.runtests {posargs:} +deps = + mock diff --git a/website/sphinx/releases/next.rst b/website/sphinx/releases/next.rst index 7a2d997f4..1e3cf40d9 100644 --- a/website/sphinx/releases/next.rst +++ b/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`.