Skip to content

Commit

Permalink
Initial work fixing constructor mocking for python3 #33
Browse files Browse the repository at this point in the history
  • Loading branch information
awestendorf committed Jan 3, 2018
1 parent 8148d7b commit 6094f29
Show file tree
Hide file tree
Showing 2 changed files with 42 additions and 21 deletions.
40 changes: 23 additions & 17 deletions chai/stub.py
Original file line number Diff line number Diff line change
Expand Up @@ -525,7 +525,7 @@ def _teardown(self):
delattr(self._instance, self._attr)


class StubNew(StubFunction):
class StubNew(Stub):

'''
Stub out the constructor, but hide the fact that we're stubbing "__new__"
Expand All @@ -539,24 +539,29 @@ 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
cache = self._cache.get(klass)
if not cache:
rval = super(StubNew, self).__new__(self, *args)
rval._expectations = []
rval._torn = False
rval._type = klass

self._cache[klass] = {
'stub': rval,
'init': getattr(klass, '__init__'),
'class': klass,
}

# Change __class__ to pretend it's a different type, and link
# the expectations references
def stub_init(stub_self, *args, **kwargs):
stub_self.__class__ = StubNew
stub_self._expectations = rval._expectations
setattr(klass, '__init__', stub_init)
else:
rval._allow_init = False
rval = cache['stub']
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
Expand All @@ -583,7 +588,8 @@ def _teardown(self):
# 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))

setattr(self._type, '__init__', self._cache[self._type]['init'])
StubNew._cache.pop(self._type)


Expand Down
23 changes: 19 additions & 4 deletions tests/stub_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ def bar(self): pass
self.assertEquals(res, f.bar.__call__)
self.assertEquals(res, stub(f.bar))

@unittest.skipIf(sys.version_info.major==3, "can't stub unbound methods by reference in python 3")
def test_stub_type_with_obj_ref(self):
class Foo(object):
def bar(self): pass
Expand Down Expand Up @@ -421,16 +422,29 @@ def test_teardown_on_object_method(self):

class StubNewTest(unittest.TestCase):

@unittest.skipIf(sys.version_info.major==3, "can't stub unbound methods in python 3")
def test_new(self):
class Foo(object): pass

f1 = Foo()
self.assertEquals(0, len(StubNew._cache))
x = StubNew(Foo)
self.assertTrue(x is StubNew(Foo))
stub = StubNew(Foo)
stub.expect()

# Due to the changes to support Py3, assert that we're no longer sharing
# the StubNew object itself, but we are sharing the expectations object
self.assertFalse(stub is Foo())
self.assertTrue(stub.expectations is Foo().expectations)

self.assertEquals(1, len(StubNew._cache))
StubNew._cache.clear()
stub.teardown()
self.assertEquals(0, len(StubNew._cache))

f2 = Foo()
self.assertTrue(isinstance(f2, Foo))
self.assertFalse(isinstance(f2, StubNew))
self.assertFalse(f1 is f2)

@unittest.skipIf(sys.version_info.major==3, "can't stub unbound methods in python 3")
def test_init(self):
class Foo(object): pass

Expand All @@ -440,6 +454,7 @@ class Foo(object): pass
self.assertEquals(s, Foo.__new__)
s.teardown()

@unittest.skipIf(sys.version_info.major==3, "can't stub unbound methods in python 3")
def test_call(self):
class Foo(object): pass
class Expect(object):
Expand Down

0 comments on commit 6094f29

Please sign in to comment.