# HG changeset patch # User Todd Larsen # Date 1219785749 0 # Node ID 5a2786fd5048639202e52a2c4fcbfa6fb769eb65 # Parent 094c8f741a082bdc64572db1525fb181d3cdc38c Add mocker 0.10.1 to trunk/thirdparty, so that it can be used to create mocks for testing the Google App Engine app itself. See also: http://blog.appenginefan.com/2008/06/unit-tests-for-google-app-engine-apps.html Patch by: Todd Larsen Review by: to-be-reviewed diff -r 094c8f741a08 -r 5a2786fd5048 thirdparty/mocker/LICENSE --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/thirdparty/mocker/LICENSE Tue Aug 26 21:22:29 2008 +0000 @@ -0,0 +1,259 @@ +A. HISTORY OF THE SOFTWARE +========================== + +Python was created in the early 1990s by Guido van Rossum at Stichting +Mathematisch Centrum (CWI, see http://www.cwi.nl) in the Netherlands +as a successor of a language called ABC. Guido remains Python's +principal author, although it includes many contributions from others. + +In 1995, Guido continued his work on Python at the Corporation for +National Research Initiatives (CNRI, see http://www.cnri.reston.va.us) +in Reston, Virginia where he released several versions of the +software. + +In May 2000, Guido and the Python core development team moved to +BeOpen.com to form the BeOpen PythonLabs team. In October of the same +year, the PythonLabs team moved to Digital Creations (now Zope +Corporation, see http://www.zope.com). In 2001, the Python Software +Foundation (PSF, see http://www.python.org/psf/) was formed, a +non-profit organization created specifically to own Python-related +Intellectual Property. Zope Corporation is a sponsoring member of +the PSF. + +All Python releases are Open Source (see http://www.opensource.org for +the Open Source Definition). Historically, most, but not all, Python +releases have also been GPL-compatible; the table below summarizes +the various releases. + + Release Derived Year Owner GPL- + from compatible? (1) + + 0.9.0 thru 1.2 1991-1995 CWI yes + 1.3 thru 1.5.2 1.2 1995-1999 CNRI yes + 1.6 1.5.2 2000 CNRI no + 2.0 1.6 2000 BeOpen.com no + 1.6.1 1.6 2001 CNRI yes (2) + 2.1 2.0+1.6.1 2001 PSF no + 2.0.1 2.0+1.6.1 2001 PSF yes + 2.1.1 2.1+2.0.1 2001 PSF yes + 2.2 2.1.1 2001 PSF yes + 2.1.2 2.1.1 2002 PSF yes + 2.1.3 2.1.2 2002 PSF yes + 2.2.1 2.2 2002 PSF yes + 2.2.2 2.2.1 2002 PSF yes + 2.2.3 2.2.2 2003 PSF yes + 2.3 2.2.2 2002-2003 PSF yes + +Footnotes: + +(1) GPL-compatible doesn't mean that we're distributing Python under + the GPL. All Python licenses, unlike the GPL, let you distribute + a modified version without making your changes open source. The + GPL-compatible licenses make it possible to combine Python with + other software that is released under the GPL; the others don't. + +(2) According to Richard Stallman, 1.6.1 is not GPL-compatible, + because its license has a choice of law clause. According to + CNRI, however, Stallman's lawyer has told CNRI's lawyer that 1.6.1 + is "not incompatible" with the GPL. + +Thanks to the many outside volunteers who have worked under Guido's +direction to make these releases possible. + + +B. TERMS AND CONDITIONS FOR ACCESSING OR OTHERWISE USING PYTHON +=============================================================== + +PSF LICENSE AGREEMENT FOR PYTHON 2.3 +------------------------------------ + +1. This LICENSE AGREEMENT is between the Python Software Foundation +("PSF"), and the Individual or Organization ("Licensee") accessing and +otherwise using Python 2.3 software in source or binary form and its +associated documentation. + +2. Subject to the terms and conditions of this License Agreement, PSF +hereby grants Licensee a nonexclusive, royalty-free, world-wide +license to reproduce, analyze, test, perform and/or display publicly, +prepare derivative works, distribute, and otherwise use Python 2.3 +alone or in any derivative version, provided, however, that PSF's +License Agreement and PSF's notice of copyright, i.e., "Copyright (c) +2001, 2002, 2003 Python Software Foundation; All Rights Reserved" are +retained in Python 2.3 alone or in any derivative version prepared by +Licensee. + +3. In the event Licensee prepares a derivative work that is based on +or incorporates Python 2.3 or any part thereof, and wants to make +the derivative work available to others as provided herein, then +Licensee hereby agrees to include in any such work a brief summary of +the changes made to Python 2.3. + +4. PSF is making Python 2.3 available to Licensee on an "AS IS" +basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND +DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON 2.3 WILL NOT +INFRINGE ANY THIRD PARTY RIGHTS. + +5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON +2.3 FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS +A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON 2.3, +OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +6. This License Agreement will automatically terminate upon a material +breach of its terms and conditions. + +7. Nothing in this License Agreement shall be deemed to create any +relationship of agency, partnership, or joint venture between PSF and +Licensee. This License Agreement does not grant permission to use PSF +trademarks or trade name in a trademark sense to endorse or promote +products or services of Licensee, or any third party. + +8. By copying, installing or otherwise using Python 2.3, Licensee +agrees to be bound by the terms and conditions of this License +Agreement. + + +BEOPEN.COM LICENSE AGREEMENT FOR PYTHON 2.0 +------------------------------------------- + +BEOPEN PYTHON OPEN SOURCE LICENSE AGREEMENT VERSION 1 + +1. This LICENSE AGREEMENT is between BeOpen.com ("BeOpen"), having an +office at 160 Saratoga Avenue, Santa Clara, CA 95051, and the +Individual or Organization ("Licensee") accessing and otherwise using +this software in source or binary form and its associated +documentation ("the Software"). + +2. Subject to the terms and conditions of this BeOpen Python License +Agreement, BeOpen hereby grants Licensee a non-exclusive, +royalty-free, world-wide license to reproduce, analyze, test, perform +and/or display publicly, prepare derivative works, distribute, and +otherwise use the Software alone or in any derivative version, +provided, however, that the BeOpen Python License is retained in the +Software, alone or in any derivative version prepared by Licensee. + +3. BeOpen is making the Software available to Licensee on an "AS IS" +basis. BEOPEN MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, BEOPEN MAKES NO AND +DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF THE SOFTWARE WILL NOT +INFRINGE ANY THIRD PARTY RIGHTS. + +4. BEOPEN SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF THE +SOFTWARE FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS +AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THE SOFTWARE, OR ANY +DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +5. This License Agreement will automatically terminate upon a material +breach of its terms and conditions. + +6. This License Agreement shall be governed by and interpreted in all +respects by the law of the State of California, excluding conflict of +law provisions. Nothing in this License Agreement shall be deemed to +create any relationship of agency, partnership, or joint venture +between BeOpen and Licensee. This License Agreement does not grant +permission to use BeOpen trademarks or trade names in a trademark +sense to endorse or promote products or services of Licensee, or any +third party. As an exception, the "BeOpen Python" logos available at +http://www.pythonlabs.com/logos.html may be used according to the +permissions granted on that web page. + +7. By copying, installing or otherwise using the software, Licensee +agrees to be bound by the terms and conditions of this License +Agreement. + + +CNRI LICENSE AGREEMENT FOR PYTHON 1.6.1 +--------------------------------------- + +1. This LICENSE AGREEMENT is between the Corporation for National +Research Initiatives, having an office at 1895 Preston White Drive, +Reston, VA 20191 ("CNRI"), and the Individual or Organization +("Licensee") accessing and otherwise using Python 1.6.1 software in +source or binary form and its associated documentation. + +2. Subject to the terms and conditions of this License Agreement, CNRI +hereby grants Licensee a nonexclusive, royalty-free, world-wide +license to reproduce, analyze, test, perform and/or display publicly, +prepare derivative works, distribute, and otherwise use Python 1.6.1 +alone or in any derivative version, provided, however, that CNRI's +License Agreement and CNRI's notice of copyright, i.e., "Copyright (c) +1995-2001 Corporation for National Research Initiatives; All Rights +Reserved" are retained in Python 1.6.1 alone or in any derivative +version prepared by Licensee. Alternately, in lieu of CNRI's License +Agreement, Licensee may substitute the following text (omitting the +quotes): "Python 1.6.1 is made available subject to the terms and +conditions in CNRI's License Agreement. This Agreement together with +Python 1.6.1 may be located on the Internet using the following +unique, persistent identifier (known as a handle): 1895.22/1013. This +Agreement may also be obtained from a proxy server on the Internet +using the following URL: http://hdl.handle.net/1895.22/1013". + +3. In the event Licensee prepares a derivative work that is based on +or incorporates Python 1.6.1 or any part thereof, and wants to make +the derivative work available to others as provided herein, then +Licensee hereby agrees to include in any such work a brief summary of +the changes made to Python 1.6.1. + +4. CNRI is making Python 1.6.1 available to Licensee on an "AS IS" +basis. CNRI MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, CNRI MAKES NO AND +DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON 1.6.1 WILL NOT +INFRINGE ANY THIRD PARTY RIGHTS. + +5. CNRI SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON +1.6.1 FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS +A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON 1.6.1, +OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +6. This License Agreement will automatically terminate upon a material +breach of its terms and conditions. + +7. This License Agreement shall be governed by the federal +intellectual property law of the United States, including without +limitation the federal copyright law, and, to the extent such +U.S. federal law does not apply, by the law of the Commonwealth of +Virginia, excluding Virginia's conflict of law provisions. +Notwithstanding the foregoing, with regard to derivative works based +on Python 1.6.1 that incorporate non-separable material that was +previously distributed under the GNU General Public License (GPL), the +law of the Commonwealth of Virginia shall govern this License +Agreement only as to issues arising under or with respect to +Paragraphs 4, 5, and 7 of this License Agreement. Nothing in this +License Agreement shall be deemed to create any relationship of +agency, partnership, or joint venture between CNRI and Licensee. This +License Agreement does not grant permission to use CNRI trademarks or +trade name in a trademark sense to endorse or promote products or +services of Licensee, or any third party. + +8. By clicking on the "ACCEPT" button where indicated, or by copying, +installing or otherwise using Python 1.6.1, Licensee agrees to be +bound by the terms and conditions of this License Agreement. + + ACCEPT + + +CWI LICENSE AGREEMENT FOR PYTHON 0.9.0 THROUGH 1.2 +-------------------------------------------------- + +Copyright (c) 1991 - 1995, Stichting Mathematisch Centrum Amsterdam, +The Netherlands. All rights reserved. + +Permission to use, copy, modify, and distribute this software and its +documentation for any purpose and without fee is hereby granted, +provided that the above copyright notice appear in all copies and that +both that copyright notice and this permission notice appear in +supporting documentation, and that the name of Stichting Mathematisch +Centrum or CWI not be used in advertising or publicity pertaining to +distribution of the software without specific, written prior +permission. + +STICHTING MATHEMATISCH CENTRUM DISCLAIMS ALL WARRANTIES WITH REGARD TO +THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS, IN NO EVENT SHALL STICHTING MATHEMATISCH CENTRUM BE LIABLE +FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff -r 094c8f741a08 -r 5a2786fd5048 thirdparty/mocker/__init__.py diff -r 094c8f741a08 -r 5a2786fd5048 thirdparty/mocker/mocker.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/thirdparty/mocker/mocker.py Tue Aug 26 21:22:29 2008 +0000 @@ -0,0 +1,2068 @@ +""" +Copyright (c) 2007 Gustavo Niemeyer + +Graceful platform for test doubles in Python (mocks, stubs, fakes, and dummies). +""" +import __builtin__ +import tempfile +import unittest +import inspect +import shutil +import types +import sys +import os +import gc + + +if sys.version_info < (2, 4): + from sets import Set as set # pragma: nocover + + +__all__ = ["Mocker", "expect", "IS", "CONTAINS", "IN", "MATCH", + "ANY", "ARGS", "KWARGS"] + + +__author__ = "Gustavo Niemeyer " +__license__ = "PSF License" +__version__ = "0.10.1" + + +ERROR_PREFIX = "[Mocker] " + + +# -------------------------------------------------------------------- +# Exceptions + +class MatchError(AssertionError): + """Raised when an unknown expression is seen in playback mode.""" + + +# -------------------------------------------------------------------- +# Helper for chained-style calling. + +class expect(object): + """This is a simple helper that allows a different call-style. + + With this class one can comfortably do chaining of calls to the + mocker object responsible by the object being handled. For instance:: + + expect(obj.attr).result(3).count(1, 2) + + Is the same as:: + + obj.attr + mocker.result(3) + mocker.count(1, 2) + + """ + + def __init__(self, mock, attr=None): + self._mock = mock + self._attr = attr + + def __getattr__(self, attr): + return self.__class__(self._mock, attr) + + def __call__(self, *args, **kwargs): + getattr(self._mock.__mocker__, self._attr)(*args, **kwargs) + return self + + +# -------------------------------------------------------------------- +# Extensions to Python's unittest. + +class MockerTestCase(unittest.TestCase): + """unittest.TestCase subclass with Mocker support. + + @ivar mocker: The mocker instance. + + This is a convenience only. Mocker may easily be used with the + standard C{unittest.TestCase} class if wanted. + + Test methods have a Mocker instance available on C{self.mocker}. + At the end of each test method, expectations of the mocker will + be verified, and any requested changes made to the environment + will be restored. + + In addition to the integration with Mocker, this class provides + a few additional helper methods. + """ + + expect = expect + + def __init__(self, methodName="runTest"): + # So here is the trick: we take the real test method, wrap it on + # a function that do the job we have to do, and insert it in the + # *instance* dictionary, so that getattr() will return our + # replacement rather than the class method. + test_method = getattr(self, methodName, None) + if test_method is not None: + def test_method_wrapper(): + try: + result = test_method() + except: + raise + else: + if (self.mocker.is_recording() and + self.mocker.get_events()): + raise RuntimeError("Mocker must be put in replay " + "mode with self.mocker.replay()") + if (hasattr(result, "addCallback") and + hasattr(result, "addErrback")): + def verify(result): + self.mocker.verify() + return result + result.addCallback(verify) + else: + self.mocker.verify() + return result + # Copy all attributes from the original method.. + for attr in dir(test_method): + # .. unless they're present in our wrapper already. + if not hasattr(test_method_wrapper, attr) or attr == "__doc__": + setattr(test_method_wrapper, attr, + getattr(test_method, attr)) + setattr(self, methodName, test_method_wrapper) + + # We could overload run() normally, but other well-known testing + # frameworks do it as well, and some of them won't call the super, + # which might mean that cleanup wouldn't happen. With that in mind, + # we make integration easier by using the following trick. + run_method = self.run + def run_wrapper(*args, **kwargs): + try: + return run_method(*args, **kwargs) + finally: + self.__cleanup() + self.run = run_wrapper + + self.mocker = Mocker() + + self.__cleanup_funcs = [] + self.__cleanup_paths = [] + + super(MockerTestCase, self).__init__(methodName) + + def __cleanup(self): + for path in self.__cleanup_paths: + if os.path.isfile(path): + os.unlink(path) + elif os.path.isdir(path): + shutil.rmtree(path) + self.mocker.restore() + for func, args, kwargs in self.__cleanup_funcs: + func(*args, **kwargs) + + def addCleanup(self, func, *args, **kwargs): + self.__cleanup_funcs.append((func, args, kwargs)) + + def makeFile(self, content=None, suffix="", prefix="tmp", basename=None, + dirname=None, path=None): + """Create a temporary file and return the path to it. + + @param content: Initial content for the file. + @param suffix: Suffix to be given to the file's basename. + @param prefix: Prefix to be given to the file's basename. + @param basename: Full basename for the file. + @param dirname: Put file inside this directory. + + The file is removed after the test runs. + """ + if path is not None: + self.__cleanup_paths.append(path) + elif basename is not None: + if dirname is None: + dirname = tempfile.mkdtemp() + self.__cleanup_paths.append(dirname) + path = os.path.join(dirname, basename) + else: + fd, path = tempfile.mkstemp(suffix, prefix, dirname) + self.__cleanup_paths.append(path) + os.close(fd) + if content is None: + os.unlink(path) + if content is not None: + file = open(path, "w") + file.write(content) + file.close() + return path + + def makeDir(self, suffix="", prefix="tmp", dirname=None, path=None): + """Create a temporary directory and return the path to it. + + @param suffix: Suffix to be given to the file's basename. + @param prefix: Prefix to be given to the file's basename. + @param dirname: Put directory inside this parent directory. + + The directory is removed after the test runs. + """ + if path is not None: + os.makedirs(path) + else: + path = tempfile.mkdtemp(suffix, prefix, dirname) + self.__cleanup_paths.append(path) + return path + + def failUnlessIs(self, first, second, msg=None): + """Assert that C{first} is the same object as C{second}.""" + if first is not second: + raise self.failureException(msg or "%r is not %r" % (first, second)) + + def failIfIs(self, first, second, msg=None): + """Assert that C{first} is not the same object as C{second}.""" + if first is second: + raise self.failureException(msg or "%r is %r" % (first, second)) + + def failUnlessIn(self, first, second, msg=None): + """Assert that C{first} is contained in C{second}.""" + if first not in second: + raise self.failureException(msg or "%r not in %r" % (first, second)) + + def failUnlessStartsWith(self, first, second, msg=None): + """Assert that C{first} starts with C{second}.""" + if first[:len(second)] != second: + raise self.failureException(msg or "%r doesn't start with %r" % + (first, second)) + + def failIfStartsWith(self, first, second, msg=None): + """Assert that C{first} doesn't start with C{second}.""" + if first[:len(second)] == second: + raise self.failureException(msg or "%r starts with %r" % + (first, second)) + + def failUnlessEndsWith(self, first, second, msg=None): + """Assert that C{first} starts with C{second}.""" + if first[len(first)-len(second):] != second: + raise self.failureException(msg or "%r doesn't end with %r" % + (first, second)) + + def failIfEndsWith(self, first, second, msg=None): + """Assert that C{first} doesn't start with C{second}.""" + if first[len(first)-len(second):] == second: + raise self.failureException(msg or "%r ends with %r" % + (first, second)) + + def failIfIn(self, first, second, msg=None): + """Assert that C{first} is not contained in C{second}.""" + if first in second: + raise self.failureException(msg or "%r in %r" % (first, second)) + + def failUnlessApproximates(self, first, second, tolerance, msg=None): + """Assert that C{first} is near C{second} by at most C{tolerance}.""" + if abs(first - second) > tolerance: + raise self.failureException(msg or "abs(%r - %r) > %r" % + (first, second, tolerance)) + + def failIfApproximates(self, first, second, tolerance, msg=None): + """Assert that C{first} is far from C{second} by at least C{tolerance}. + """ + if abs(first - second) <= tolerance: + raise self.failureException(msg or "abs(%r - %r) <= %r" % + (first, second, tolerance)) + + def failUnlessMethodsMatch(self, first, second): + """Assert that public methods in C{first} are present in C{second}. + + This method asserts that all public methods found in C{first} are also + present in C{second} and accept the same arguments. C{first} may + have its own private methods, though, and may not have all methods + found in C{second}. Note that if a private method in C{first} matches + the name of one in C{second}, their specification is still compared. + + This is useful to verify if a fake or stub class have the same API as + the real class being simulated. + """ + first_methods = dict(inspect.getmembers(first, inspect.ismethod)) + second_methods = dict(inspect.getmembers(second, inspect.ismethod)) + for name, first_method in first_methods.items(): + first_argspec = inspect.getargspec(first_method) + first_formatted = inspect.formatargspec(*first_argspec) + + second_method = second_methods.get(name) + if second_method is None: + if name[:1] == "_": + continue # First may have its own private methods. + raise self.failureException("%s.%s%s not present in %s" % + (first.__name__, name, first_formatted, second.__name__)) + + second_argspec = inspect.getargspec(second_method) + if first_argspec != second_argspec: + second_formatted = inspect.formatargspec(*second_argspec) + raise self.failureException("%s.%s%s != %s.%s%s" % + (first.__name__, name, first_formatted, + second.__name__, name, second_formatted)) + + + assertIs = failUnlessIs + assertIsNot = failIfIs + assertIn = failUnlessIn + assertNotIn = failIfIn + assertStartsWith = failUnlessStartsWith + assertNotStartsWith = failIfStartsWith + assertEndsWith = failUnlessEndsWith + assertNotEndsWith = failIfEndsWith + assertApproximates = failUnlessApproximates + assertNotApproximates = failIfApproximates + assertMethodsMatch = failUnlessMethodsMatch + + # The following are missing in Python < 2.4. + assertTrue = unittest.TestCase.failUnless + assertFalse = unittest.TestCase.failIf + + # The following is provided for compatibility with Twisted's trial. + assertIdentical = assertIs + assertNotIdentical = assertIsNot + failUnlessIdentical = failUnlessIs + failIfIdentical = failIfIs + + +# -------------------------------------------------------------------- +# Mocker. + +class classinstancemethod(object): + + def __init__(self, method): + self.method = method + + def __get__(self, obj, cls=None): + def bound_method(*args, **kwargs): + return self.method(cls, obj, *args, **kwargs) + return bound_method + + +class MockerBase(object): + """Controller of mock objects. + + A mocker instance is used to command recording and replay of + expectations on any number of mock objects. + + Expectations should be expressed for the mock object while in + record mode (the initial one) by using the mock object itself, + and using the mocker (and/or C{expect()} as a helper) to define + additional behavior for each event. For instance:: + + mock = mocker.mock() + mock.hello() + mocker.result("Hi!") + mocker.replay() + assert mock.hello() == "Hi!" + mock.restore() + mock.verify() + + In this short excerpt a mock object is being created, then an + expectation of a call to the C{hello()} method was recorded, and + when called the method should return the value C{10}. Then, the + mocker is put in replay mode, and the expectation is satisfied by + calling the C{hello()} method, which indeed returns 10. Finally, + a call to the L{restore()} method is performed to undo any needed + changes made in the environment, and the L{verify()} method is + called to ensure that all defined expectations were met. + + The same logic can be expressed more elegantly using the + C{with mocker:} statement, as follows:: + + mock = mocker.mock() + mock.hello() + mocker.result("Hi!") + with mocker: + assert mock.hello() == "Hi!" + + Also, the MockerTestCase class, which integrates the mocker on + a unittest.TestCase subclass, may be used to reduce the overhead + of controlling the mocker. A test could be written as follows:: + + class SampleTest(MockerTestCase): + + def test_hello(self): + mock = self.mocker.mock() + mock.hello() + self.mocker.result("Hi!") + self.mocker.replay() + self.assertEquals(mock.hello(), "Hi!") + """ + + _recorders = [] + + # For convenience only. + on = expect + + class __metaclass__(type): + def __init__(self, name, bases, dict): + # Make independent lists on each subclass, inheriting from parent. + self._recorders = list(getattr(self, "_recorders", ())) + + def __init__(self): + self._recorders = self._recorders[:] + self._events = [] + self._recording = True + self._ordering = False + self._last_orderer = None + + def is_recording(self): + """Return True if in recording mode, False if in replay mode. + + Recording is the initial state. + """ + return self._recording + + def replay(self): + """Change to replay mode, where recorded events are reproduced. + + If already in replay mode, the mocker will be restored, with all + expectations reset, and then put again in replay mode. + + An alternative and more comfortable way to replay changes is + using the 'with' statement, as follows:: + + mocker = Mocker() + + with mocker: + + + The 'with' statement will automatically put mocker in replay + mode, and will also verify if all events were correctly reproduced + at the end (using L{verify()}), and also restore any changes done + in the environment (with L{restore()}). + + Also check the MockerTestCase class, which integrates the + unittest.TestCase class with mocker. + """ + if not self._recording: + for event in self._events: + event.restore() + else: + self._recording = False + for event in self._events: + event.replay() + + def restore(self): + """Restore changes in the environment, and return to recording mode. + + This should always be called after the test is complete (succeeding + or not). There are ways to call this method automatically on + completion (e.g. using a C{with mocker:} statement, or using the + L{MockerTestCase} class. + """ + if not self._recording: + self._recording = True + for event in self._events: + event.restore() + + def reset(self): + """Reset the mocker state. + + This will restore environment changes, if currently in replay + mode, and then remove all events previously recorded. + """ + if not self._recording: + self.restore() + self.unorder() + del self._events[:] + + def get_events(self): + """Return all recorded events.""" + return self._events[:] + + def add_event(self, event): + """Add an event. + + This method is used internally by the implementation, and + shouldn't be needed on normal mocker usage. + """ + self._events.append(event) + if self._ordering: + orderer = event.add_task(Orderer(event.path)) + if self._last_orderer: + orderer.add_dependency(self._last_orderer) + self._last_orderer = orderer + return event + + def verify(self): + """Check if all expectations were met, and raise AssertionError if not. + + The exception message will include a nice description of which + expectations were not met, and why. + """ + errors = [] + for event in self._events: + try: + event.verify() + except AssertionError, e: + error = str(e) + if not error: + raise RuntimeError("Empty error message from %r" + % event) + errors.append(error) + if errors: + message = [ERROR_PREFIX + "Unmet expectations:", ""] + for error in errors: + lines = error.splitlines() + message.append("=> " + lines.pop(0)) + message.extend([" " + line for line in lines]) + message.append("") + raise AssertionError(os.linesep.join(message)) + + def mock(self, spec_and_type=None, spec=None, type=None, + name=None, count=True): + """Return a new mock object. + + @param spec_and_type: Handy positional argument which sets both + spec and type. + @param spec: Method calls will be checked for correctness against + the given class. + @param type: If set, the Mock's __class__ attribute will return + the given type. This will make C{isinstance()} calls + on the object work. + @param name: Name for the mock object, used in the representation of + expressions. The name is rarely needed, as it's usually + guessed correctly from the variable name used. + @param count: If set to false, expressions may be executed any number + of times, unless an expectation is explicitly set using + the L{count()} method. By default, expressions are + expected once. + """ + if spec_and_type is not None: + spec = type = spec_and_type + return Mock(self, spec=spec, type=type, name=name, count=count) + + def proxy(self, object, spec=True, type=True, name=None, count=True, + passthrough=True): + """Return a new mock object which proxies to the given object. + + Proxies are useful when only part of the behavior of an object + is to be mocked. Unknown expressions may be passed through to + the real implementation implicitly (if the C{passthrough} argument + is True), or explicitly (using the L{passthrough()} method + on the event). + + @param object: Real object to be proxied, and replaced by the mock + on replay mode. It may also be an "import path", + such as C{"time.time"}, in which case the object + will be the C{time} function from the C{time} module. + @param spec: Method calls will be checked for correctness against + the given object, which may be a class or an instance + where attributes will be looked up. Defaults to the + the C{object} parameter. May be set to None explicitly, + in which case spec checking is disabled. Checks may + also be disabled explicitly on a per-event basis with + the L{nospec()} method. + @param type: If set, the Mock's __class__ attribute will return + the given type. This will make C{isinstance()} calls + on the object work. Defaults to the type of the + C{object} parameter. May be set to None explicitly. + @param name: Name for the mock object, used in the representation of + expressions. The name is rarely needed, as it's usually + guessed correctly from the variable name used. + @param count: If set to false, expressions may be executed any number + of times, unless an expectation is explicitly set using + the L{count()} method. By default, expressions are + expected once. + @param passthrough: If set to False, passthrough of actions on the + proxy to the real object will only happen when + explicitly requested via the L{passthrough()} + method. + """ + if isinstance(object, basestring): + if name is None: + name = object + import_stack = object.split(".") + attr_stack = [] + while import_stack: + module_path = ".".join(import_stack) + try: + object = __import__(module_path, {}, {}, [""]) + except ImportError: + attr_stack.insert(0, import_stack.pop()) + if not import_stack: + raise + continue + else: + for attr in attr_stack: + object = getattr(object, attr) + break + if spec is True: + spec = object + if type is True: + type = __builtin__.type(object) + return Mock(self, spec=spec, type=type, object=object, + name=name, count=count, passthrough=passthrough) + + def replace(self, object, spec=True, type=True, name=None, count=True, + passthrough=True): + """Create a proxy, and replace the original object with the mock. + + On replay, the original object will be replaced by the returned + proxy in all dictionaries found in the running interpreter via + the garbage collecting system. This should cover module + namespaces, class namespaces, instance namespaces, and so on. + + @param object: Real object to be proxied, and replaced by the mock + on replay mode. It may also be an "import path", + such as C{"time.time"}, in which case the object + will be the C{time} function from the C{time} module. + @param spec: Method calls will be checked for correctness against + the given object, which may be a class or an instance + where attributes will be looked up. Defaults to the + the C{object} parameter. May be set to None explicitly, + in which case spec checking is disabled. Checks may + also be disabled explicitly on a per-event basis with + the L{nospec()} method. + @param type: If set, the Mock's __class__ attribute will return + the given type. This will make C{isinstance()} calls + on the object work. Defaults to the type of the + C{object} parameter. May be set to None explicitly. + @param name: Name for the mock object, used in the representation of + expressions. The name is rarely needed, as it's usually + guessed correctly from the variable name used. + @param passthrough: If set to False, passthrough of actions on the + proxy to the real object will only happen when + explicitly requested via the L{passthrough()} + method. + """ + mock = self.proxy(object, spec, type, name, count, passthrough) + event = self._get_replay_restore_event() + event.add_task(ProxyReplacer(mock)) + return mock + + def patch(self, object, spec=True): + """Patch an existing object to reproduce recorded events. + + @param object: Class or instance to be patched. + @param spec: Method calls will be checked for correctness against + the given object, which may be a class or an instance + where attributes will be looked up. Defaults to the + the C{object} parameter. May be set to None explicitly, + in which case spec checking is disabled. Checks may + also be disabled explicitly on a per-event basis with + the L{nospec()} method. + + The result of this method is still a mock object, which can be + used like any other mock object to record events. The difference + is that when the mocker is put on replay mode, the *real* object + will be modified to behave according to recorded expectations. + + Patching works in individual instances, and also in classes. + When an instance is patched, recorded events will only be + considered on this specific instance, and other instances should + behave normally. When a class is patched, the reproduction of + events will be considered on any instance of this class once + created (collectively). + + Observe that, unlike with proxies which catch only events done + through the mock object, *all* accesses to recorded expectations + will be considered; even these coming from the object itself + (e.g. C{self.hello()} is considered if this method was patched). + While this is a very powerful feature, and many times the reason + to use patches in the first place, it's important to keep this + behavior in mind. + + Patching of the original object only takes place when the mocker + is put on replay mode, and the patched object will be restored + to its original state once the L{restore()} method is called + (explicitly, or implicitly with alternative conventions, such as + a C{with mocker:} block, or a MockerTestCase class). + """ + if spec is True: + spec = object + patcher = Patcher() + event = self._get_replay_restore_event() + event.add_task(patcher) + mock = Mock(self, object=object, patcher=patcher, + passthrough=True, spec=spec) + object.__mocker_mock__ = mock + return mock + + def act(self, path): + """This is called by mock objects whenever something happens to them. + + This method is part of the implementation between the mocker + and mock objects. + """ + if self._recording: + event = self.add_event(Event(path)) + for recorder in self._recorders: + recorder(self, event) + return Mock(self, path) + else: + # First run events that may run, then run unsatisfied events, then + # ones not previously run. We put the index in the ordering tuple + # instead of the actual event because we want a stable sort + # (ordering between 2 events is undefined). + events = self._events + order = [(events[i].satisfied()*2 + events[i].has_run(), i) + for i in range(len(events))] + order.sort() + postponed = None + for weight, i in order: + event = events[i] + if event.matches(path): + if event.may_run(path): + return event.run(path) + elif postponed is None: + postponed = event + if postponed is not None: + return postponed.run(path) + raise MatchError(ERROR_PREFIX + "Unexpected expression: %s" % path) + + def get_recorders(cls, self): + """Return recorders associated with this mocker class or instance. + + This method may be called on mocker instances and also on mocker + classes. See the L{add_recorder()} method for more information. + """ + return (self or cls)._recorders[:] + get_recorders = classinstancemethod(get_recorders) + + def add_recorder(cls, self, recorder): + """Add a recorder to this mocker class or instance. + + @param recorder: Callable accepting C{(mocker, event)} as parameters. + + This is part of the implementation of mocker. + + All registered recorders are called for translating events that + happen during recording into expectations to be met once the state + is switched to replay mode. + + This method may be called on mocker instances and also on mocker + classes. When called on a class, the recorder will be used by + all instances, and also inherited on subclassing. When called on + instances, the recorder is added only to the given instance. + """ + (self or cls)._recorders.append(recorder) + return recorder + add_recorder = classinstancemethod(add_recorder) + + def remove_recorder(cls, self, recorder): + """Remove the given recorder from this mocker class or instance. + + This method may be called on mocker classes and also on mocker + instances. See the L{add_recorder()} method for more information. + """ + (self or cls)._recorders.remove(recorder) + remove_recorder = classinstancemethod(remove_recorder) + + def result(self, value): + """Make the last recorded event return the given value on replay. + + @param value: Object to be returned when the event is replayed. + """ + self.call(lambda *args, **kwargs: value) + + def generate(self, sequence): + """Last recorded event will return a generator with the given sequence. + + @param sequence: Sequence of values to be generated. + """ + def generate(*args, **kwargs): + for value in sequence: + yield value + self.call(generate) + + def throw(self, exception): + """Make the last recorded event raise the given exception on replay. + + @param exception: Class or instance of exception to be raised. + """ + def raise_exception(*args, **kwargs): + raise exception + self.call(raise_exception) + + def call(self, func): + """Make the last recorded event cause the given function to be called. + + @param func: Function to be called. + + The result of the function will be used as the event result. + """ + self._events[-1].add_task(FunctionRunner(func)) + + def count(self, min, max=False): + """Last recorded event must be replayed between min and max times. + + @param min: Minimum number of times that the event must happen. + @param max: Maximum number of times that the event must happen. If + not given, it defaults to the same value of the C{min} + parameter. If set to None, there is no upper limit, and + the expectation is met as long as it happens at least + C{min} times. + """ + event = self._events[-1] + for task in event.get_tasks(): + if isinstance(task, RunCounter): + event.remove_task(task) + event.add_task(RunCounter(min, max)) + + def is_ordering(self): + """Return true if all events are being ordered. + + See the L{order()} method. + """ + return self._ordering + + def unorder(self): + """Disable the ordered mode. + + See the L{order()} method for more information. + """ + self._ordering = False + self._last_orderer = None + + def order(self, *path_holders): + """Create an expectation of order between two or more events. + + @param path_holders: Objects returned as the result of recorded events. + + By default, mocker won't force events to happen precisely in + the order they were recorded. Calling this method will change + this behavior so that events will only match if reproduced in + the correct order. + + There are two ways in which this method may be used. Which one + is used in a given occasion depends only on convenience. + + If no arguments are passed, the mocker will be put in a mode where + all the recorded events following the method call will only be met + if they happen in order. When that's used, the mocker may be put + back in unordered mode by calling the L{unorder()} method, or by + using a 'with' block, like so:: + + with mocker.ordered(): + + + In this case, only expressions in will be ordered, + and the mocker will be back in unordered mode after the 'with' block. + + The second way to use it is by specifying precisely which events + should be ordered. As an example:: + + mock = mocker.mock() + expr1 = mock.hello() + expr2 = mock.world + expr3 = mock.x.y.z + mocker.order(expr1, expr2, expr3) + + This method of ordering only works when the expression returns + another object. + + Also check the L{after()} and L{before()} methods, which are + alternative ways to perform this. + """ + if not path_holders: + self._ordering = True + return OrderedContext(self) + + last_orderer = None + for path_holder in path_holders: + if type(path_holder) is Path: + path = path_holder + else: + path = path_holder.__mocker_path__ + for event in self._events: + if event.path is path: + for task in event.get_tasks(): + if isinstance(task, Orderer): + orderer = task + break + else: + orderer = Orderer(path) + event.add_task(orderer) + if last_orderer: + orderer.add_dependency(last_orderer) + last_orderer = orderer + break + + def after(self, *path_holders): + """Last recorded event must happen after events referred to. + + @param path_holders: Objects returned as the result of recorded events + which should happen before the last recorded event + + As an example, the idiom:: + + expect(mock.x).after(mock.y, mock.z) + + is an alternative way to say:: + + expr_x = mock.x + expr_y = mock.y + expr_z = mock.z + mocker.order(expr_y, expr_x) + mocker.order(expr_z, expr_x) + + See L{order()} for more information. + """ + last_path = self._events[-1].path + for path_holder in path_holders: + self.order(path_holder, last_path) + + def before(self, *path_holders): + """Last recorded event must happen before events referred to. + + @param path_holders: Objects returned as the result of recorded events + which should happen after the last recorded event + + As an example, the idiom:: + + expect(mock.x).before(mock.y, mock.z) + + is an alternative way to say:: + + expr_x = mock.x + expr_y = mock.y + expr_z = mock.z + mocker.order(expr_x, expr_y) + mocker.order(expr_x, expr_z) + + See L{order()} for more information. + """ + last_path = self._events[-1].path + for path_holder in path_holders: + self.order(last_path, path_holder) + + def nospec(self): + """Don't check method specification of real object on last event. + + By default, when using a mock created as the result of a call to + L{proxy()}, L{replace()}, and C{patch()}, or when passing the spec + attribute to the L{mock()} method, method calls on the given object + are checked for correctness against the specification of the real + object (or the explicitly provided spec). + + This method will disable that check specifically for the last + recorded event. + """ + event = self._events[-1] + for task in event.get_tasks(): + if isinstance(task, SpecChecker): + event.remove_task(task) + + def passthrough(self, result_callback=None): + """Make the last recorded event run on the real object once seen. + + @param result_callback: If given, this function will be called with + the result of the *real* method call as the only argument. + + This can only be used on proxies, as returned by the L{proxy()} + and L{replace()} methods, or on mocks representing patched objects, + as returned by the L{patch()} method. + """ + event = self._events[-1] + if event.path.root_object is None: + raise TypeError("Mock object isn't a proxy") + event.add_task(PathExecuter(result_callback)) + + def __enter__(self): + """Enter in a 'with' context. This will run replay().""" + self.replay() + return self + + def __exit__(self, type, value, traceback): + """Exit from a 'with' context. + + This will run restore() at all times, but will only run verify() + if the 'with' block itself hasn't raised an exception. Exceptions + in that block are never swallowed. + """ + self.restore() + if type is None: + self.verify() + return False + + def _get_replay_restore_event(self): + """Return unique L{ReplayRestoreEvent}, creating if needed. + + Some tasks only want to replay/restore. When that's the case, + they shouldn't act on other events during replay. Also, they + can all be put in a single event when that's the case. Thus, + we add a single L{ReplayRestoreEvent} as the first element of + the list. + """ + if not self._events or type(self._events[0]) != ReplayRestoreEvent: + self._events.insert(0, ReplayRestoreEvent()) + return self._events[0] + + +class OrderedContext(object): + + def __init__(self, mocker): + self._mocker = mocker + + def __enter__(self): + return None + + def __exit__(self, type, value, traceback): + self._mocker.unorder() + + +class Mocker(MockerBase): + __doc__ = MockerBase.__doc__ + +# Decorator to add recorders on the standard Mocker class. +recorder = Mocker.add_recorder + + +# -------------------------------------------------------------------- +# Mock object. + +class Mock(object): + + def __init__(self, mocker, path=None, name=None, spec=None, type=None, + object=None, passthrough=False, patcher=None, count=True): + self.__mocker__ = mocker + self.__mocker_path__ = path or Path(self, object) + self.__mocker_name__ = name + self.__mocker_spec__ = spec + self.__mocker_object__ = object + self.__mocker_passthrough__ = passthrough + self.__mocker_patcher__ = patcher + self.__mocker_replace__ = False + self.__mocker_type__ = type + self.__mocker_count__ = count + + def __mocker_act__(self, kind, args=(), kwargs={}, object=None): + if self.__mocker_name__ is None: + self.__mocker_name__ = find_object_name(self, 2) + action = Action(kind, args, kwargs, self.__mocker_path__) + path = self.__mocker_path__ + action + if object is not None: + path.root_object = object + try: + return self.__mocker__.act(path) + except MatchError, exception: + root_mock = path.root_mock + if (path.root_object is not None and + root_mock.__mocker_passthrough__): + return path.execute(path.root_object) + # Reinstantiate to show raise statement on traceback, and + # also to make the traceback shown shorter. + raise MatchError(str(exception)) + except AssertionError, e: + lines = str(e).splitlines() + message = [ERROR_PREFIX + "Unmet expectation:", ""] + message.append("=> " + lines.pop(0)) + message.extend([" " + line for line in lines]) + message.append("") + raise AssertionError(os.linesep.join(message)) + + def __getattribute__(self, name): + if name.startswith("__mocker_"): + return super(Mock, self).__getattribute__(name) + if name == "__class__": + if self.__mocker__.is_recording() or self.__mocker_type__ is None: + return type(self) + return self.__mocker_type__ + return self.__mocker_act__("getattr", (name,)) + + def __setattr__(self, name, value): + if name.startswith("__mocker_"): + return super(Mock, self).__setattr__(name, value) + return self.__mocker_act__("setattr", (name, value)) + + def __delattr__(self, name): + return self.__mocker_act__("delattr", (name,)) + + def __call__(self, *args, **kwargs): + return self.__mocker_act__("call", args, kwargs) + + def __contains__(self, value): + return self.__mocker_act__("contains", (value,)) + + def __getitem__(self, key): + return self.__mocker_act__("getitem", (key,)) + + def __setitem__(self, key, value): + return self.__mocker_act__("setitem", (key, value)) + + def __delitem__(self, key): + return self.__mocker_act__("delitem", (key,)) + + def __len__(self): + # MatchError is turned on an AttributeError so that list() and + # friends act properly when trying to get length hints on + # something that doesn't offer them. + try: + result = self.__mocker_act__("len") + except MatchError, e: + raise AttributeError(str(e)) + if type(result) is Mock: + return 0 + return result + + def __nonzero__(self): + try: + return self.__mocker_act__("nonzero") + except MatchError, e: + return True + + def __iter__(self): + # XXX On py3k, when next() becomes __next__(), we'll be able + # to return the mock itself because it will be considered + # an iterator (we'll be mocking __next__ as well, which we + # can't now). + result = self.__mocker_act__("iter") + if type(result) is Mock: + return iter([]) + return result + + # When adding a new action kind here, also add support for it on + # Action.execute() and Path.__str__(). + + +def find_object_name(obj, depth=0): + """Try to detect how the object is named on a previous scope.""" + try: + frame = sys._getframe(depth+1) + except: + return None + for name, frame_obj in frame.f_locals.iteritems(): + if frame_obj is obj: + return name + self = frame.f_locals.get("self") + if self is not None: + try: + items = list(self.__dict__.iteritems()) + except: + pass + else: + for name, self_obj in items: + if self_obj is obj: + return name + return None + + +# -------------------------------------------------------------------- +# Action and path. + +class Action(object): + + def __init__(self, kind, args, kwargs, path=None): + self.kind = kind + self.args = args + self.kwargs = kwargs + self.path = path + self._execute_cache = {} + + def __repr__(self): + if self.path is None: + return "Action(%r, %r, %r)" % (self.kind, self.args, self.kwargs) + return "Action(%r, %r, %r, %r)" % \ + (self.kind, self.args, self.kwargs, self.path) + + def __eq__(self, other): + return (self.kind == other.kind and + self.args == other.args and + self.kwargs == other.kwargs) + + def __ne__(self, other): + return not self.__eq__(other) + + def matches(self, other): + return (self.kind == other.kind and + match_params(self.args, self.kwargs, other.args, other.kwargs)) + + def execute(self, object): + # This caching scheme may fail if the object gets deallocated before + # the action, as the id might get reused. It's somewhat easy to fix + # that with a weakref callback. For our uses, though, the object + # should never get deallocated before the action itself, so we'll + # just keep it simple. + if id(object) in self._execute_cache: + return self._execute_cache[id(object)] + execute = getattr(object, "__mocker_execute__", None) + if execute is not None: + result = execute(self, object) + else: + kind = self.kind + if kind == "getattr": + result = getattr(object, self.args[0]) + elif kind == "setattr": + result = setattr(object, self.args[0], self.args[1]) + elif kind == "delattr": + result = delattr(object, self.args[0]) + elif kind == "call": + result = object(*self.args, **self.kwargs) + elif kind == "contains": + result = self.args[0] in object + elif kind == "getitem": + result = object[self.args[0]] + elif kind == "setitem": + result = object[self.args[0]] = self.args[1] + elif kind == "delitem": + del object[self.args[0]] + result = None + elif kind == "len": + result = len(object) + elif kind == "nonzero": + result = bool(object) + elif kind == "iter": + result = iter(object) + else: + raise RuntimeError("Don't know how to execute %r kind." % kind) + self._execute_cache[id(object)] = result + return result + + +class Path(object): + + def __init__(self, root_mock, root_object=None, actions=()): + self.root_mock = root_mock + self.root_object = root_object + self.actions = tuple(actions) + self.__mocker_replace__ = False + + def parent_path(self): + if not self.actions: + return None + return self.actions[-1].path + parent_path = property(parent_path) + + def __add__(self, action): + """Return a new path which includes the given action at the end.""" + return self.__class__(self.root_mock, self.root_object, + self.actions + (action,)) + + def __eq__(self, other): + """Verify if the two paths are equal. + + Two paths are equal if they refer to the same mock object, and + have the actions with equal kind, args and kwargs. + """ + if (self.root_mock is not other.root_mock or + self.root_object is not other.root_object or + len(self.actions) != len(other.actions)): + return False + for action, other_action in zip(self.actions, other.actions): + if action != other_action: + return False + return True + + def matches(self, other): + """Verify if the two paths are equivalent. + + Two paths are equal if they refer to the same mock object, and + have the same actions performed on them. + """ + if (self.root_mock is not other.root_mock or + len(self.actions) != len(other.actions)): + return False + for action, other_action in zip(self.actions, other.actions): + if not action.matches(other_action): + return False + return True + + def execute(self, object): + """Execute all actions sequentially on object, and return result. + """ + for action in self.actions: + object = action.execute(object) + return object + + def __str__(self): + """Transform the path into a nice string such as obj.x.y('z').""" + result = self.root_mock.__mocker_name__ or "" + for action in self.actions: + if action.kind == "getattr": + result = "%s.%s" % (result, action.args[0]) + elif action.kind == "setattr": + result = "%s.%s = %r" % (result, action.args[0], action.args[1]) + elif action.kind == "delattr": + result = "del %s.%s" % (result, action.args[0]) + elif action.kind == "call": + args = [repr(x) for x in action.args] + items = list(action.kwargs.iteritems()) + items.sort() + for pair in items: + args.append("%s=%r" % pair) + result = "%s(%s)" % (result, ", ".join(args)) + elif action.kind == "contains": + result = "%r in %s" % (action.args[0], result) + elif action.kind == "getitem": + result = "%s[%r]" % (result, action.args[0]) + elif action.kind == "setitem": + result = "%s[%r] = %r" % (result, action.args[0], + action.args[1]) + elif action.kind == "delitem": + result = "del %s[%r]" % (result, action.args[0]) + elif action.kind == "len": + result = "len(%s)" % result + elif action.kind == "nonzero": + result = "bool(%s)" % result + elif action.kind == "iter": + result = "iter(%s)" % result + else: + raise RuntimeError("Don't know how to format kind %r" % + action.kind) + return result + + +class SpecialArgument(object): + """Base for special arguments for matching parameters.""" + + def __init__(self, object=None): + self.object = object + + def __repr__(self): + if self.object is None: + return self.__class__.__name__ + else: + return "%s(%r)" % (self.__class__.__name__, self.object) + + def matches(self, other): + return True + + def __eq__(self, other): + return type(other) == type(self) and self.object == other.object + + +class ANY(SpecialArgument): + """Matches any single argument.""" + +ANY = ANY() + + +class ARGS(SpecialArgument): + """Matches zero or more positional arguments.""" + +ARGS = ARGS() + + +class KWARGS(SpecialArgument): + """Matches zero or more keyword arguments.""" + +KWARGS = KWARGS() + + +class IS(SpecialArgument): + + def matches(self, other): + return self.object is other + + def __eq__(self, other): + return type(other) == type(self) and self.object is other.object + + +class CONTAINS(SpecialArgument): + + def matches(self, other): + try: + other.__contains__ + except AttributeError: + try: + iter(other) + except TypeError: + # If an object can't be iterated, and has no __contains__ + # hook, it'd blow up on the test below. We test this in + # advance to prevent catching more errors than we really + # want. + return False + return self.object in other + + +class IN(SpecialArgument): + + def matches(self, other): + return other in self.object + + +class MATCH(SpecialArgument): + + def matches(self, other): + return bool(self.object(other)) + + def __eq__(self, other): + return type(other) == type(self) and self.object is other.object + + +def match_params(args1, kwargs1, args2, kwargs2): + """Match the two sets of parameters, considering special parameters.""" + + has_args = ARGS in args1 + has_kwargs = KWARGS in args1 + + if has_kwargs: + args1 = [arg1 for arg1 in args1 if arg1 is not KWARGS] + elif len(kwargs1) != len(kwargs2): + return False + + if not has_args and len(args1) != len(args2): + return False + + # Either we have the same number of kwargs, or unknown keywords are + # accepted (KWARGS was used), so check just the ones in kwargs1. + for key, arg1 in kwargs1.iteritems(): + if key not in kwargs2: + return False + arg2 = kwargs2[key] + if isinstance(arg1, SpecialArgument): + if not arg1.matches(arg2): + return False + elif arg1 != arg2: + return False + + # Keywords match. Now either we have the same number of + # arguments, or ARGS was used. If ARGS wasn't used, arguments + # must match one-on-one necessarily. + if not has_args: + for arg1, arg2 in zip(args1, args2): + if isinstance(arg1, SpecialArgument): + if not arg1.matches(arg2): + return False + elif arg1 != arg2: + return False + return True + + # Easy choice. Keywords are matching, and anything on args is accepted. + if (ARGS,) == args1: + return True + + # We have something different there. If we don't have positional + # arguments on the original call, it can't match. + if not args2: + # Unless we have just several ARGS (which is bizarre, but..). + for arg1 in args1: + if arg1 is not ARGS: + return False + return True + + # Ok, all bets are lost. We have to actually do the more expensive + # matching. This is an algorithm based on the idea of the Levenshtein + # Distance between two strings, but heavily hacked for this purpose. + args2l = len(args2) + if args1[0] is ARGS: + args1 = args1[1:] + array = [0]*args2l + else: + array = [1]*args2l + for i in range(len(args1)): + last = array[0] + if args1[i] is ARGS: + for j in range(1, args2l): + last, array[j] = array[j], min(array[j-1], array[j], last) + else: + array[0] = i or int(args1[i] != args2[0]) + for j in range(1, args2l): + last, array[j] = array[j], last or int(args1[i] != args2[j]) + if 0 not in array: + return False + if array[-1] != 0: + return False + return True + + +# -------------------------------------------------------------------- +# Event and task base. + +class Event(object): + """Aggregation of tasks that keep track of a recorded action. + + An event represents something that may or may not happen while the + mocked environment is running, such as an attribute access, or a + method call. The event is composed of several tasks that are + orchestrated together to create a composed meaning for the event, + including for which actions it should be run, what happens when it + runs, and what's the expectations about the actions run. + """ + + def __init__(self, path=None): + self.path = path + self._tasks = [] + self._has_run = False + + def add_task(self, task): + """Add a new task to this taks.""" + self._tasks.append(task) + return task + + def remove_task(self, task): + self._tasks.remove(task) + + def get_tasks(self): + return self._tasks[:] + + def matches(self, path): + """Return true if *all* tasks match the given path.""" + for task in self._tasks: + if not task.matches(path): + return False + return bool(self._tasks) + + def has_run(self): + return self._has_run + + def may_run(self, path): + """Verify if any task would certainly raise an error if run. + + This will call the C{may_run()} method on each task and return + false if any of them returns false. + """ + for task in self._tasks: + if not task.may_run(path): + return False + return True + + def run(self, path): + """Run all tasks with the given action. + + @param path: The path of the expression run. + + Running an event means running all of its tasks individually and in + order. An event should only ever be run if all of its tasks claim to + match the given action. + + The result of this method will be the last result of a task + which isn't None, or None if they're all None. + """ + self._has_run = True + result = None + errors = [] + for task in self._tasks: + try: + task_result = task.run(path) + except AssertionError, e: + error = str(e) + if not error: + raise RuntimeError("Empty error message from %r" % task) + errors.append(error) + else: + if task_result is not None: + result = task_result + if errors: + message = [str(self.path)] + if str(path) != message[0]: + message.append("- Run: %s" % path) + for error in errors: + lines = error.splitlines() + message.append("- " + lines.pop(0)) + message.extend([" " + line for line in lines]) + raise AssertionError(os.linesep.join(message)) + return result + + def satisfied(self): + """Return true if all tasks are satisfied. + + Being satisfied means that there are no unmet expectations. + """ + for task in self._tasks: + try: + task.verify() + except AssertionError: + return False + return True + + def verify(self): + """Run verify on all tasks. + + The verify method is supposed to raise an AssertionError if the + task has unmet expectations, with a one-line explanation about + why this item is unmet. This method should be safe to be called + multiple times without side effects. + """ + errors = [] + for task in self._tasks: + try: + task.verify() + except AssertionError, e: + error = str(e) + if not error: + raise RuntimeError("Empty error message from %r" % task) + errors.append(error) + if errors: + message = [str(self.path)] + for error in errors: + lines = error.splitlines() + message.append("- " + lines.pop(0)) + message.extend([" " + line for line in lines]) + raise AssertionError(os.linesep.join(message)) + + def replay(self): + """Put all tasks in replay mode.""" + self._has_run = False + for task in self._tasks: + task.replay() + + def restore(self): + """Restore the state of all tasks.""" + for task in self._tasks: + task.restore() + + +class ReplayRestoreEvent(Event): + """Helper event for tasks which need replay/restore but shouldn't match.""" + + def matches(self, path): + return False + + +class Task(object): + """Element used to track one specific aspect on an event. + + A task is responsible for adding any kind of logic to an event. + Examples of that are counting the number of times the event was + made, verifying parameters if any, and so on. + """ + + def matches(self, path): + """Return true if the task is supposed to be run for the given path. + """ + return True + + def may_run(self, path): + """Return false if running this task would certainly raise an error.""" + return True + + def run(self, path): + """Perform the task item, considering that the given action happened. + """ + + def verify(self): + """Raise AssertionError if expectations for this item are unmet. + + The verify method is supposed to raise an AssertionError if the + task has unmet expectations, with a one-line explanation about + why this item is unmet. This method should be safe to be called + multiple times without side effects. + """ + + def replay(self): + """Put the task in replay mode. + + Any expectations of the task should be reset. + """ + + def restore(self): + """Restore any environmental changes made by the task. + + Verify should continue to work after this is called. + """ + + +# -------------------------------------------------------------------- +# Task implementations. + +class OnRestoreCaller(Task): + """Call a given callback when restoring.""" + + def __init__(self, callback): + self._callback = callback + + def restore(self): + self._callback() + + +class PathMatcher(Task): + """Match the action path against a given path.""" + + def __init__(self, path): + self.path = path + + def matches(self, path): + return self.path.matches(path) + +def path_matcher_recorder(mocker, event): + event.add_task(PathMatcher(event.path)) + +Mocker.add_recorder(path_matcher_recorder) + + +class RunCounter(Task): + """Task which verifies if the number of runs are within given boundaries. + """ + + def __init__(self, min, max=False): + self.min = min + if max is None: + self.max = sys.maxint + elif max is False: + self.max = min + else: + self.max = max + self._runs = 0 + + def replay(self): + self._runs = 0 + + def may_run(self, path): + return self._runs < self.max + + def run(self, path): + self._runs += 1 + if self._runs > self.max: + self.verify() + + def verify(self): + if not self.min <= self._runs <= self.max: + if self._runs < self.min: + raise AssertionError("Performed fewer times than expected.") + raise AssertionError("Performed more times than expected.") + + +class ImplicitRunCounter(RunCounter): + """RunCounter inserted by default on any event. + + This is a way to differentiate explicitly added counters and + implicit ones. + """ + +def run_counter_recorder(mocker, event): + """Any event may be repeated once, unless disabled by default.""" + if event.path.root_mock.__mocker_count__: + event.add_task(ImplicitRunCounter(1)) + +Mocker.add_recorder(run_counter_recorder) + +def run_counter_removal_recorder(mocker, event): + """ + Events created by getattr actions which lead to other events + may be repeated any number of times. For that, we remove implicit + run counters of any getattr actions leading to the current one. + """ + parent_path = event.path.parent_path + for event in mocker.get_events()[::-1]: + if (event.path is parent_path and + event.path.actions[-1].kind == "getattr"): + for task in event.get_tasks(): + if type(task) is ImplicitRunCounter: + event.remove_task(task) + +Mocker.add_recorder(run_counter_removal_recorder) + + +class MockReturner(Task): + """Return a mock based on the action path.""" + + def __init__(self, mocker): + self.mocker = mocker + + def run(self, path): + return Mock(self.mocker, path) + +def mock_returner_recorder(mocker, event): + """Events that lead to other events must return mock objects.""" + parent_path = event.path.parent_path + for event in mocker.get_events(): + if event.path is parent_path: + for task in event.get_tasks(): + if isinstance(task, MockReturner): + break + else: + event.add_task(MockReturner(mocker)) + break + +Mocker.add_recorder(mock_returner_recorder) + + +class FunctionRunner(Task): + """Task that runs a function everything it's run. + + Arguments of the last action in the path are passed to the function, + and the function result is also returned. + """ + + def __init__(self, func): + self._func = func + + def run(self, path): + action = path.actions[-1] + return self._func(*action.args, **action.kwargs) + + +class PathExecuter(Task): + """Task that executes a path in the real object, and returns the result.""" + + def __init__(self, result_callback=None): + self._result_callback = result_callback + + def get_result_callback(self): + return self._result_callback + + def run(self, path): + result = path.execute(path.root_object) + if self._result_callback is not None: + self._result_callback(result) + return result + + +class Orderer(Task): + """Task to establish an order relation between two events. + + An orderer task will only match once all its dependencies have + been run. + """ + + def __init__(self, path): + self.path = path + self._run = False + self._dependencies = [] + + def replay(self): + self._run = False + + def has_run(self): + return self._run + + def may_run(self, path): + for dependency in self._dependencies: + if not dependency.has_run(): + return False + return True + + def run(self, path): + for dependency in self._dependencies: + if not dependency.has_run(): + raise AssertionError("Should be after: %s" % dependency.path) + self._run = True + + def add_dependency(self, orderer): + self._dependencies.append(orderer) + + def get_dependencies(self): + return self._dependencies + + +class SpecChecker(Task): + """Task to check if arguments of the last action conform to a real method. + """ + + def __init__(self, method): + self._method = method + self._unsupported = False + + if method: + try: + self._args, self._varargs, self._varkwargs, self._defaults = \ + inspect.getargspec(method) + except TypeError: + self._unsupported = True + else: + if self._defaults is None: + self._defaults = () + if type(method) is type(self.run): + self._args = self._args[1:] + + def get_method(self): + return self._method + + def _raise(self, message): + spec = inspect.formatargspec(self._args, self._varargs, + self._varkwargs, self._defaults) + raise AssertionError("Specification is %s%s: %s" % + (self._method.__name__, spec, message)) + + def verify(self): + if not self._method: + raise AssertionError("Method not found in real specification") + + def may_run(self, path): + try: + self.run(path) + except AssertionError: + return False + return True + + def run(self, path): + if not self._method: + raise AssertionError("Method not found in real specification") + if self._unsupported: + return # Can't check it. Happens with builtin functions. :-( + action = path.actions[-1] + obtained_len = len(action.args) + obtained_kwargs = action.kwargs.copy() + nodefaults_len = len(self._args) - len(self._defaults) + for i, name in enumerate(self._args): + if i < obtained_len and name in action.kwargs: + self._raise("%r provided twice" % name) + if (i >= obtained_len and i < nodefaults_len and + name not in action.kwargs): + self._raise("%r not provided" % name) + obtained_kwargs.pop(name, None) + if obtained_len > len(self._args) and not self._varargs: + self._raise("too many args provided") + if obtained_kwargs and not self._varkwargs: + self._raise("unknown kwargs: %s" % ", ".join(obtained_kwargs)) + +def spec_checker_recorder(mocker, event): + spec = event.path.root_mock.__mocker_spec__ + if spec: + actions = event.path.actions + if len(actions) == 1: + if actions[0].kind == "call": + method = getattr(spec, "__call__", None) + event.add_task(SpecChecker(method)) + elif len(actions) == 2: + if actions[0].kind == "getattr" and actions[1].kind == "call": + method = getattr(spec, actions[0].args[0], None) + event.add_task(SpecChecker(method)) + +Mocker.add_recorder(spec_checker_recorder) + + +class ProxyReplacer(Task): + """Task which installs and deinstalls proxy mocks. + + This task will replace a real object by a mock in all dictionaries + found in the running interpreter via the garbage collecting system. + """ + + def __init__(self, mock): + self.mock = mock + self.__mocker_replace__ = False + + def replay(self): + global_replace(self.mock.__mocker_object__, self.mock) + + def restore(self): + global_replace(self.mock, self.mock.__mocker_object__) + + +def global_replace(remove, install): + """Replace object 'remove' with object 'install' on all dictionaries.""" + for referrer in gc.get_referrers(remove): + if (type(referrer) is dict and + referrer.get("__mocker_replace__", True)): + for key, value in referrer.items(): + if value is remove: + referrer[key] = install + + +class Undefined(object): + + def __repr__(self): + return "Undefined" + +Undefined = Undefined() + + +class Patcher(Task): + + def __init__(self): + super(Patcher, self).__init__() + self._monitored = {} # {kind: {id(object): object}} + self._patched = {} + + def is_monitoring(self, obj, kind): + monitored = self._monitored.get(kind) + if monitored: + if id(obj) in monitored: + return True + cls = type(obj) + if issubclass(cls, type): + cls = obj + bases = set([id(base) for base in cls.__mro__]) + bases.intersection_update(monitored) + return bool(bases) + return False + + def monitor(self, obj, kind): + if kind not in self._monitored: + self._monitored[kind] = {} + self._monitored[kind][id(obj)] = obj + + def patch_attr(self, obj, attr, value): + original = obj.__dict__.get(attr, Undefined) + self._patched[id(obj), attr] = obj, attr, original + setattr(obj, attr, value) + + def get_unpatched_attr(self, obj, attr): + cls = type(obj) + if issubclass(cls, type): + cls = obj + result = Undefined + for mro_cls in cls.__mro__: + key = (id(mro_cls), attr) + if key in self._patched: + result = self._patched[key][2] + if result is not Undefined: + break + elif attr in mro_cls.__dict__: + result = mro_cls.__dict__.get(attr, Undefined) + break + if isinstance(result, object) and hasattr(type(result), "__get__"): + if cls is obj: + obj = None + return result.__get__(obj, cls) + return result + + def _get_kind_attr(self, kind): + if kind == "getattr": + return "__getattribute__" + return "__%s__" % kind + + def replay(self): + for kind in self._monitored: + attr = self._get_kind_attr(kind) + seen = set() + for obj in self._monitored[kind].itervalues(): + cls = type(obj) + if issubclass(cls, type): + cls = obj + if cls not in seen: + seen.add(cls) + unpatched = getattr(cls, attr, Undefined) + self.patch_attr(cls, attr, + PatchedMethod(kind, unpatched, + self.is_monitoring)) + self.patch_attr(cls, "__mocker_execute__", + self.execute) + + def restore(self): + for obj, attr, original in self._patched.itervalues(): + if original is Undefined: + delattr(obj, attr) + else: + setattr(obj, attr, original) + self._patched.clear() + + def execute(self, action, object): + attr = self._get_kind_attr(action.kind) + unpatched = self.get_unpatched_attr(object, attr) + try: + return unpatched(*action.args, **action.kwargs) + except AttributeError: + if action.kind == "getattr": + # The normal behavior of Python is to try __getattribute__, + # and if it raises AttributeError, try __getattr__. We've + # tried the unpatched __getattribute__ above, and we'll now + # try __getattr__. + try: + __getattr__ = unpatched("__getattr__") + except AttributeError: + pass + else: + return __getattr__(*action.args, **action.kwargs) + raise + + +class PatchedMethod(object): + + def __init__(self, kind, unpatched, is_monitoring): + self._kind = kind + self._unpatched = unpatched + self._is_monitoring = is_monitoring + + def __get__(self, obj, cls=None): + object = obj or cls + if not self._is_monitoring(object, self._kind): + return self._unpatched.__get__(obj, cls) + def method(*args, **kwargs): + if self._kind == "getattr" and args[0].startswith("__mocker_"): + return self._unpatched.__get__(obj, cls)(args[0]) + mock = object.__mocker_mock__ + return mock.__mocker_act__(self._kind, args, kwargs, object) + return method + + def __call__(self, obj, *args, **kwargs): + # At least with __getattribute__, Python seems to use *both* the + # descriptor API and also call the class attribute directly. It + # looks like an interpreter bug, or at least an undocumented + # inconsistency. + return self.__get__(obj)(*args, **kwargs) + + +def patcher_recorder(mocker, event): + mock = event.path.root_mock + if mock.__mocker_patcher__ and len(event.path.actions) == 1: + patcher = mock.__mocker_patcher__ + patcher.monitor(mock.__mocker_object__, event.path.actions[0].kind) + +Mocker.add_recorder(patcher_recorder)