我正在使用其他人编写的模块。我想修补模块中定义的类的__init__
方法。我发现如何做到这一点的例子都假设我自己打电话给班级(例如Monkey-patch Python class)。然而,这种情况并非如此。在我的情况下,该类在另一个模块中的函数内被初始化。请参阅下面的(大大简化)示例:
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()
有没有办法修改__init__
的{{1}}方法,以便从mymodule.py调用SomeClass
时,例如打印43而不是42?理想情况下,我可以包装现有方法。
我无法更改thirdpartymodule * .py文件,因为其他脚本依赖于现有功能。我宁愿不必创建我自己的模块副本,因为我需要做的改变非常简单。
修改2013-10-24
我忽略了上面例子中的一个小而重要的细节。 dosomething
由SomeClass
导入thirdpartymodule_b
,如下所示:from thirdpartymodule_a import SomeClass
。
要执行F.J建议的修补程序,我需要替换thirdpartymodule_b
中的副本,而不是thirdpartymodule_a
。例如thirdpartymodule_b.SomeClass.__init__ = new_init
。
答案 0 :(得分:63)
以下内容应该有效:
import thirdpartymodule_a
import thirdpartymodule_b
def new_init(self):
self.a = 43
thirdpartymodule_a.SomeClass.__init__ = new_init
thirdpartymodule_b.dosomething()
如果您希望新的init调用旧的init,请使用以下内容替换new_init()
定义:
old_init = thirdpartymodule_a.SomeClass.__init__
def new_init(self, *k, **kw):
old_init(self, *k, **kw)
self.a = 43
答案 1 :(得分:37)
使用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()
答案 2 :(得分:2)
另一种可能与Andrew Clark's one非常相似的方法是使用wrapt库。
除了其他有用的东西之外,这个库还提供了wrap_function_wrapper
和patch_function_wrapper
个助手。它们可以像这样使用:
import wrapt
import thirdpartymodule_a
import thirdpartymodule_b
@wrapt.patch_function_wrapper(thirdpartymodule_a.SomeClass, '__init__')
def new_init(wrapped, instance, args, kwargs):
# here, wrapped is the original __init__,
# instance is `self` instance (it is not true for classmethods though),
# args and kwargs are tuple and dict respectively.
# first call original init
wrapped(*args, **kwargs) # note it is already bound to the instance
# and now do our changes
instance.a = 43
thirdpartymodule_b.do_something()
或者有时你可能想要使用wrap_function_wrapper
而不是装饰者,但是othrewise的工作方式相同:
def new_init(wrapped, instance, args, kwargs):
pass # ...
wrapt.wrap_function_wrapper(thirdpartymodule_a.SomeClass, '__init__', new_init)
答案 3 :(得分:1)
很脏,但是有效:
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
答案 4 :(得分:1)
一个只有轻微的hacky版本使用全局变量作为参数:
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
当sentinel为false时,它的行为与以前完全相同。如果这是真的,那么你就会得到新的行为。在您的代码中,您可以这样做:
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
现有代码将继续工作 - 他们将在没有参数的情况下调用它,这将保留默认的false值,这将保持旧的行为。但是你的代码现在有办法告诉整个堆栈,新的行为是可用的。
答案 5 :(得分:1)
以下是我使用Popen
向monkeypatch 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
收集模拟popens的MockPopen
工厂:
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
一个例子测试:
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
它会覆盖Popen
中内置的helpers
类导入:
@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']