python - قرد يرمي فئة في وحدة أخرى في بايثون




class unit-testing monkeypatching (6)

أنا أعمل مع وحدة كتبها شخص آخر. أود أن قرد التصحيح طريقة __init__ من فئة محددة في الوحدة النمطية. لقد افترضت كل الأمثلة التي اكتشفتها والتي توضح كيفية القيام بذلك ، أنني سأتصل بالطبقة بنفسي (على سبيل المثال ، فئة بيثون Monkey-patch ). ولكن هذا ليس هو الحال. في حالتي يتم تنشيط الفئة داخل دالة في وحدة نمطية أخرى. انظر المثال (المبسطة بشكل كبير) أدناه:

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__ من SomeClass بحيث عندما يتم استدعاء dosomething من mymodule.py ، على سبيل المثال ، يطبع 43 بدلاً من 42؟ من الناحية المثالية سأتمكن من لف الأسلوب الحالي.

لا يمكنني تغيير ملفات * .py thirdpartymodule * ، حيث تعتمد البرامج النصية الأخرى على الوظائف الموجودة. أنا أفضل عدم إنشاء نسخة خاصة بي من الوحدة ، حيث إن التغيير الذي أحتاج إلى إجرائه بسيط للغاية.

تحرير 2013-10-24

لقد تجاهلت تفاصيل صغيرة ولكنها مهمة في المثال أعلاه. يتم استيراد SomeClass بواسطة thirdpartymodule_b مثل هذا: from thirdpartymodule_a import SomeClass .

للقيام بالتصحيح المقترح من قبل FJ ، أحتاج إلى استبدال النسخة في thirdpartymodule_b ، بدلاً من thirdpartymodule_a . على سبيل المثال thirdpartymodule_b.SomeClass.__init__ = new_init .


Answers

هنا هو مثال جئت مع monkeypatch Popen باستخدام 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 لجمع البانورانات الوهمية:

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

يستخدم واحد فقط أقل قليلا من نسخة 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

عندما يكون الحارس غير صحيح ، فهو يعمل بالضبط كما كان من قبل. عندما يكون ذلك صحيحًا ، ستحصل على سلوكك الجديد. في شفرتك ، ستفعل:

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

ستستمر الشفرة الحالية في العمل - وستطلق عليها بلا حجج ، والتي ستحتفظ بالقيمة الافتراضية الخاطئة ، والتي ستحافظ على السلوك القديم. لكن التعليمات البرمجية الخاصة بك الآن طريقة لإخبار المكدس بأكمله لأسفل أن يتوفر سلوك جديد.


أحد الأساليب الأخرى الممكنة ، والتي تشبه إلى حد كبير wrapt أندرو كلارك ، هو استخدام المكتبة wrapt . من بين الأشياء المفيدة الأخرى ، توفر هذه المكتبة المساعدة patch_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 وهو ليس الديكور ولكن طب الأسنان يعمل بنفس الطريقة:

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

wrapt.wrap_function_wrapper(thirdpartymodule_a.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 القديم ، قم باستبدال تعريف new_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

بشكل عام ، يهدف اختبار الوحدة إلى ممارسة الواجهة العامة للفئة أو الوحدة. لذلك ، تعتبر الطرق الخاصة هي تفاصيل التنفيذ التي لا تتوقع اختبارها بشكل صريح.







python class unit-testing monkeypatching