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

用Python在另一个模块中修补类的Monkey,python,class,unit-testing,monkeypatching,Python,Class,Unit Testing,Monkeypatching,我正在使用其他人编写的模块。我想对模块中定义的类的\uuuu init\uuu方法进行修补。我发现的演示如何实现这一点的示例都假设我自己会调用这个类(例如)。然而,情况并非如此。在我的例子中,该类是在另一个模块的函数中初始化的。请参见下面的(大大简化的)示例: 第三方模块a.py class SomeClass(object): def __init__(self): self.a = 42 def show(self): print self.a

我正在使用其他人编写的模块。我想对模块中定义的类的
\uuuu init\uuu
方法进行修补。我发现的演示如何实现这一点的示例都假设我自己会调用这个类(例如)。然而,情况并非如此。在我的例子中,该类是在另一个模块的函数中初始化的。请参见下面的(大大简化的)示例:

第三方模块a.py

class SomeClass(object):
    def __init__(self):
        self.a = 42
    def show(self):
        print self.a
import thirdpartymodule_a
def dosomething():
    sc = thirdpartymodule_a.SomeClass()
    sc.show()
import thirdpartymodule_b
thirdpartymodule_b.dosomething()
thirdpartymodule_b.py

class SomeClass(object):
    def __init__(self):
        self.a = 42
    def show(self):
        print self.a
import thirdpartymodule_a
def dosomething():
    sc = thirdpartymodule_a.SomeClass()
    sc.show()
import thirdpartymodule_b
thirdpartymodule_b.dosomething()
mymodule.py

class SomeClass(object):
    def __init__(self):
        self.a = 42
    def show(self):
        print self.a
import thirdpartymodule_a
def dosomething():
    sc = thirdpartymodule_a.SomeClass()
    sc.show()
import thirdpartymodule_b
thirdpartymodule_b.dosomething()
有没有办法修改
SomeClass
\uuuu init\uuuuu
方法,例如,当从mymodule.py调用
dosomething
时,它会打印43而不是42?理想情况下,我能够包装现有的方法

我无法更改thirdpartymodule*.py文件,因为其他脚本依赖于现有功能。我不想创建自己的模块副本,因为我需要做的更改非常简单

编辑2013-10-24

在上面的例子中,我忽略了一个小而重要的细节
SomeClass
thirdpartymodule\u b
导入,如下所示:
从thirdpartymodule\u导入SomeClass

要执行F.J建议的修补程序,我需要替换
thirdpartymodule\u b
中的副本,而不是
thirdpartymodule\u a
。e、 g.
thirdpartymodule\b.SomeClass.\uuuu init\uuuu=new\u 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,请将
new\u init()
定义替换为以下内容:

old_init = thirdpartymodule_a.SomeClass.__init__
def new_init(self, *k, **kw):
    old_init(self, *k, **kw)
    self.a = 43
肮脏,但它可以工作:

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

只有一个稍微不太老练的版本使用全局变量作为参数:

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_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
class-SomeClass(对象):
定义初始化(self,sentinel=False):
如果是哨兵:
其他:
#原始代码
self.a=42
def显示(自我):
打印自我

现有代码将继续工作-他们将不带参数地调用它,这将保留默认的false值,这将保留旧的行为。但是您的代码现在有了一种方法,可以告诉整个堆栈新的行为是可用的

下面是我使用
pytest
为monkeypatch
Popen
提供的一个示例

导入模块:

# 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
收集模拟popen的
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
类导入:

@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']

另一种可能的方法,非常类似于,是使用库。 在其他有用的东西中,该库提供了
wrap\u函数包装器
patch\u函数包装器
帮助程序。它们可以这样使用:

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\u function\u wrapper
,它不是装饰器,但othrewise的工作方式相同:

def new_init(wrapped, instance, args, kwargs):
    pass  # ...

wrapt.wrap_function_wrapper(thirdpartymodule_a.SomeClass, '__init__', new_init)

我不明白为什么从何处调用该类会有区别。文件名应该是
thirdpartymodule\u a.py
thirdpartymodule\u b.py
。这是唯一正确工作的方法。这基本上是猴子补丁,而你做你的呼吁,做一些事情,然后撤销猴子补丁。这样,其他调用它的模块仍然可以获得原始行为;只有您才能获得修改后的行为。(感谢您指出mock!)@CorleyBrigman这只适用于同一流程中的其他模块。对我来说“其他脚本”听起来它们将是独立的Python进程,不会受到简单的修补程序的影响。也许您应该包括对旧的
\uuuu init\uuuu
的调用。似乎继承自
SomeClass
,替换该类将比使用
\uuu init\uuuu
函数更优雅他们自己。@JonathonReinhart你可能是对的,但我不认为OP真的想在自己的代码中将42替换为43。他特别问了关于monkey patching的问题,想知道如果我想让我的新类定义扩展旧类定义(通过继承),那该怎么办?为什么不从
SomeClass
继承呢?