]> git.ipfire.org Git - thirdparty/tornado.git/commitdiff
Add a mock.patch-compatible wrapper for options objects.
authorBen Darnell <ben@bendarnell.com>
Sat, 1 Dec 2012 20:50:40 +0000 (15:50 -0500)
committerBen Darnell <ben@bendarnell.com>
Sat, 1 Dec 2012 20:50:40 +0000 (15:50 -0500)
maint/requirements.txt
tornado/options.py
tornado/test/options_test.py
tox.ini
website/sphinx/releases/next.rst

index 7ec3c490e36716d88a5cb1273b36cfaa6d321405..7e0b1ab478d514050c510ac1a441b188cdc56426 100644 (file)
@@ -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
index 83c3bac9f196bdc05a7bc166a0467ce8cfeec498..ac1f07c19cbf6f094842e8b55334efe6d9888993 100644 (file)
@@ -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,
index d29a813b5b42ef9523cad13b9fded5183e0f9a48..c774dd8ffefabaf6002d9fec78cad14c610a7e6f 100644 (file)
@@ -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 4367b572546547746f77cf8eff1732b0b4d373fa..714981e60df4df00efb9e01f263f3df600103118 100644 (file)
--- 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
index 7a2d997f4b8bdc166ed04710b5bf948cf39a597b..1e3cf40d9cd573a772e66eacc909ef1830976f53 100644 (file)
@@ -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`.