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 SomeClass
: from 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']