From db0f08aaf8c7db00fc500480813c6c6baefc75e5 Mon Sep 17 00:00:00 2001 From: Aaron Westendorf Date: Fri, 2 May 2014 22:22:30 -0400 Subject: [PATCH] Fix PEP8 and #18 --- chai/_termcolor.py | 6 +- chai/chai.py | 392 +++++++++-------- chai/comparators.py | 487 +++++++++++---------- chai/exception.py | 130 +++--- chai/expectation.py | 549 ++++++++++++----------- chai/mock.py | 198 ++++----- chai/python2.py | 2 +- chai/stub.py | 1014 ++++++++++++++++++++++--------------------- 8 files changed, 1471 insertions(+), 1307 deletions(-) diff --git a/chai/_termcolor.py b/chai/_termcolor.py index cf2cbc4..b3a091f 100644 --- a/chai/_termcolor.py +++ b/chai/_termcolor.py @@ -5,7 +5,7 @@ ''' try: - from termcolor import colored + from termcolor import colored except ImportError: - def colored(s, *args, **kargs): - return s + def colored(s, *args, **kargs): + return s diff --git a/chai/chai.py b/chai/chai.py index f860c4a..d9e689e 100644 --- a/chai/chai.py +++ b/chai/chai.py @@ -6,10 +6,10 @@ from __future__ import absolute_import try: - import unittest2 - unittest = unittest2 + import unittest2 + unittest = unittest2 except ImportError: - import unittest + import unittest import re import sys @@ -22,195 +22,219 @@ from .stub import stub from .comparators import * + class ChaiTestType(type): - """ - Metaclass used to wrap all test methods to make sure the assert_expectations - in the correct context. - """ - - def __init__(cls, name, bases, d): - type.__init__(cls, name, bases, d) - - # also get all the attributes from the base classes to account - # for a case when test class is not the immediate child of Chai - # also alias all the cAmElCaSe methods to more helpful ones - for base in bases: - for attr_name in dir(base): - d[attr_name] = getattr(base, attr_name) - if attr_name.startswith('assert') and attr_name!='assert_': - pieces = ['assert'] + re.findall('[A-Z][a-z]+', attr_name[5:]) - name = '_'.join( [s.lower() for s in pieces] ) - d[name] = getattr(base,attr_name) - setattr(cls, name, getattr(base,attr_name)) - - for func_name, func in d.items(): - if func_name.startswith('test') and callable(func): - setattr(cls, func_name, ChaiTestType.test_wrapper(cls, func)) - - @staticmethod - def test_wrapper(cls, func): + """ - Wraps a test method, when that test method has completed it - calls assert_expectations on the stub. This is to avoid getting to exceptions about the same error. + Metaclass used to wrap all test methods to make sure the + assert_expectations in the correct context. """ - def wrapper(self, *args, **kwargs): - try: - func(self, *args, **kwargs) - except UnexpectedCall as e: - # if this is not python3, use python2 syntax - if not hasattr(e, '__traceback__'): - from .python2 import reraise - reraise(AssertionError, '\n\n'+str(e), sys.exc_info()[-1]) - exc = AssertionError('\n\n'+str(e)) - setattr(exc, '__traceback__', sys.exc_info()[-1]) - raise exc - finally: - # Teardown all stubs so that if anyone stubbed methods that would be - # called during exception handling (e.g. "open"), the original method - # is used. Without, recursion limits are common with little insight - # into what went wrong. - exceptions = [] - try: - for stub in self._stubs: - # Make sure we collect any unmet expectations before teardown. - exceptions.extend(stub.unmet_expectations()) - stub.teardown() - except: - # A rare case where this is about the best that can be done, as we - # don't want to supersede the actual exception if there is one. - traceback.print_exc() - - if exceptions: - raise ExpectationNotSatisfied(*exceptions) - - wrapper.__name__ = func.__name__ - wrapper.__doc__ = func.__doc__ - wrapper.__module__ = func.__module__ - wrapper.__wrapped__ = func - if getattr(func, '__unittest_skip__',False): - wrapper.__unittest_skip__ = True - wrapper.__unittest_skip_why__ = func.__unittest_skip_why__ - return wrapper -class ChaiBase(unittest.TestCase): - ''' - Base class for all tests - ''' - #__metaclass__ = ChaiTestType - - - # Load in the comparators - equals = Equals - almost_equals = AlmostEqual - length = Length - is_a = IsA - is_arg = Is - any_of = Any - all_of = All - not_of = Not - matches = Regex - func = Function - ignore_arg = Ignore - ignore = Ignore - in_arg = In - contains = Contains - var = Variable - like = Like - - def setUp(self): - super(ChaiBase,self).setUp() - - # Setup stub tracking - self._stubs = deque() - - # Setup mock tracking - self._mocks = deque() - - # Try to load this into the module that the test case is defined in, so - # that 'self.' can be removed. This has to be done at the start of the test - # because we need the reference to be correct at the time of test run, not - # when the class is defined or an instance is created. Walks through the - # method resolution order to set it on every module for Chai subclasses - # to handle when tests are defined in subclasses. - for cls in inspect.getmro(self.__class__): - if cls.__module__.startswith('chai'): - break - mod = sys.modules[ cls.__module__ ] - for attr in dir(cls): - if hasattr(mod, attr): continue - if attr.startswith('assert'): - setattr(mod, attr, getattr(self, attr) ) - elif isinstance(getattr(self,attr), type) and issubclass( getattr(self,attr), Comparator ): - setattr(mod, attr, getattr(self, attr) ) - setattr(mod, 'stub', self.stub) - setattr(mod, 'expect', self.expect) - setattr(mod, 'mock', self.mock) - - - # Because cAmElCaSe sucks - setup = setUp - - def tearDown(self): - super(ChaiBase,self).tearDown() - - # Docs insist that this will be called no matter what happens in runTest(), - # so this should be a safe spot to unstub everything. - # Even with teardown at the end of test_wrapper, tear down here in case the - # test was skipped or there was otherwise a problem with that test. - while len(self._stubs): - stub = self._stubs.popleft() - stub.teardown() # Teardown the reset of the stub - - # Do the mocks in reverse order in the rare case someone called mock(obj,attr) - # twice. - while len(self._mocks): - mock = self._mocks.pop() - if len(mock)==2: - delattr( mock[0], mock[1] ) - else: - setattr( mock[0], mock[1], mock[2] ) - - # Clear out any cached variables - Variable.clear() - - # Because cAmElCaSe sucks - teardown = tearDown - - def stub(self, obj, attr=None): - ''' - Stub an object. If attr is not None, will attempt to stub that attribute - on the object. Only required for modules and other rare cases where we - can't determine the binding from the object. - ''' - s = stub(obj, attr) - if s not in self._stubs: - self._stubs.append( s ) - return s + def __init__(cls, name, bases, d): + type.__init__(cls, name, bases, d) + + # also get all the attributes from the base classes to account + # for a case when test class is not the immediate child of Chai + # also alias all the cAmElCaSe methods to more helpful ones + for base in bases: + for attr_name in dir(base): + d[attr_name] = getattr(base, attr_name) + if attr_name.startswith('assert') and attr_name != 'assert_': + pieces = ['assert'] + \ + re.findall('[A-Z][a-z]+', attr_name[5:]) + name = '_'.join([s.lower() for s in pieces]) + d[name] = getattr(base, attr_name) + setattr(cls, name, getattr(base, attr_name)) + + for func_name, func in d.items(): + if func_name.startswith('test') and callable(func): + setattr(cls, func_name, ChaiTestType.test_wrapper(cls, func)) + + @staticmethod + def test_wrapper(cls, func): + """ + Wraps a test method, when that test method has completed it + calls assert_expectations on the stub. This is to avoid getting to + exceptions about the same error. + """ + + def wrapper(self, *args, **kwargs): + try: + func(self, *args, **kwargs) + except UnexpectedCall as e: + # if this is not python3, use python2 syntax + if not hasattr(e, '__traceback__'): + from .python2 import reraise + reraise( + AssertionError, '\n\n' + str(e), sys.exc_info()[-1]) + exc = AssertionError('\n\n' + str(e)) + setattr(exc, '__traceback__', sys.exc_info()[-1]) + raise exc + finally: + # Teardown all stubs so that if anyone stubbed methods that + # would be called during exception handling (e.g. "open"), + # the original method is used. Without, recursion limits are + # common with little insight into what went wrong. + exceptions = [] + try: + for s in self._stubs: + # Make sure we collect any unmet expectations before + # teardown. + exceptions.extend(s.unmet_expectations()) + s.teardown() + except: + # A rare case where this is about the best that can be + # done, as we don't want to supersede the actual + # exception if there is one. + traceback.print_exc() + + if exceptions: + raise ExpectationNotSatisfied(*exceptions) + + wrapper.__name__ = func.__name__ + wrapper.__doc__ = func.__doc__ + wrapper.__module__ = func.__module__ + wrapper.__wrapped__ = func + if getattr(func, '__unittest_skip__', False): + wrapper.__unittest_skip__ = True + wrapper.__unittest_skip_why__ = func.__unittest_skip_why__ + return wrapper - def expect(self, obj, attr=None): - ''' - Open and return an expectation on an object. Will automatically create a - stub for the object. See stub documentation for argument information. - ''' - return self.stub(obj, attr).expect() - def mock(self, obj=None, attr=None, **kwargs): +class ChaiBase(unittest.TestCase): ''' - Return a mock object. + Base class for all tests ''' - rval = Mock(**kwargs) - if obj!=None and attr!=None: - rval._object = obj - rval._attr = attr - - if hasattr(obj,attr): - orig = getattr(obj, attr) - self._mocks.append( (obj,attr,orig) ) - setattr(obj, attr, rval) - else: - self._mocks.append( (obj,attr) ) - setattr(obj, attr, rval) - return rval + + # Load in the comparators + equals = Equals + almost_equals = AlmostEqual + length = Length + is_a = IsA + is_arg = Is + any_of = Any + all_of = All + not_of = Not + matches = Regex + func = Function + ignore_arg = Ignore + ignore = Ignore + in_arg = In + contains = Contains + var = Variable + like = Like + + def setUp(self): + super(ChaiBase, self).setUp() + + # Setup stub tracking + self._stubs = deque() + + # Setup mock tracking + self._mocks = deque() + + # Try to load this into the module that the test case is defined in, so + # that 'self.' can be removed. This has to be done at the start of the + # test because we need the reference to be correct at the time of test + # run, not when the class is defined or an instance is created. Walks + # through the method resolution order to set it on every module for + # Chai subclasses to handle when tests are defined in subclasses. + for cls in inspect.getmro(self.__class__): + if cls.__module__.startswith('chai'): + break + mod = sys.modules[cls.__module__] + for attr in dir(cls): + if hasattr(mod, attr): + continue + if attr.startswith('assert'): + setattr(mod, attr, getattr(self, attr)) + elif isinstance(getattr(self, attr), type) and \ + issubclass(getattr(self, attr), Comparator): + setattr(mod, attr, getattr(self, attr)) + if not hasattr(mod, 'stub'): + setattr(mod, 'stub', self.stub) + if not hasattr(mod, 'expect'): + setattr(mod, 'expect', self.expect) + if not hasattr(mod, 'mock'): + setattr(mod, 'mock', self.mock) + + # Because cAmElCaSe sucks + setup = setUp + + def tearDown(self): + super(ChaiBase, self).tearDown() + + for cls in inspect.getmro(self.__class__): + if cls.__module__.startswith('chai'): + break + mod = sys.modules[cls.__module__] + + if getattr(mod, 'stub', None) == self.stub: + delattr(mod, 'stub') + if getattr(mod, 'expect', None) == self.expect: + delattr(mod, 'expect') + if getattr(mod, 'mock', None) == self.mock: + delattr(mod, 'mock') + + # Docs insist that this will be called no matter what happens in + # runTest(), so this should be a safe spot to unstub everything. + # Even with teardown at the end of test_wrapper, tear down here in + # case the test was skipped or there was otherwise a problem with + # that test. + while len(self._stubs): + stub = self._stubs.popleft() + stub.teardown() # Teardown the reset of the stub + + # Do the mocks in reverse order in the rare case someone called + # mock(obj,attr) twice. + while len(self._mocks): + mock = self._mocks.pop() + if len(mock) == 2: + delattr(mock[0], mock[1]) + else: + setattr(mock[0], mock[1], mock[2]) + + # Clear out any cached variables + Variable.clear() + + # Because cAmElCaSe sucks + teardown = tearDown + + def stub(self, obj, attr=None): + ''' + Stub an object. If attr is not None, will attempt to stub that + attribute on the object. Only required for modules and other rare + cases where we can't determine the binding from the object. + ''' + s = stub(obj, attr) + if s not in self._stubs: + self._stubs.append(s) + return s + + def expect(self, obj, attr=None): + ''' + Open and return an expectation on an object. Will automatically create + a stub for the object. See stub documentation for argument information. + ''' + return self.stub(obj, attr).expect() + + def mock(self, obj=None, attr=None, **kwargs): + ''' + Return a mock object. + ''' + rval = Mock(**kwargs) + if obj is not None and attr is not None: + rval._object = obj + rval._attr = attr + + if hasattr(obj, attr): + orig = getattr(obj, attr) + self._mocks.append((obj, attr, orig)) + setattr(obj, attr, rval) + else: + self._mocks.append((obj, attr)) + setattr(obj, attr, rval) + return rval Chai = ChaiTestType('Chai', (ChaiBase,), {}) diff --git a/chai/comparators.py b/chai/comparators.py index b09dc02..49a2ddb 100644 --- a/chai/comparators.py +++ b/chai/comparators.py @@ -7,285 +7,336 @@ def build_comparators(*values_or_types): - ''' - All of the comparators that can be used for arguments. - ''' - comparators = [] - for item in values_or_types: - if isinstance(item,Comparator): - comparators.append( item ) - elif isinstance(item,type): - # If you are passing around a type you will have to build a Equals comparator - comparators.append( Any(IsA(item),Is(item)) ) - else: - comparators.append( Equals(item) ) - return comparators + ''' + All of the comparators that can be used for arguments. + ''' + comparators = [] + for item in values_or_types: + if isinstance(item, Comparator): + comparators.append(item) + elif isinstance(item, type): + # If you are passing around a type you will have to build a Equals + # comparator + comparators.append(Any(IsA(item), Is(item))) + else: + comparators.append(Equals(item)) + return comparators + class Comparator(object): - ''' - Base class of all comparators, used for type testing - ''' - def __eq__(self, value): - return self.test(value) + + ''' + Base class of all comparators, used for type testing + ''' + + def __eq__(self, value): + return self.test(value) + class Equals(Comparator): - ''' - Simplest comparator. - ''' - def __init__(self, value): - self._value = value - def test(self, value): - return self._value == value + ''' + Simplest comparator. + ''' + + def __init__(self, value): + self._value = value + + def test(self, value): + return self._value == value + + def __repr__(self): + return repr(self._value) + __str__ = __repr__ - def __repr__(self): - return repr(self._value) - __str__ = __repr__ class Length(Comparator): - ''' - Compare the length of the argument. - ''' - def __init__(self, value): - self._value = value - def test(self, value): - if isinstance(self._value,int): - return len(value)==self._value - return len(value) in self._value + ''' + Compare the length of the argument. + ''' + + def __init__(self, value): + self._value = value + + def test(self, value): + if isinstance(self._value, int): + return len(value) == self._value + return len(value) in self._value + + def __repr__(self): + return repr(self._value) + __str__ = __repr__ - def __repr__(self): - return repr(self._value) - __str__ = __repr__ class IsA(Comparator): - ''' - Test to see if a value is an instance of something. Arguments match - isinstance - ''' - def __init__(self, types): - self._types = types - - def test(self, value): - return isinstance(value, self._types) - - def _format_name(self): - if isinstance(self._types, type): - return self._types.__name__ - else: - return str([o.__name__ for o in self._types]) - - def __repr__(self): - return "IsA(%s)" % (self._format_name()) - __str__ = __repr__ + + ''' + Test to see if a value is an instance of something. Arguments match + isinstance + ''' + + def __init__(self, types): + self._types = types + + def test(self, value): + return isinstance(value, self._types) + + def _format_name(self): + if isinstance(self._types, type): + return self._types.__name__ + else: + return str([o.__name__ for o in self._types]) + + def __repr__(self): + return "IsA(%s)" % (self._format_name()) + __str__ = __repr__ + class Is(Comparator): - ''' - Checks for identity not equality - ''' - def __init__(self, obj): - self._obj = obj - def test(self, value): - return self._obj is value + ''' + Checks for identity not equality + ''' + + def __init__(self, obj): + self._obj = obj + + def test(self, value): + return self._obj is value + + def __repr__(self): + return "Is(%s)" % (str(self._obj)) + __str__ = __repr__ - def __repr__(self): - return "Is(%s)" % (str(self._obj)) - __str__ = __repr__ class AlmostEqual(Comparator): - ''' - Compare a float value to n number of palces - ''' - def __init__(self, float_value, places=7): - self._float_value = float_value - self._places = places + ''' + Compare a float value to n number of palces + ''' + + def __init__(self, float_value, places=7): + self._float_value = float_value + self._places = places + + def test(self, value): + return round(value - self._float_value, self._places) == 0 - def test(self, value): - return round(value - self._float_value, self._places) == 0 + def __repr__(self): + return "AlmostEqual(value: %s, places: %d)" % ( + str(self._float_value), self._places) + __str__ = __repr__ - def __repr__(self): - return "AlmostEqual(value: %s, places: %d)" % (str(self._float_value), self._places) - __str__ = __repr__ class Regex(Comparator): - ''' - Checks to see if a string matches a regex - ''' - def __init__(self, pattern, flags=0): - self._pattern = pattern - self._flags = flags - self._regex = re.compile(pattern) + ''' + Checks to see if a string matches a regex + ''' + + def __init__(self, pattern, flags=0): + self._pattern = pattern + self._flags = flags + self._regex = re.compile(pattern) - def test(self, value): - return self._regex.search(value) is not None + def test(self, value): + return self._regex.search(value) is not None + + def __repr__(self): + return "Regex(pattern: %s, flags: %s)" % (self._pattern, self._flags) + __str__ = __repr__ - def __repr__(self): - return "Regex(pattern: %s, flags: %s)" % (self._pattern, self._flags) - __str__ = __repr__ class Any(Comparator): - ''' - Test to see if any comparator matches - ''' - def __init__(self, *comparators): - self._comparators = build_comparators(*comparators) - def test(self, value): - for comp in self._comparators: - if comp.test(value): return True - return False + ''' + Test to see if any comparator matches + ''' + + def __init__(self, *comparators): + self._comparators = build_comparators(*comparators) + + def test(self, value): + for comp in self._comparators: + if comp.test(value): + return True + return False + + def __repr__(self): + return "Any(%s)" % str(self._comparators) + __str__ = __repr__ - def __repr__(self): - return "Any(%s)" % str(self._comparators) - __str__ = __repr__ class In(Comparator): - ''' - Test if a key is in a list or dict - ''' - def __init__(self, hay_stack): - self._hay_stack = hay_stack - def test(self, needle): - return needle in self._hay_stack + ''' + Test if a key is in a list or dict + ''' + + def __init__(self, hay_stack): + self._hay_stack = hay_stack + + def test(self, needle): + return needle in self._hay_stack + + def __repr__(self): + return "In(%s)" % (str(self._hay_stack)) + __str__ = __repr__ - def __repr__(self): - return "In(%s)" % (str(self._hay_stack)) - __str__ = __repr__ class Contains(Comparator): - ''' - Test if a key is in a list or dict - ''' - def __init__(self, needle): - self._needle = needle - def test(self, hay_stack): - return self._needle in hay_stack + ''' + Test if a key is in a list or dict + ''' + + def __init__(self, needle): + self._needle = needle + + def test(self, hay_stack): + return self._needle in hay_stack + + def __repr__(self): + return "Contains('%s')" % (str(self._needle)) + __str__ = __repr__ - def __repr__(self): - return "Contains('%s')" % (str(self._needle)) - __str__ = __repr__ class All(Comparator): - ''' - Test to see if all comparators match - ''' - def __init__(self, *comparators): - self._comparators = build_comparators(*comparators) - def test(self, value): - for comp in self._comparators: - if not comp.test(value): return False - return True + ''' + Test to see if all comparators match + ''' + + def __init__(self, *comparators): + self._comparators = build_comparators(*comparators) + + def test(self, value): + for comp in self._comparators: + if not comp.test(value): + return False + return True + + def __repr__(self): + return "All(%s)" % (self._comparators) + __str__ = __repr__ - def __repr__(self): - return "All(%s)" % (self._comparators) - __str__ = __repr__ class Not(Comparator): - ''' - Return the opposite of a comparator - ''' - def __init__(self, *comparators): - self._comparators = build_comparators(*comparators) - def test(self, value): - return all([not c.test(value) for c in self._comparators]) + ''' + Return the opposite of a comparator + ''' + + def __init__(self, *comparators): + self._comparators = build_comparators(*comparators) + + def test(self, value): + return all([not c.test(value) for c in self._comparators]) + + def __repr__(self): + return "Not(%s)" % (repr(self._comparators)) + __str__ = __repr__ - def __repr__(self): - return "Not(%s)" % (repr(self._comparators)) - __str__ = __repr__ class Function(Comparator): - ''' - Call a func to compare the values - ''' - def __init__(self, func): - self._func = func - def test(self, value): - return self._func(value) + ''' + Call a func to compare the values + ''' + + def __init__(self, func): + self._func = func + + def test(self, value): + return self._func(value) + + def __repr__(self): + return "Function(%s)" % (str(self._func)) + __str__ = __repr__ - def __repr__(self): - return "Function(%s)" % (str(self._func)) - __str__ = __repr__ class Ignore(Comparator): - ''' - Igore this argument - ''' - def test(self, value): - return True - def __repr__(self): - return "Ignore()" - __str__ = __repr__ + ''' + Igore this argument + ''' + + def test(self, value): + return True + + def __repr__(self): + return "Ignore()" + __str__ = __repr__ + class Variable(Comparator): - ''' - A mechanism for tracking variables and their values. - ''' - _cache = {} - @classmethod - def clear(self): ''' - Delete all cached values. Should only be used by the test suite. + A mechanism for tracking variables and their values. ''' - self._cache.clear() + _cache = {} + + @classmethod + def clear(self): + ''' + Delete all cached values. Should only be used by the test suite. + ''' + self._cache.clear() - def __init__(self, name): - self._name = name + def __init__(self, name): + self._name = name - @property - def value(self): - try: return self._cache[self._name] - except KeyError: raise ValueError("no value '%s'"%(self._name)) + @property + def value(self): + try: + return self._cache[self._name] + except KeyError: + raise ValueError("no value '%s'" % (self._name)) - def test(self, value): - try: - return self._cache[self._name] == value - except KeyError: - self._cache[self._name] = value - return True + def test(self, value): + try: + return self._cache[self._name] == value + except KeyError: + self._cache[self._name] = value + return True + + def __repr__(self): + return "Variable('%s')" % (self._name) + __str__ = __repr__ - def __repr__(self): - return "Variable('%s')"%(self._name) - __str__ = __repr__ class Like(Comparator): - ''' - A comparator that will assert that fields of a container look like - another. - ''' - - def __init__(self, src): - # This might have to change to support more iterable types - if not isinstance(src, (dict,set,list,tuple)): - raise ValueError("Like comparator only implemented for basic container types") - self._src = src - - def test(self, value): - # This might need to change so that the ctor arg can be a list, but - # any iterable type can be tested. - if not isinstance(value, type(self._src)): - return False - - rval = True - if isinstance(self._src, dict): - for k,v in self._src.items(): - rval = rval and value.get(k)==v - - else: - for item in self._src: - rval = rval and item in value - - return rval - - def __repr__(self): - return "Like(%s)"%(str(self._src)) - __str__ = __repr__ + + ''' + A comparator that will assert that fields of a container look like + another. + ''' + + def __init__(self, src): + # This might have to change to support more iterable types + if not isinstance(src, (dict, set, list, tuple)): + raise ValueError( + "Like comparator only implemented for basic container types") + self._src = src + + def test(self, value): + # This might need to change so that the ctor arg can be a list, but + # any iterable type can be tested. + if not isinstance(value, type(self._src)): + return False + + rval = True + if isinstance(self._src, dict): + for k, v in self._src.items(): + rval = rval and value.get(k) == v + + else: + for item in self._src: + rval = rval and item in value + + return rval + + def __repr__(self): + return "Like(%s)" % (str(self._src)) + __str__ = __repr__ diff --git a/chai/exception.py b/chai/exception.py index ad04ddc..080d9ad 100644 --- a/chai/exception.py +++ b/chai/exception.py @@ -11,70 +11,92 @@ from ._termcolor import colored # Refactored from ArgumentsExpectationRule + + def pretty_format_args(*args, **kwargs): - """ - Take the args, and kwargs that are passed them and format in a prototype style. - """ - args = list([repr(a) for a in args]) - for key, value in kwargs.items(): - args.append("%s=%s" % (key, repr(value))) - return "(%s)" % ", ".join([a for a in args]) + """ + Take the args, and kwargs that are passed them and format in a + prototype style. + """ + args = list([repr(a) for a in args]) + for key, value in kwargs.items(): + args.append("%s=%s" % (key, repr(value))) + return "(%s)" % ", ".join([a for a in args]) + class ChaiException(RuntimeError): - ''' - Base class for an actual error in chai. - ''' + + ''' + Base class for an actual error in chai. + ''' + class UnsupportedStub(ChaiException): - ''' - Can't stub the requested object or attribute. - ''' + + ''' + Can't stub the requested object or attribute. + ''' + class ChaiAssertion(AssertionError): - ''' - Base class for all assertion errors. - ''' + + ''' + Base class for all assertion errors. + ''' + class UnexpectedCall(BaseException): - ''' - Raised when a unexpected call occurs to a stub. - ''' - def __init__(self, msg=None, prefix=None, suffix=None, call=None, args=None, kwargs=None, expected_args=None, expected_kwargs=None): - if msg: - msg = colored('\n\n'+msg.strip(), 'red') - else: - msg = '' - - if prefix: - msg = '\n\n' + prefix.strip() + msg - - if call: - msg += colored('\n\nNo expectation in place for\n', 'white', attrs=['bold']) - msg += colored(call, 'red') - if args or kwargs: - msg += colored(pretty_format_args( *(args or ()), **(kwargs or {}) ), 'red') - if expected_args or expected_kwargs: - msg += colored('\n\nExpected\n', 'white', attrs=['bold']) - msg += colored(call, 'red') - msg += colored(pretty_format_args( *(expected_args or ()), **(expected_kwargs or {}) ), 'red') - - # If handling an exception, add printing of it here. - if sys.exc_info()[0]: - msg += colored('\n\nWhile handling\n', 'white', attrs=['bold']) - msg += colored(''.join(traceback.format_exception( *sys.exc_info() )), 'red') - - if suffix: - msg = msg + '\n\n' + suffix.strip() - - super(UnexpectedCall, self).__init__( msg ) + + ''' + Raised when a unexpected call occurs to a stub. + ''' + + def __init__(self, msg=None, prefix=None, suffix=None, call=None, + args=None, kwargs=None, expected_args=None, + expected_kwargs=None): + if msg: + msg = colored('\n\n' + msg.strip(), 'red') + else: + msg = '' + + if prefix: + msg = '\n\n' + prefix.strip() + msg + + if call: + msg += colored('\n\nNo expectation in place for\n', + 'white', attrs=['bold']) + msg += colored(call, 'red') + if args or kwargs: + msg += colored(pretty_format_args(*(args or ()), + **(kwargs or {})), 'red') + if expected_args or expected_kwargs: + msg += colored('\n\nExpected\n', 'white', attrs=['bold']) + msg += colored(call, 'red') + msg += colored(pretty_format_args( + *(expected_args or ()), + **(expected_kwargs or {})), 'red') + + # If handling an exception, add printing of it here. + if sys.exc_info()[0]: + msg += colored('\n\nWhile handling\n', 'white', attrs=['bold']) + msg += colored(''.join( + traceback.format_exception(*sys.exc_info())), + 'red') + + if suffix: + msg = msg + '\n\n' + suffix.strip() + + super(UnexpectedCall, self).__init__(msg) + class ExpectationNotSatisfied(ChaiAssertion): - ''' - Raised when all expectations are not met - ''' - def __init__(self, *expectations): - self._expectations = expectations + ''' + Raised when all expectations are not met + ''' + + def __init__(self, *expectations): + self._expectations = expectations - def __str__(self): - return str("\n".join([ str(e) for e in self._expectations])) + def __str__(self): + return str("\n".join([str(e) for e in self._expectations])) diff --git a/chai/expectation.py b/chai/expectation.py index f19f52e..4b3390c 100644 --- a/chai/expectation.py +++ b/chai/expectation.py @@ -13,273 +13,300 @@ class ExpectationRule(object): - def __init__(self, *args, **kwargs): - self._passed = False - def validate(self, *args, **kwargs): - raise NotImplementedError("Must be implemented by subclasses") - -class ArgumentsExpectationRule(ExpectationRule): - def __init__(self, *args, **kwargs): - super(ArgumentsExpectationRule, self).__init__(*args, **kwargs) - self.set_args( *args, **kwargs ) - - def set_args(self, *args, **kwargs): - self.args = [] - self.kwargs = {} - - # Convert all of the arguments to comparators - self.args = build_comparators(*args) - self.kwargs = dict([(k, build_comparators(v)[0]) for k, v in kwargs.items()]) - - def validate(self, *args, **kwargs): - self.in_args = args[:] - self.in_kwargs = kwargs.copy() - - # First just check that the number of arguments is the same or different - if len(args)!=len(self.args) or len(kwargs)!=len(self.kwargs): - self._passed = False - return False - - for x in range(len(self.args)): - if not self.args[x].test( args[x] ): + def __init__(self, *args, **kwargs): self._passed = False - return False - for arg_name,arg_test in self.kwargs.items(): - try: - value=kwargs.pop(arg_name) - except KeyError: - self._passed = False - return False - if not arg_test.test( value ): - self._passed = False - return False - - # If there are arguments left over, is error - if len(kwargs): - self._passed = False - return False - - self._passed = True - return self._passed - - @classmethod - def pretty_format_args(self, *args, **kwargs): - """ - Take the args, and kwargs that are passed them and format in a prototype style. - """ - args = list([repr(a) for a in args]) - for key, value in kwargs.items(): - args.append("%s=%s" % (key, repr(value))) - - return "(%s)" % ", ".join([a for a in args]) - - def __str__(self): - if hasattr(self, 'in_args') and hasattr(self, 'in_kwargs'): - return "\tExpected: %s\n\t\t Used: %s" % \ - (self.pretty_format_args(*self.args, **self.kwargs), self.pretty_format_args(*self.in_args, **self.in_kwargs)) - - return "\tExpected: %s" % \ - (self.pretty_format_args(*self.args, **self.kwargs)) + def validate(self, *args, **kwargs): + raise NotImplementedError("Must be implemented by subclasses") + + +class ArgumentsExpectationRule(ExpectationRule): + + def __init__(self, *args, **kwargs): + super(ArgumentsExpectationRule, self).__init__(*args, **kwargs) + self.set_args(*args, **kwargs) + + def set_args(self, *args, **kwargs): + self.args = [] + self.kwargs = {} + + # Convert all of the arguments to comparators + self.args = build_comparators(*args) + self.kwargs = dict([(k, build_comparators(v)[0]) + for k, v in kwargs.items()]) + + def validate(self, *args, **kwargs): + self.in_args = args[:] + self.in_kwargs = kwargs.copy() + + # First just check that the number of arguments is the same or + # different + if len(args) != len(self.args) or len(kwargs) != len(self.kwargs): + self._passed = False + return False + + for x in range(len(self.args)): + if not self.args[x].test(args[x]): + self._passed = False + return False + + for arg_name, arg_test in self.kwargs.items(): + try: + value = kwargs.pop(arg_name) + except KeyError: + self._passed = False + return False + if not arg_test.test(value): + self._passed = False + return False + + # If there are arguments left over, is error + if len(kwargs): + self._passed = False + return False + + self._passed = True + return self._passed + + @classmethod + def pretty_format_args(self, *args, **kwargs): + """ + Take the args, and kwargs that are passed them and format in a + prototype style. + """ + args = list([repr(a) for a in args]) + for key, value in kwargs.items(): + args.append("%s=%s" % (key, repr(value))) + + return "(%s)" % ", ".join([a for a in args]) + + def __str__(self): + if hasattr(self, 'in_args') and hasattr(self, 'in_kwargs'): + return "\tExpected: %s\n\t\t Used: %s" % \ + (self.pretty_format_args(*self.args, **self.kwargs), + self.pretty_format_args(*self.in_args, **self.in_kwargs)) + + return "\tExpected: %s" % \ + (self.pretty_format_args(*self.args, **self.kwargs)) + class Expectation(object): - ''' - Encapsulate an expectation. - ''' - - def __init__(self, stub): - self._met = False - self._stub = stub - self._arguments_rule = ArgumentsExpectationRule() - self._raises = None - self._returns = None - self._max_count = None - self._min_count = 1 - self._counts_defined = False - self._run_count = 0 - self._any_order = False - self._side_effect = False - self._side_effect_args = None - self._side_effect_kwargs = None - self._teardown = False - self._any_args = True - - # If the last expectation has no counts defined yet, set it to the - # run count if it's already been used, else set it to 1 just like - # the original implementation. This makes iterative testing much - # simpler without needing to know ahead of time exactly how many - # times an expectation will be called. - prev_expect = None if not stub.expectations else stub.expectations[-1] - if prev_expect and not prev_expect._counts_defined: - if prev_expect._run_count: - # Close immediately - prev_expect._met = True - prev_expect._max_count = prev_expect._run_count - else: - prev_expect._max_count = prev_expect._min_count - - # Support expectations as context managers. See - # https://github.com/agoragames/chai/issues/1 - def __enter__(self): - return self._returns - - def __exit__(*args): - pass - - def args(self, *args, **kwargs): - """ - Creates a ArgumentsExpectationRule and adds it to the expectation - """ - self._any_args = False - self._arguments_rule.set_args(*args, **kwargs) - return self - - def any_args(self): - ''' - Accept any arguments passed to this call. - ''' - self._any_args = True - return self - - def returns(self, value): - """ - What this expectation should return - """ - self._returns = value - return self - - def raises(self, exception): - """ - Adds a raises to the expectation, this will be raised when the expectation is met. - - This can be either the exception class or instance of a exception - """ - self._raises = exception - return self - - def times(self, count): - self._min_count = self._max_count = count - self._counts_defined = True - return self - - def at_least(self, min_count): - self._min_count = min_count - self._max_count = None - self._counts_defined = True - return self - - def at_least_once(self): - self.at_least(1) - self._counts_defined = True - return self - - def at_most(self, max_count): - self._max_count = max_count - self._counts_defined = True - return self - - def at_most_once(self): - self.at_most(1) - self._counts_defined = True - return self - - def once(self): - self._min_count = 1 - self._max_count = 1 - self._counts_defined = True - return self - - def any_order(self): - self._any_order = True - return self - - def is_any_order(self): - return self._any_order - - def side_effect(self, func, *args, **kwargs): - self._side_effect = func - self._side_effect_args = args - self._side_effect_kwargs = kwargs - return self - - def teardown(self): - self._teardown = True - - # If counts have not been defined yet, then there's an implied use case - # here where once the expectation has been run, it should be torn down, - # i.e. max_count is same as min_count, i.e. 1 - if not self._counts_defined: - self._max_count = self._min_count - return self - - def return_value(self): - """ - Returns the value for this expectation or raises the proper exception. - """ - if self._raises: - # Handle exceptions - if inspect.isclass(self._raises): - raise self._raises() - else: - raise self._raises - else: - if isinstance(self._returns, tuple): - return tuple([x.value if isinstance(x,Variable) else x for x in self._returns]) - return self._returns.value if isinstance(self._returns,Variable) else self._returns - - def close(self, *args, **kwargs): + ''' - Mark this expectation as closed. It will no longer be used for matches. + Encapsulate an expectation. ''' - # If any_order, then this effectively is never closed. The Stub.__call__ - # will just bypass it when it doesn't match. If there is a strict count - # it will also be bypassed, but if there's just a min set up, then it'll - # effectively stay open and catch any matching call no matter the order - if not self._any_order: - self._met = True - - def closed(self, with_counts=False): - rval = self._met - if with_counts: - rval = rval or self.counts_met() - return rval - - def counts_met(self): - return self._run_count >= self._min_count and not (self._max_count and not self._max_count == self._run_count) - - def match(self, *args, **kwargs): - """ - Check the if these args match this expectation. - """ - return self._any_args or self._arguments_rule.validate(*args, **kwargs) - - def test(self, *args, **kwargs): - """ - Validate all the rules with in this expectation to see if this expectation has been met. - """ - if not self._met: - if self.match(*args, **kwargs): - self._run_count += 1 - if not self._max_count == None and self._run_count == self._max_count: - self._met = True - if self._side_effect: - if self._side_effect_args or self._side_effect_kwargs: - self._side_effect(*self._side_effect_args, **self._side_effect_kwargs) - else: - self._side_effect(*args, **kwargs) - else: + + def __init__(self, stub): self._met = False + self._stub = stub + self._arguments_rule = ArgumentsExpectationRule() + self._raises = None + self._returns = None + self._max_count = None + self._min_count = 1 + self._counts_defined = False + self._run_count = 0 + self._any_order = False + self._side_effect = False + self._side_effect_args = None + self._side_effect_kwargs = None + self._teardown = False + self._any_args = True + + # If the last expectation has no counts defined yet, set it to the + # run count if it's already been used, else set it to 1 just like + # the original implementation. This makes iterative testing much + # simpler without needing to know ahead of time exactly how many + # times an expectation will be called. + prev_expect = None if not stub.expectations else stub.expectations[-1] + if prev_expect and not prev_expect._counts_defined: + if prev_expect._run_count: + # Close immediately + prev_expect._met = True + prev_expect._max_count = prev_expect._run_count + else: + prev_expect._max_count = prev_expect._min_count + + # Support expectations as context managers. See + # https://github.com/agoragames/chai/issues/1 + def __enter__(self): + return self._returns + + def __exit__(*args): + pass + + def args(self, *args, **kwargs): + """ + Creates a ArgumentsExpectationRule and adds it to the expectation + """ + self._any_args = False + self._arguments_rule.set_args(*args, **kwargs) + return self + + def any_args(self): + ''' + Accept any arguments passed to this call. + ''' + self._any_args = True + return self + + def returns(self, value): + """ + What this expectation should return + """ + self._returns = value + return self + + def raises(self, exception): + """ + Adds a raises to the expectation, this will be raised when the + expectation is met. + + This can be either the exception class or instance of a exception. + """ + self._raises = exception + return self + + def times(self, count): + self._min_count = self._max_count = count + self._counts_defined = True + return self + + def at_least(self, min_count): + self._min_count = min_count + self._max_count = None + self._counts_defined = True + return self + + def at_least_once(self): + self.at_least(1) + self._counts_defined = True + return self + + def at_most(self, max_count): + self._max_count = max_count + self._counts_defined = True + return self + + def at_most_once(self): + self.at_most(1) + self._counts_defined = True + return self + + def once(self): + self._min_count = 1 + self._max_count = 1 + self._counts_defined = True + return self + + def any_order(self): + self._any_order = True + return self + + def is_any_order(self): + return self._any_order + + def side_effect(self, func, *args, **kwargs): + self._side_effect = func + self._side_effect_args = args + self._side_effect_kwargs = kwargs + return self + + def teardown(self): + self._teardown = True + + # If counts have not been defined yet, then there's an implied use case + # here where once the expectation has been run, it should be torn down, + # i.e. max_count is same as min_count, i.e. 1 + if not self._counts_defined: + self._max_count = self._min_count + return self + + def return_value(self): + """ + Returns the value for this expectation or raises the proper exception. + """ + if self._raises: + # Handle exceptions + if inspect.isclass(self._raises): + raise self._raises() + else: + raise self._raises + else: + if isinstance(self._returns, tuple): + return tuple([x.value if isinstance(x, Variable) + else x for x in self._returns]) + return self._returns.value if isinstance(self._returns, Variable) \ + else self._returns + + def close(self, *args, **kwargs): + ''' + Mark this expectation as closed. It will no longer be used for matches. + ''' + # If any_order, then this effectively is never closed. The + # Stub.__call__ will just bypass it when it doesn't match. If there + # is a strict count it will also be bypassed, but if there's just a + # min set up, then it'll effectively stay open and catch any matching + # call no matter the order. + if not self._any_order: + self._met = True + + def closed(self, with_counts=False): + rval = self._met + if with_counts: + rval = rval or self.counts_met() + return rval + + def counts_met(self): + return self._run_count >= self._min_count and not ( + self._max_count and not self._max_count == self._run_count) + + def match(self, *args, **kwargs): + """ + Check the if these args match this expectation. + """ + return self._any_args or \ + self._arguments_rule.validate(*args, **kwargs) + + def test(self, *args, **kwargs): + """ + Validate all the rules with in this expectation to see if this + expectation has been met. + """ + if not self._met: + if self.match(*args, **kwargs): + self._run_count += 1 + if self._max_count is not None and \ + self._run_count == self._max_count: + self._met = True + if self._side_effect: + if self._side_effect_args or self._side_effect_kwargs: + self._side_effect( + *self._side_effect_args, + **self._side_effect_kwargs) + else: + self._side_effect(*args, **kwargs) + else: + self._met = False + + # If this is met and we're supposed to tear down, must do it now + # so that this stub is not called again + if self._met and self._teardown: + self._stub.teardown() + + return self.return_value() - # If this is met and we're supposed to tear down, must do it now so that - # this stub is not called again - if self._met and self._teardown: - self._stub.teardown() - - return self.return_value() - - def __str__(self): - runs_string = " Ran: %s, Min Runs: %s, Max Runs: %s" % (self._run_count, self._min_count, "∞" if self._max_count == None else self._max_count) - return_string = " Raises: %s" % self._raises if self._raises else " Returns: %s" % repr(self._returns) - return "\n\t%s\n\t%s\n\t\t%s\n\t\t%s" % (colored("%s - %s" % (self._stub.name, "Passed" if self._arguments_rule._passed else "Failed") - , "green" if self._arguments_rule._passed else "red"), self._arguments_rule, return_string, runs_string) + def __str__(self): + runs_string = " Ran: %s, Min Runs: %s, Max Runs: %s" % ( + self._run_count, self._min_count, + "∞" if self._max_count is None else self._max_count) + return_string = " Raises: %s" % ( + self._raises if self._raises else " Returns: %s" % repr( + self._returns)) + return "\n\t%s\n\t%s\n\t\t%s\n\t\t%s" % ( + colored("%s - %s" % ( + self._stub.name, + "Passed" if self._arguments_rule._passed else "Failed"), + "green" if self._arguments_rule._passed else "red"), + self._arguments_rule, return_string, runs_string) diff --git a/chai/mock.py b/chai/mock.py index a85f132..33baf9c 100644 --- a/chai/mock.py +++ b/chai/mock.py @@ -3,104 +3,108 @@ https://github.com/agoragames/chai/blob/master/LICENSE.txt ''' -from types import MethodType from .stub import Stub from .exception import UnexpectedCall -from .expectation import ArgumentsExpectationRule + class Mock(object): - ''' - A class where all calls are stubbed. - ''' - - def __init__(self, **kwargs): - for name, value in kwargs.items(): - setattr(self, name, value) - self._name = 'mock' - - # For whatever reason, new-style objects require this method defined before - # any instance is created. Defining it through __getattr__ is not enough. This - # appears to be a bug/feature in new classes where special members, or at least - # __call__, have to defined when the instance is created. Also, if it's already - # defined on the instance, getattr() will return the stub but the original - # method will always be called. Anyway, it's all crazy, but that's why the - # implementation of __call__ is so weird. - def __call__(self, *args, **kwargs): - if isinstance(getattr(self,'__call__'), Stub): - return getattr(self,'__call__')(*args, **kwargs) - - raise UnexpectedCall(call=self._name, args=args, kwargs=kwargs) - - def __getattr__(self,name): - rval = self.__dict__.get(name) - - if not rval or not isinstance(rval,(Stub,Mock)): - rval = Mock() - rval._name = '%s.%s'%(self._name,name) - setattr(self, name, rval) - - return rval - - ### - ### Define nonzero so that basic "if :" stanzas will work. - ### - def __nonzero__(self): - if isinstance(getattr(self,'__nonzero__'), Stub): - return getattr(self,'__nonzero__')() - return True - - ### - ### Emulate container types, the 99% of cases where we want to mock the - ### special methods. They all raise UnexpectedCall unless they're mocked out - ### http://docs.python.org/reference/datamodel.html#emulating-container-types - ### - # HACK: it would be nice to abstract this lookup-stub behavior in a decorator - # but that gets in the way of stubbing. Would like to figure that out @AW - def __len__(self): - if isinstance(getattr(self,'__len__'), Stub): - return getattr(self,'__len__')() - raise UnexpectedCall(call=self._name+'.__len__') - - def __getitem__(self, key): - if isinstance(getattr(self,'__getitem__'), Stub): - return getattr(self,'__getitem__')(key) - raise UnexpectedCall(call=self._name+'.__getitem__', args=(key,)) - - def __setitem__(self, key, value): - if isinstance(getattr(self,'__setitem__'), Stub): - return getattr(self,'__setitem__')(key, value) - raise UnexpectedCall(call=self._name+'.__setitem__', args=(key,value)) - - def __delitem__(self, key): - if isinstance(getattr(self,'__delitem__'), Stub): - return getattr(self,'__delitem__')(key) - raise UnexpectedCall(call=self._name+'.__delitem__', args=(key,)) - - def __iter__(self): - if isinstance(getattr(self,'__iter__'), Stub): - return getattr(self,'__iter__')() - raise UnexpectedCall(call=self._name+'.__iter__') - - def __reversed__(self): - if isinstance(getattr(self,'__reversed__'), Stub): - return getattr(self,'__reversed__')() - raise UnexpectedCall(call=self._name+'.__reversed__') - - def __contains__(self, item): - if isinstance(getattr(self,'__contains__'), Stub): - return getattr(self,'__contains__')(item) - raise UnexpectedCall(call=self._name+'.__contains__', args=(item,)) - - ### - ### Emulate context managers - ### http://docs.python.org/reference/datamodel.html#with-statement-context-managers - ### - def __enter__(self): - if isinstance(getattr(self,'__enter__'), Stub): - return getattr(self,'__enter__')() - raise UnexpectedCall(call=self._name+'.__enter__') - - def __exit__(self, exc_type, exc_value, traceback): - if isinstance(getattr(self,'__exit__'), Stub): - return getattr(self,'__exit__')(exc_type, exc_value, traceback) - raise UnexpectedCall(call=self._name+'.__exit__', args=(exc_type,exc_value,traceback)) + + ''' + A class where all calls are stubbed. + ''' + + def __init__(self, **kwargs): + for name, value in kwargs.items(): + setattr(self, name, value) + self._name = 'mock' + + # For whatever reason, new-style objects require this method defined before + # any instance is created. Defining it through __getattr__ is not enough. + # This appears to be a bug/feature in new classes where special members, + # or at least __call__, have to defined when the instance is created. + # Also, if it's already defined on the instance, getattr() will return + # the stub but the original method will always be called. Anyway, it's + # all crazy, but that's why the implementation of __call__ is so weird. + def __call__(self, *args, **kwargs): + if isinstance(getattr(self, '__call__'), Stub): + return getattr(self, '__call__')(*args, **kwargs) + + raise UnexpectedCall(call=self._name, args=args, kwargs=kwargs) + + def __getattr__(self, name): + rval = self.__dict__.get(name) + + if not rval or not isinstance(rval, (Stub, Mock)): + rval = Mock() + rval._name = '%s.%s' % (self._name, name) + setattr(self, name, rval) + + return rval + + ### + # Define nonzero so that basic "if :" stanzas will work. + ### + def __nonzero__(self): + if isinstance(getattr(self, '__nonzero__'), Stub): + return getattr(self, '__nonzero__')() + return True + + ### + # Emulate container types, the 99% of cases where we want to mock the + # special methods. They all raise UnexpectedCall unless they're mocked out + # http://docs.python.org/reference/datamodel.html#emulating-container-types + ### + # HACK: it would be nice to abstract this lookup-stub behavior in a + # decorator but that gets in the way of stubbing. Would like to figure + # that out @AW + def __len__(self): + if isinstance(getattr(self, '__len__'), Stub): + return getattr(self, '__len__')() + raise UnexpectedCall(call=self._name + '.__len__') + + def __getitem__(self, key): + if isinstance(getattr(self, '__getitem__'), Stub): + return getattr(self, '__getitem__')(key) + raise UnexpectedCall(call=self._name + '.__getitem__', args=(key,)) + + def __setitem__(self, key, value): + if isinstance(getattr(self, '__setitem__'), Stub): + return getattr(self, '__setitem__')(key, value) + raise UnexpectedCall( + call=self._name + '.__setitem__', args=(key, value)) + + def __delitem__(self, key): + if isinstance(getattr(self, '__delitem__'), Stub): + return getattr(self, '__delitem__')(key) + raise UnexpectedCall(call=self._name + '.__delitem__', args=(key,)) + + def __iter__(self): + if isinstance(getattr(self, '__iter__'), Stub): + return getattr(self, '__iter__')() + raise UnexpectedCall(call=self._name + '.__iter__') + + def __reversed__(self): + if isinstance(getattr(self, '__reversed__'), Stub): + return getattr(self, '__reversed__')() + raise UnexpectedCall(call=self._name + '.__reversed__') + + def __contains__(self, item): + if isinstance(getattr(self, '__contains__'), Stub): + return getattr(self, '__contains__')(item) + raise UnexpectedCall(call=self._name + '.__contains__', args=(item,)) + + ### + # Emulate context managers + # http://docs.python.org/reference/datamodel.html#with-statement-context-managers + ### + def __enter__(self): + if isinstance(getattr(self, '__enter__'), Stub): + return getattr(self, '__enter__')() + raise UnexpectedCall(call=self._name + '.__enter__') + + def __exit__(self, exc_type, exc_value, traceback): + if isinstance(getattr(self, '__exit__'), Stub): + return getattr(self, '__exit__')(exc_type, exc_value, traceback) + raise UnexpectedCall( + call=self._name + '.__exit__', + args=(exc_type, exc_value, traceback)) diff --git a/chai/python2.py b/chai/python2.py index a5ba46a..9448d1b 100644 --- a/chai/python2.py +++ b/chai/python2.py @@ -1,3 +1,3 @@ def reraise(exc, msg, traceback): - raise exc, msg, traceback + raise exc, msg, traceback diff --git a/chai/stub.py b/chai/stub.py index 94e8ec8..4a5ca64 100644 --- a/chai/stub.py +++ b/chai/stub.py @@ -5,563 +5,599 @@ ''' import inspect import types -import os import sys import gc -from .expectation import Expectation, ArgumentsExpectationRule +from .expectation import Expectation from .exception import * from ._termcolor import colored # For clarity here and in tests, could make these class or static methods on # Stub. Chai base class would hide that. -def stub(obj, attr=None): - ''' - Stub an object. If attr is not None, will attempt to stub that attribute - on the object. Only required for modules and other rare cases where we - can't determine the binding from the object. - ''' - if attr: - return _stub_attr(obj, attr) - else: - return _stub_obj(obj) - -def _stub_attr(obj, attr_name): - ''' - Stub an attribute of an object. Will return an existing stub if there already - is one. - ''' - # Annoying circular reference requires importing here. Would like to see - # this cleaned up. @AW - from .mock import Mock - - # Check to see if this a property, this check is only for when dealing with an - # instance. getattr will work for classes. - is_property = False - - if not inspect.isclass(obj) and not inspect.ismodule(obj): - # It's possible that the attribute is defined after initialization, and - # so is not on the class itself. - attr = getattr(obj.__class__, attr_name, None) - if isinstance(attr, property): - is_property = True - - if not is_property: - attr = getattr(obj, attr_name) - - # Return an existing stub - if isinstance(attr, Stub): - return attr - - # If a Mock object, stub its __call__ - if isinstance(attr, Mock): - return stub(attr.__call__) - - if isinstance(attr, property): - return StubProperty(obj, attr_name) - - # Sadly, builtin functions and methods have the same type, so we have to use - # the same stub class even though it's a bit ugly - if inspect.ismodule(obj) and \ - isinstance(attr, (types.FunctionType,types.BuiltinFunctionType,types.BuiltinMethodType)): - return StubFunction(obj, attr_name) - - # I thought that types.UnboundMethodType differentiated these cases but - # apparently not. - if isinstance(attr, types.MethodType): - # Handle differently if unbound because it's an implicit "any instance" - if getattr(attr, 'im_self', None)==None: - # Handle the python3 case and py2 filter - if hasattr(attr, '__self__'): - if attr.__self__!=None: - return StubMethod(obj, attr_name) - if sys.version_info.major==2: - return StubUnboundMethod(attr) - else: - return StubMethod(obj, attr_name) - if isinstance(attr, (types.BuiltinFunctionType,types.BuiltinMethodType)): - return StubFunction(obj, attr_name) - # What an absurd type this is .... - if type(attr).__name__ == 'method-wrapper': - return StubMethodWrapper(attr) - - # This is also slot_descriptor - if type(attr).__name__ == 'wrapper_descriptor': - return StubWrapperDescriptor(obj, attr_name) - - raise UnsupportedStub("can't stub %s(%s) of %s", attr_name, type(attr), obj) - - -def _stub_obj(obj): - ''' - Stub an object directly. - ''' - # Annoying circular reference requires importing here. Would like to see - # this cleaned up. @AW - from .mock import Mock - - # Return an existing stub - if isinstance(obj, Stub): - return obj - - # If a Mock object, stub its __call__ - if isinstance(obj, Mock): - return stub(obj.__call__) - - # If passed-in a type, assume that we're going to stub out the creation. - # See StubNew for the awesome sauce. - #if isinstance(obj, types.TypeType): - if hasattr(types,'TypeType') and isinstance(obj, types.TypeType): - return StubNew(obj) - elif hasattr(__builtins__,'type') and isinstance(obj, __builtins__['type']): - return StubNew(obj) - elif inspect.isclass(obj): - return StubNew(obj) - - # I thought that types.UnboundMethodType differentiated these cases but - # apparently not. - if isinstance(obj, types.MethodType): - # Handle differently if unbound because it's an implicit "any instance" - if getattr(obj, 'im_self', None)==None: - # Handle the python3 case and py2 filter - if hasattr(obj, '__self__'): - if obj.__self__!=None: - return StubMethod(obj) - if sys.version_info.major==2: - return StubUnboundMethod(obj) +def stub(obj, attr=None): + ''' + Stub an object. If attr is not None, will attempt to stub that attribute + on the object. Only required for modules and other rare cases where we + can't determine the binding from the object. + ''' + if attr: + return _stub_attr(obj, attr) else: - return StubMethod(obj) - - # These aren't in the types library - if type(obj).__name__ == 'method-wrapper': - return StubMethodWrapper(obj) - - if type(obj).__name__ == 'wrapper_descriptor': - raise UnsupportedStub("must call stub(obj,'%s') for slot wrapper on %s", - obj.__name__, obj.__objclass__.__name__ ) - - # (Mostly) Lastly, look for properties. - # First look for the situation where there's a reference back to the property. - prop = obj - if isinstance( getattr( obj, '__self__', None), property ): - obj = prop.__self__ - - # Once we've found a property, we have to figure out how to reference back to - # the owning class. This is a giant pain and we have to use gc to find out - # where it comes from. This code is dense but resolves to something like this: - # >>> gc.get_referrers( foo.x ) - # [{'__dict__': , - # 'x': , - # '__module__': '__main__', - # '__weakref__': , - # '__doc__': None}] - if isinstance(obj, property): - klass,attr = None,None - for ref in gc.get_referrers( obj ): - if klass and attr: break - if isinstance(ref,dict) and ref.get('prop',None) is obj : - klass = getattr( ref.get('__dict__',None), '__objclass__', None ) - for name,val in getattr(klass,'__dict__',{}).items(): - if val is obj: - attr = name - break - - if klass and attr: - rval = stub(klass,attr) - if prop != obj: - return stub(rval, prop.__name__) - return rval - - # If a function and it has an associated module, we can mock directly. - # Note that this *must* be after properties, otherwise it conflicts with - # stubbing out the deleter methods and such - # Sadly, builtin functions and methods have the same type, so we have to use - # the same stub class even though it's a bit ugly - if isinstance(obj, (types.FunctionType,types.BuiltinFunctionType,types.BuiltinMethodType)) and hasattr(obj, '__module__'): - return StubFunction(obj) - - raise UnsupportedStub("can't stub %s", obj) + return _stub_obj(obj) -class Stub(object): - ''' - Base class for all stubs. - ''' - def __init__(self, obj, attr=None): +def _stub_attr(obj, attr_name): ''' - Setup the structs for expectations + Stub an attribute of an object. Will return an existing stub if + there already is one. ''' - self._obj = obj - self._attr = attr - self._expectations = [] - self._torn = False + # Annoying circular reference requires importing here. Would like to see + # this cleaned up. @AW + from .mock import Mock - @property - def name(self): - return None # The base class implement this. + # Check to see if this a property, this check is only for when dealing + # with an instance. getattr will work for classes. + is_property = False - @property - def expectations(self): - return self._expectations + if not inspect.isclass(obj) and not inspect.ismodule(obj): + # It's possible that the attribute is defined after initialization, and + # so is not on the class itself. + attr = getattr(obj.__class__, attr_name, None) + if isinstance(attr, property): + is_property = True - def unmet_expectations(self): - ''' - Assert that all expectations on the stub have been met. - ''' - unmet = [] - for exp in self._expectations: - if not exp.closed(with_counts=True): - unmet.append(ExpectationNotSatisfied(exp)) - return unmet + if not is_property: + attr = getattr(obj, attr_name) - def teardown(self): - ''' - Clean up all expectations and restore the original attribute of the mocked - object. - ''' - if not self._torn: - self._expectations = [] - self._torn = True - self._teardown() + # Return an existing stub + if isinstance(attr, Stub): + return attr - def _teardown(self): - ''' - Hook for subclasses to teardown their stubs. Called only once. - ''' + # If a Mock object, stub its __call__ + if isinstance(attr, Mock): + return stub(attr.__call__) - def expect(self): - ''' - Add an expectation to this stub. Return the expectation - ''' - exp = Expectation(self) - self._expectations.append( exp ) - return exp - - def __call__(self, *args, **kwargs): - for exp in self._expectations: - # If expectation closed skip - if exp.closed(): - continue - - # If args don't match the expectation but its minimum counts have been - # met, close it and move on, else it's an unexpected call. Have to check - # counts here now due to the looser definitions of expectations in 0.3.x - # If we dont match, the counts aren't met and we're not allowing - # out-of-order, then break out and raise an exception. - if not exp.match(*args, **kwargs): - if exp.counts_met(): - exp.close(*args, **kwargs) - elif not exp.is_any_order(): - break - else: - return exp.test(*args, **kwargs) - - raise UnexpectedCall(call=self.name, suffix=self._format_exception(), args=args, kwargs=kwargs) - - def _format_exception(self): - result = [ - colored("All expectations", 'white', attrs=['bold']) - ] - for e in self._expectations: - result.append( str(e) ) - return "\n".join(result) + if isinstance(attr, property): + return StubProperty(obj, attr_name) + + # Sadly, builtin functions and methods have the same type, so we have to + # use the same stub class even though it's a bit ugly + if inspect.ismodule(obj) and isinstance(attr, (types.FunctionType, + types.BuiltinFunctionType, + types.BuiltinMethodType)): + return StubFunction(obj, attr_name) + + # I thought that types.UnboundMethodType differentiated these cases but + # apparently not. + if isinstance(attr, types.MethodType): + # Handle differently if unbound because it's an implicit "any instance" + if getattr(attr, 'im_self', None) is None: + # Handle the python3 case and py2 filter + if hasattr(attr, '__self__'): + if attr.__self__ is not None: + return StubMethod(obj, attr_name) + if sys.version_info.major == 2: + return StubUnboundMethod(attr) + else: + return StubMethod(obj, attr_name) + + if isinstance(attr, (types.BuiltinFunctionType, types.BuiltinMethodType)): + return StubFunction(obj, attr_name) + + # What an absurd type this is .... + if type(attr).__name__ == 'method-wrapper': + return StubMethodWrapper(attr) + + # This is also slot_descriptor + if type(attr).__name__ == 'wrapper_descriptor': + return StubWrapperDescriptor(obj, attr_name) + + raise UnsupportedStub( + "can't stub %s(%s) of %s", attr_name, type(attr), obj) -class StubProperty(Stub, property): - ''' - Property stubbing. - ''' - - def __init__(self, obj, attr): - super(StubProperty,self).__init__(obj, attr) - property.__init__(self, lambda x: self(), - lambda x, val: self.setter(val), lambda x: self.deleter() ) - # In order to stub out a property we have ask the class for the propery object - # that was created we python execute class code. - if inspect.isclass(obj): - self._instance = obj - else: - self._instance = obj.__class__ - # Use a simple Mock object for the deleter and setter. Use same namespace - # as property type so that it simply works. +def _stub_obj(obj): + ''' + Stub an object directly. + ''' # Annoying circular reference requires importing here. Would like to see # this cleaned up. @AW from .mock import Mock - self._obj = getattr(self._instance, attr) - self.setter = Mock() - self.deleter = Mock() - setattr(self._instance, self._attr, self) + # Return an existing stub + if isinstance(obj, Stub): + return obj + + # If a Mock object, stub its __call__ + if isinstance(obj, Mock): + return stub(obj.__call__) + + # If passed-in a type, assume that we're going to stub out the creation. + # See StubNew for the awesome sauce. + # if isinstance(obj, types.TypeType): + if hasattr(types, 'TypeType') and isinstance(obj, types.TypeType): + return StubNew(obj) + elif hasattr(__builtins__, 'type') and \ + isinstance(obj, __builtins__['type']): + return StubNew(obj) + elif inspect.isclass(obj): + return StubNew(obj) + + # I thought that types.UnboundMethodType differentiated these cases but + # apparently not. + if isinstance(obj, types.MethodType): + # Handle differently if unbound because it's an implicit "any instance" + if getattr(obj, 'im_self', None) is None: + # Handle the python3 case and py2 filter + if hasattr(obj, '__self__'): + if obj.__self__ is not None: + return StubMethod(obj) + if sys.version_info.major == 2: + return StubUnboundMethod(obj) + else: + return StubMethod(obj) + + # These aren't in the types library + if type(obj).__name__ == 'method-wrapper': + return StubMethodWrapper(obj) + + if type(obj).__name__ == 'wrapper_descriptor': + raise UnsupportedStub( + "must call stub(obj,'%s') for slot wrapper on %s", + obj.__name__, obj.__objclass__.__name__) + + # (Mostly) Lastly, look for properties. + # First look for the situation where there's a reference back to the + # property. + prop = obj + if isinstance(getattr(obj, '__self__', None), property): + obj = prop.__self__ + + # Once we've found a property, we have to figure out how to reference + # back to the owning class. This is a giant pain and we have to use gc + # to find out where it comes from. This code is dense but resolves to + # something like this: + # >>> gc.get_referrers( foo.x ) + # [{'__dict__': , + # 'x': , + # '__module__': '__main__', + # '__weakref__': , + # '__doc__': None}] + if isinstance(obj, property): + klass, attr = None, None + for ref in gc.get_referrers(obj): + if klass and attr: + break + if isinstance(ref, dict) and ref.get('prop', None) is obj: + klass = getattr( + ref.get('__dict__', None), '__objclass__', None) + for name, val in getattr(klass, '__dict__', {}).items(): + if val is obj: + attr = name + break + + if klass and attr: + rval = stub(klass, attr) + if prop != obj: + return stub(rval, prop.__name__) + return rval + + # If a function and it has an associated module, we can mock directly. + # Note that this *must* be after properties, otherwise it conflicts with + # stubbing out the deleter methods and such + # Sadly, builtin functions and methods have the same type, so we have to + # use the same stub class even though it's a bit ugly + if isinstance(obj, (types.FunctionType, types.BuiltinFunctionType, + types.BuiltinMethodType)) and hasattr(obj, '__module__'): + return StubFunction(obj) + + raise UnsupportedStub("can't stub %s", obj) - @property - def name(self): - return "%s.%s" % (self._instance.__name__, self._attr) +class Stub(object): - def _teardown(self): ''' - Replace the original method. - ''' - setattr( self._instance, self._attr, self._obj ) + Base class for all stubs. + ''' + + def __init__(self, obj, attr=None): + ''' + Setup the structs for expectations + ''' + self._obj = obj + self._attr = attr + self._expectations = [] + self._torn = False + + @property + def name(self): + return None # The base class implement this. + + @property + def expectations(self): + return self._expectations + + def unmet_expectations(self): + ''' + Assert that all expectations on the stub have been met. + ''' + unmet = [] + for exp in self._expectations: + if not exp.closed(with_counts=True): + unmet.append(ExpectationNotSatisfied(exp)) + return unmet + + def teardown(self): + ''' + Clean up all expectations and restore the original attribute of the + mocked object. + ''' + if not self._torn: + self._expectations = [] + self._torn = True + self._teardown() + + def _teardown(self): + ''' + Hook for subclasses to teardown their stubs. Called only once. + ''' + + def expect(self): + ''' + Add an expectation to this stub. Return the expectation + ''' + exp = Expectation(self) + self._expectations.append(exp) + return exp + + def __call__(self, *args, **kwargs): + for exp in self._expectations: + # If expectation closed skip + if exp.closed(): + continue + + # If args don't match the expectation but its minimum counts have + # been met, close it and move on, else it's an unexpected call. + # Have to check counts here now due to the looser definitions of + # expectations in 0.3.x If we dont match, the counts aren't met + # and we're not allowing out-of-order, then break out and raise + # an exception. + if not exp.match(*args, **kwargs): + if exp.counts_met(): + exp.close(*args, **kwargs) + elif not exp.is_any_order(): + break + else: + return exp.test(*args, **kwargs) + + raise UnexpectedCall( + call=self.name, suffix=self._format_exception(), + args=args, kwargs=kwargs) + + def _format_exception(self): + result = [ + colored("All expectations", 'white', attrs=['bold']) + ] + for e in self._expectations: + result.append(str(e)) + return "\n".join(result) -class StubMethod(Stub): - ''' - Stub a method. - ''' - def __init__(self, obj, attr=None): +class StubProperty(Stub, property): + ''' - Initialize with an object of type MethodType + Property stubbing. ''' - super(StubMethod,self).__init__(obj, attr) - if not self._attr: - # python3 - if sys.version_info.major==3: #hasattr(obj,'__func__'): - self._attr = obj.__func__.__name__ - else: - self._attr = obj.im_func.func_name - if sys.version_info.major==3: #hasattr(obj, '__self__'): - self._instance = obj.__self__ - else: - self._instance = obj.im_self - else: - self._instance = self._obj - self._obj = getattr( self._instance, self._attr ) - setattr( self._instance, self._attr, self ) + def __init__(self, obj, attr): + super(StubProperty, self).__init__(obj, attr) + property.__init__(self, lambda x: self(), + lambda x, val: self.setter(val), + lambda x: self.deleter()) + # In order to stub out a property we have ask the class for the + # propery object that was created we python execute class code. + if inspect.isclass(obj): + self._instance = obj + else: + self._instance = obj.__class__ - @property - def name(self): - from .mock import Mock # Import here for the same reason as above. - if hasattr(self._obj, 'im_class'): - if issubclass(self._obj.im_class, Mock): - return self._obj.im_self._name + # Use a simple Mock object for the deleter and setter. Use same + # namespace as property type so that it simply works. + # Annoying circular reference requires importing here. Would like to + # see this cleaned up. @AW + from .mock import Mock + self._obj = getattr(self._instance, attr) + self.setter = Mock() + self.deleter = Mock() - # Always use the class to get the name - klass = self._instance - if not inspect.isclass(self._instance): - klass = self._instance.__class__ + setattr(self._instance, self._attr, self) - return "%s.%s" % (klass.__name__, self._attr) + @property + def name(self): + return "%s.%s" % (self._instance.__name__, self._attr) - def _teardown(self): - ''' - Put the original method back in place. This will also handle the special case - when it putting back a class method. - - The following code snippet best describe why it fails using settar, the - class method would be replaced with a bound method not a class method. - - >>> class Example(object): - ... @classmethod - ... def a_classmethod(self): - ... pass - ... - >>> Example.__dict__['a_classmethod'] # Note the classmethod is returned. - - >>> orig = getattr(Example, 'a_classmethod') - >>> orig - > - >>> setattr(Example, 'a_classmethod', orig) - >>> Example.__dict__['a_classmethod'] # Note that setattr set a bound method not a class method. - > - - The only way to figure out if this is a class method is to check and see if - the bound method im_self is a class, if so then we need to wrap the function - object (im_func) with class method before setting it back on the class. + def _teardown(self): + ''' + Replace the original method. + ''' + setattr(self._instance, self._attr, self._obj) - ''' - # Figure out if this is a class method and we're unstubbing it on the class - # to which it belongs. This addresses an edge case where a module can - # expose a method of an instance. gevent does this, for example. - if hasattr(self._obj,'__self__') and inspect.isclass(self._obj.__self__) and self._obj.__self__ is self._instance: - setattr(self._instance, self._attr, classmethod(self._obj.__func__)) - elif hasattr(self._obj,'im_self') and inspect.isclass(self._obj.im_self) and self._obj.im_self is self._instance: - # Wrap it and set it back on the class - setattr(self._instance, self._attr, classmethod(self._obj.im_func)) - else: - setattr( self._instance, self._attr, self._obj ) -class StubFunction(Stub): - ''' - Stub a function. - ''' +class StubMethod(Stub): - def __init__(self, obj, attr=None): ''' - Initialize with an object that is an unbound method - ''' - super(StubFunction, self).__init__(obj, attr) - if not self._attr: - if getattr(obj, '__module__', None): - self._instance = sys.modules[obj.__module__] - elif getattr(obj, '__self__', None): - self._instance = obj.__self__ - else: + Stub a method. + ''' + + def __init__(self, obj, attr=None): + ''' + Initialize with an object of type MethodType + ''' + super(StubMethod, self).__init__(obj, attr) + if not self._attr: + # python3 + if sys.version_info.major == 3: # hasattr(obj,'__func__'): + self._attr = obj.__func__.__name__ + else: + self._attr = obj.im_func.func_name + + if sys.version_info.major == 3: # hasattr(obj, '__self__'): + self._instance = obj.__self__ + else: + self._instance = obj.im_self + else: + self._instance = self._obj + self._obj = getattr(self._instance, self._attr) + setattr(self._instance, self._attr, self) + + @property + def name(self): + from .mock import Mock # Import here for the same reason as above. + if hasattr(self._obj, 'im_class'): + if issubclass(self._obj.im_class, Mock): + return self._obj.im_self._name + + # Always use the class to get the name + klass = self._instance + if not inspect.isclass(self._instance): + klass = self._instance.__class__ + + return "%s.%s" % (klass.__name__, self._attr) + + def _teardown(self): + ''' + Put the original method back in place. This will also handle the + special case when it putting back a class method. + + The following code snippet best describe why it fails using settar, + the class method would be replaced with a bound method not a class + method. + + >>> class Example(object): + ... @classmethod + ... def a_classmethod(self): + ... pass + ... + >>> Example.__dict__['a_classmethod'] + + >>> orig = getattr(Example, 'a_classmethod') + >>> orig + > + >>> setattr(Example, 'a_classmethod', orig) + >>> Example.__dict__['a_classmethod'] + > + + The only way to figure out if this is a class method is to check and + see if the bound method im_self is a class, if so then we need to wrap + the function object (im_func) with class method before setting it back + on the class. + ''' + # Figure out if this is a class method and we're unstubbing it on the + # class to which it belongs. This addresses an edge case where a + # module can expose a method of an instance. e.g gevent. + if hasattr(self._obj, '__self__') and \ + inspect.isclass(self._obj.__self__) and \ + self._obj.__self__ is self._instance: + setattr( + self._instance, self._attr, classmethod(self._obj.__func__)) + elif hasattr(self._obj, 'im_self') and \ + inspect.isclass(self._obj.im_self) and \ + self._obj.im_self is self._instance: + # Wrap it and set it back on the class + setattr(self._instance, self._attr, classmethod(self._obj.im_func)) + else: + setattr(self._instance, self._attr, self._obj) - raise UnsupportedStub("Failed to find instance of %s"%(obj)) - if getattr(obj,'func_name', None): - self._attr = obj.func_name - elif getattr(obj,'__name__', None): - self._attr = obj.__name__ - else: - raise UnsupportedStub("Failed to find name of %s"%(obj)) - else: - self._instance = self._obj - self._obj = getattr(self._instance, self._attr) - - # This handles the case where we're stubbing a special method that's - # inherited from object, and so instead of calling setattr on teardown, - # we want to call delattr. This is particularly important for not seeing - # those stupid DeprecationWarnings after StubNew - self._was_object_method = False - if hasattr(self._instance, '__dict__'): - self._was_object_method = \ - self._attr not in self._instance.__dict__.keys() and\ - self._attr in object.__dict__.keys() - setattr( self._instance, self._attr, self ) - - @property - def name(self): - return "%s.%s" % (self._instance.__name__, self._attr) - - def _teardown(self): - ''' - Replace the original method. - ''' - if not self._was_object_method: - setattr( self._instance, self._attr, self._obj ) - else: - delattr( self._instance, self._attr ) +class StubFunction(Stub): -class StubNew(StubFunction): - ''' - Stub out the constructor, but hide the fact that we're stubbing "__new__" - and act more like we're stubbing "__init__". Needs to use the logic in - the StubFunction ctor. - ''' - _cache = {} - - def __new__(self, klass, *args): - ''' - Because we're not saving the stub into any attribute, then we have - to do some faking here to return the same handle. ''' - rval = self._cache.get(klass) - if not rval: - rval = self._cache[klass] = super(StubNew,self).__new__(self, *args) - rval._allow_init = True - else: - rval._allow_init = False - return rval + Stub a function. + ''' + + def __init__(self, obj, attr=None): + ''' + Initialize with an object that is an unbound method + ''' + super(StubFunction, self).__init__(obj, attr) + if not self._attr: + if getattr(obj, '__module__', None): + self._instance = sys.modules[obj.__module__] + elif getattr(obj, '__self__', None): + self._instance = obj.__self__ + else: + + raise UnsupportedStub("Failed to find instance of %s" % (obj)) + + if getattr(obj, 'func_name', None): + self._attr = obj.func_name + elif getattr(obj, '__name__', None): + self._attr = obj.__name__ + else: + raise UnsupportedStub("Failed to find name of %s" % (obj)) + else: + self._instance = self._obj + self._obj = getattr(self._instance, self._attr) + + # This handles the case where we're stubbing a special method that's + # inherited from object, and so instead of calling setattr on teardown, + # we want to call delattr. This is particularly important for not + # seeing those stupid DeprecationWarnings after StubNew + self._was_object_method = False + if hasattr(self._instance, '__dict__'): + self._was_object_method = \ + self._attr not in self._instance.__dict__.keys() and\ + self._attr in object.__dict__.keys() + setattr(self._instance, self._attr, self) + + @property + def name(self): + return "%s.%s" % (self._instance.__name__, self._attr) + + def _teardown(self): + ''' + Replace the original method. + ''' + if not self._was_object_method: + setattr(self._instance, self._attr, self._obj) + else: + delattr(self._instance, self._attr) - def __init__(self, obj): - ''' - Overload the initialization so that we can hack access to __new__. - ''' - if self._allow_init: - self._new = obj.__new__ - super(StubNew,self).__init__(obj, '__new__') - self._type = obj - def __call__(self, *args, **kwargs): - ''' - When calling the new function, strip out the first arg which is - the type. In this way, the mocker writes their expectation as if it - was an __init__. - ''' - return super(StubNew,self).__call__( *(args[1:]), **kwargs ) +class StubNew(StubFunction): - def _teardown(self): - ''' - Overload so that we can clear out the cache after a test run. ''' - # __new__ is a super-special case in that even when stubbing a class - # which implements its own __new__ and subclasses object, the - # "Class.__new__" reference is a staticmethod and not a method (or - # function). That confuses the "was_object_method" logic in StubFunction - # which then fails to delattr and from then on the class is corrupted. - # So skip that teardown and use a __new__-specific case. - setattr( self._instance, self._attr, staticmethod(self._new) ) - StubNew._cache.pop(self._type) + Stub out the constructor, but hide the fact that we're stubbing "__new__" + and act more like we're stubbing "__init__". Needs to use the logic in + the StubFunction ctor. + ''' + _cache = {} + + def __new__(self, klass, *args): + ''' + Because we're not saving the stub into any attribute, then we have + to do some faking here to return the same handle. + ''' + rval = self._cache.get(klass) + if not rval: + rval = self._cache[klass] = super( + StubNew, self).__new__(self, *args) + rval._allow_init = True + else: + rval._allow_init = False + return rval + + def __init__(self, obj): + ''' + Overload the initialization so that we can hack access to __new__. + ''' + if self._allow_init: + self._new = obj.__new__ + super(StubNew, self).__init__(obj, '__new__') + self._type = obj + + def __call__(self, *args, **kwargs): + ''' + When calling the new function, strip out the first arg which is + the type. In this way, the mocker writes their expectation as if it + was an __init__. + ''' + return super(StubNew, self).__call__(*(args[1:]), **kwargs) + + def _teardown(self): + ''' + Overload so that we can clear out the cache after a test run. + ''' + # __new__ is a super-special case in that even when stubbing a class + # which implements its own __new__ and subclasses object, the + # "Class.__new__" reference is a staticmethod and not a method (or + # function). That confuses the "was_object_method" logic in + # StubFunction which then fails to delattr and from then on the class + # is corrupted. So skip that teardown and use a __new__-specific case. + setattr(self._instance, self._attr, staticmethod(self._new)) + StubNew._cache.pop(self._type) + class StubUnboundMethod(Stub): - ''' - Stub an unbound method. - ''' - def __init__(self, obj): - ''' - Initialize with an object that is an unbound method - ''' - # NOTE: It doesn't appear that there's any way to support this in python3 - # because an unbound method has no reference to its parent class, it looks - # just like a regular function - super(StubUnboundMethod,self).__init__(obj) - self._instance = obj.im_class - self._attr = obj.im_func.func_name - setattr( self._instance, self._attr, self ) - - @property - def name(self): - return "%s.%s" % (self._instance.__name__, self._attr) - - def _teardown(self): ''' - Replace the original method. + Stub an unbound method. ''' - setattr( self._instance, self._attr, self._obj ) + + def __init__(self, obj): + ''' + Initialize with an object that is an unbound method + ''' + # NOTE: It doesn't appear that there's any way to support this in + # python3 because an unbound method has no reference to its parent + # class, it looks just like a regular function + super(StubUnboundMethod, self).__init__(obj) + self._instance = obj.im_class + self._attr = obj.im_func.func_name + setattr(self._instance, self._attr, self) + + @property + def name(self): + return "%s.%s" % (self._instance.__name__, self._attr) + + def _teardown(self): + ''' + Replace the original method. + ''' + setattr(self._instance, self._attr, self._obj) + class StubMethodWrapper(Stub): - ''' - Stub a method-wrapper. - ''' - def __init__(self, obj): ''' - Initialize with an object that is a method wrapper. + Stub a method-wrapper. ''' - super(StubMethodWrapper,self).__init__(obj) - self._instance = obj.__self__ - self._attr = obj.__name__ - setattr( self._instance, self._attr, self ) - @property - def name(self): - return "%s.%s" % (self._instance.__class__.__name__, self._attr) + def __init__(self, obj): + ''' + Initialize with an object that is a method wrapper. + ''' + super(StubMethodWrapper, self).__init__(obj) + self._instance = obj.__self__ + self._attr = obj.__name__ + setattr(self._instance, self._attr, self) + + @property + def name(self): + return "%s.%s" % (self._instance.__class__.__name__, self._attr) + + def _teardown(self): + ''' + Replace the original method. + ''' + setattr(self._instance, self._attr, self._obj) - def _teardown(self): - ''' - Replace the original method. - ''' - setattr( self._instance, self._attr, self._obj ) class StubWrapperDescriptor(Stub): - ''' - Stub a wrapper-descriptor. Only works when we can fetch it by name. Because - the w-d object doesn't contain both the instance ref and the attribute name - to be able to look it up. Used for mocking object.__init__ and related - builtin methods when subclasses that don't overload those. - ''' - - def __init__(self, obj, attr_name): + ''' - Initialize with an object that is a method wrapper. + Stub a wrapper-descriptor. Only works when we can fetch it by name. Because + the w-d object doesn't contain both the instance ref and the attribute name + to be able to look it up. Used for mocking object.__init__ and related + builtin methods when subclasses that don't overload those. ''' - super(StubWrapperDescriptor,self).__init__(obj, attr_name) - self._orig = getattr( self._obj, self._attr ) - setattr( self._obj, self._attr, self ) - @property - def name(self): - return "%s.%s" % (self._obj.__name__, self._attr) + def __init__(self, obj, attr_name): + ''' + Initialize with an object that is a method wrapper. + ''' + super(StubWrapperDescriptor, self).__init__(obj, attr_name) + self._orig = getattr(self._obj, self._attr) + setattr(self._obj, self._attr, self) - def _teardown(self): - ''' - Replace the original method. - ''' - setattr( self._obj, self._attr, self._orig ) + @property + def name(self): + return "%s.%s" % (self._obj.__name__, self._attr) + def _teardown(self): + ''' + Replace the original method. + ''' + setattr(self._obj, self._attr, self._orig)