Monkey在Python的另一个模块中修补一个类

我正在使用其他人编写的模块。 我想猴子修补模块中定义的类的__init__方法。 我已经find的例子展示了如何做到这一点,都假设我自己会调用这个类(例如Monkey-patch Python类 )。 然而,这种情况并非如此。 在我的情况下,这个类在另一个模块的函数中初始化。 看下面的(大大简化的)例子:

thirdpartymodule_a.py

 class SomeClass(object): def __init__(self): self.a = 42 def show(self): print self.a 

thirdpartymodule_b.py

 import thirdpartymodule_a def dosomething(): sc = thirdpartymodule_a.SomeClass() sc.show() 

mymodule.py

 import thirdpartymodule_b thirdpartymodule.dosomething() 

有没有办法修改SomeClass__init__方法,以便当从mymodule.py中调用dosomething时,例如打印43而不是42? 理想情况下,我可以包装现有的方法。

我无法更改第三方模块* .py文件,因为其他脚本依赖于现有的function。 我宁愿不必创build自己的模块副本,因为我需要做的更改非常简单。

编辑2013-10-24

我忽略了上面例子中的一个小而重要的细节。 SomeClass是像这样from thirdpartymodule_a import SomeClassfrom thirdpartymodule_a import SomeClass

要做FJbuild议的补丁,我需要replacethirdpartymodule_b的副本,而不是thirdpartymodule_a 。 例如thirdpartymodule_b.SomeClass.__init__ = new_init

以下应该工作:

 import thirdpartymodule_a import thirdpartymodule_b def new_init(self): self.a = 43 thirdpartymodule_a.SomeClass.__init__ = new_init thirdpartymodule_b.dosomething() 

如果您希望新的init调用旧的init,请使用以下命令replacenew_init()定义:

 old_init = thirdpartymodule_a.SomeClass.__init__ def new_init(self, *k, **kw): old_init(self, *k, **kw) self.a = 43 

使用mock库。

 import thirdpartymodule_a import thirdpartymodule_b import mock def new_init(self): self.a = 43 with mock.patch.object(thirdpartymodule_a.SomeClass, '__init__', new_init): thirdpartymodule_b.dosomething() # -> print 43 thirdpartymodule_b.dosomething() # -> print 42 

要么

 import thirdpartymodule_b import mock def new_init(self): self.a = 43 with mock.patch('thirdpartymodule_a.SomeClass.__init__', new_init): thirdpartymodule_b.dosomething() thirdpartymodule_b.dosomething() 

脏,但它的作品:

 class SomeClass2(object): def __init__(self): self.a = 43 def show(self): print self.a import thirdpartymodule_b # Monkey patch the class thirdpartymodule_b.thirdpartymodule_a.SomeClass = SomeClass2 thirdpartymodule_b.dosomething() # output 43 

一个只有稍微不好的版本使用全局variables作为参数:

 sentinel = False class SomeClass(object): def __init__(self): global sentinel if sentinel: <do my custom code> else: # Original code self.a = 42 def show(self): print self.a 

当哨兵是假的时候,它的行为和以前一样。 当这是真的,那么你得到你的新行为。 在你的代码中,你会这样做:

 import thirdpartymodule_b thirdpartymodule_b.sentinel = True thirdpartymodule.dosomething() thirdpartymodule_b.sentinel = False 

当然,在不影响现有代码的情况下进行适当的修复是相当简单的。 但是你必须稍微改变其他模块:

 import thirdpartymodule_a def dosomething(sentinel = False): sc = thirdpartymodule_a.SomeClass(sentinel) sc.show() 

并传递给init:

 class SomeClass(object): def __init__(self, sentinel=False): if sentinel: <do my custom code> else: # Original code self.a = 42 def show(self): print self.a 

现有的代码将继续工作 – 他们将调用它没有参数,这将保持默认的假值,这将保持旧的行为。 但是你的代码现在有一种方法可以告诉整个堆栈,新的行为是可用的。

这里是一个例子,我想出了用pytest pytest

导入模块:

 # must be at module level in order to affect the test function context from some_module import helpers 

一个MockBytes对象:

 class MockBytes(object): all_read = [] all_write = [] all_close = [] def read(self, *args, **kwargs): # print('read', args, kwargs, dir(self)) self.all_read.append((self, args, kwargs)) def write(self, *args, **kwargs): # print('wrote', args, kwargs) self.all_write.append((self, args, kwargs)) def close(self, *args, **kwargs): # print('closed', self, args, kwargs) self.all_close.append((self, args, kwargs)) def get_all_mock_bytes(self): return self.all_read, self.all_write, self.all_close 

一个MockPopen工厂收集模拟popens:

 def mock_popen_factory(): all_popens = [] class MockPopen(object): def __init__(self, args, stdout=None, stdin=None, stderr=None): all_popens.append(self) self.args = args self.byte_collection = MockBytes() self.stdin = self.byte_collection self.stdout = self.byte_collection self.stderr = self.byte_collection pass return MockPopen, all_popens 

还有一个例子testing:

 def test_copy_file_to_docker(): MockPopen, all_opens = mock_popen_factory() helpers.Popen = MockPopen # replace builtin Popen with the MockPopen result = copy_file_to_docker('asdf', 'asdf') collected_popen = all_popens.pop() mock_read, mock_write, mock_close = collected_popen.byte_collection.get_all_mock_bytes() assert mock_read assert result.args == ['docker', 'cp', 'asdf', 'some_container:asdf'] 

这是相同的例子,但是使用pytest.fixture它覆盖了helpers内置的pytest.fixture类的导入:

 @pytest.fixture def all_popens(monkeypatch): # monkeypatch is magically injected all_popens = [] class MockPopen(object): def __init__(self, args, stdout=None, stdin=None, stderr=None): all_popens.append(self) self.args = args self.byte_collection = MockBytes() self.stdin = self.byte_collection self.stdout = self.byte_collection self.stderr = self.byte_collection pass monkeypatch.setattr(helpers, 'Popen', MockPopen) return all_popens def test_copy_file_to_docker(all_popens): result = copy_file_to_docker('asdf', 'asdf') collected_popen = all_popens.pop() mock_read, mock_write, mock_close = collected_popen.byte_collection.get_all_mock_bytes() assert mock_read assert result.args == ['docker', 'cp', 'asdf', 'fastload_cont:asdf']