क्या मैं किसी फ़ंक्शन को लपेटने से पहले पायथन डेकोरेटर को पैच कर सकता हूं?


83

मेरे पास एक सज्जाकार के साथ एक फ़ंक्शन है जिसे मैं पायथन मॉक लाइब्रेरी की मदद से परीक्षण करने की कोशिश कर रहा हूं । मैं mock.patchअसली डेकोरेटर को मॉक 'बाईपास' डेकोरेटर से बदलना चाहता हूं, जो सिर्फ फ़ंक्शन को कॉल करता है।

मैं यह पता नहीं लगा सकता कि वास्तविक डेकोरेटर फ़ंक्शन को लपेटने से पहले पैच को कैसे लागू किया जाए। मैंने पैच लक्ष्य पर कुछ अलग बदलावों की कोशिश की है और पैच को पुन: व्यवस्थित करने और बयानों को आयात करने के लिए, लेकिन सफलता के बिना। कोई विचार?

जवाबों:


59

डेकोरेटर्स को फंक्शन डेफिनिशन टाइम पर लगाया जाता है। अधिकांश कार्यों के लिए, यह तब होता है जब मॉड्यूल लोड किया जाता है। (कार्य जो अन्य कार्यों में परिभाषित किए गए हैं, डेकोरेटर को हर बार लागू किया जाता है जिसे एन्क्लोजिंग फ़ंक्शन कहा जाता है।)

तो अगर आप एक डेकोरेटर को बंदर-पैच करना चाहते हैं, तो आपको क्या करना है:

  1. इसमें शामिल मॉड्यूल को आयात करें
  2. मॉक डेकोरेटर फंक्शन को परिभाषित करें
  3. उदाहरण के लिए सेट करें module.decorator = mymockdecorator
  4. मॉड्यूल (ओं) को आयात करें जो डेकोरेटर का उपयोग करते हैं, या इसे अपने मॉड्यूल में उपयोग करते हैं

यदि मॉड्यूल में डेकोरेटर होता है, तो ऐसे फ़ंक्शन भी होते हैं जो इसका उपयोग करते हैं, जिन्हें पहले से ही आप उन्हें देख सकते हैं, और संभवतः एसओएल द्वारा सजाए गए हैं।

जब से मैंने मूल रूप से यह लिखा है तब से पायथन में परिवर्तन को प्रतिबिंबित करने के लिए संपादित करें: यदि डेकोरेटर उपयोग करता है functools.wraps()और पायथन का संस्करण काफी नया है, तो आप __wrapped__विशेषता का उपयोग करके मूल फ़ंक्शन को खोद सकते हैं और इसे फिर से सजा सकते हैं, लेकिन यह किसी भी तरह से नहीं है गारंटीकृत, और आप जिस डेकोरेटर को बदलना चाहते हैं, वह एकमात्र डेकोरेटर नहीं हो सकता है।


17
निम्नलिखित ने मेरा काफी समय बर्बाद किया: ध्यान रखें कि पायथन केवल एक बार मॉड्यूल आयात करता है। यदि आप परीक्षणों का एक सूट चला रहे हैं, तो अपने एक परीक्षण में डेकोरेटर का मजाक बनाने की कोशिश कर रहे हैं, और सजाए गए फ़ंक्शन को कहीं और आयात किया जाता है, डेकोरेटर का मजाक उड़ाने से कोई प्रभाव नहीं पड़ेगा।
पराग

2
reloadअजगर बाइनरी कोड docs.python.org/2/library/functions.html#reload और अपने डेकोरेटर को पकड़ने के लिए अंतर्निहित फ़ंक्शन का उपयोग करें
IxDay

3
@Paragon द्वारा रिपोर्ट किए गए मुद्दे में भाग गया और परीक्षण निर्देशिका में मेरे डेकोरेटर को पैच करके इसके चारों ओर काम किया __init__। यह सुनिश्चित किया कि पैच किसी भी परीक्षण फ़ाइल से पहले लोड किया गया था। हमारे पास एक पृथक परीक्षण फ़ोल्डर है, इसलिए रणनीति हमारे लिए काम करती है, लेकिन यह हर फ़ोल्डर लेआउट के लिए काम नहीं कर सकता है।
क्लेडटॉन्ड

4
इसे कई बार पढ़ने के बाद, मैं अभी भी उलझन में हूं। यह एक कोड उदाहरण की जरूरत है!
अनुष्ठान

@claytond धन्यवाद आपके समाधान ने मेरे लिए काम किया क्योंकि मेरे पास एक अलग परीक्षण फ़ोल्डर था!
श्रीवत्स

56

यह ध्यान दिया जाना चाहिए कि यहां कई उत्तर एकल परीक्षण उदाहरण के बजाय पूरे परीक्षण सत्र के लिए डेकोरेटर को पैच करेंगे; जो अवांछनीय हो सकता है। यहां एक डेकोरेटर को पैच करना है जो केवल एक ही परीक्षण के माध्यम से बनी रहती है।

हमारी इकाई को अनछुए डेकोरेटर के साथ परीक्षण किया जाना है:

# app/uut.py

from app.decorators import func_decor

@func_decor
def unit_to_be_tested():
    # Do stuff
    pass

डेकोरेटर्स मॉड्यूल से:

# app/decorators.py

def func_decor(func):
    def inner(*args, **kwargs):
        print "Do stuff we don't want in our test"
        return func(*args, **kwargs)
    return inner

जब तक हमारा परीक्षण एक परीक्षण चलाने के दौरान एकत्र हो जाता है, तब तक अवांछनीय डेकोरेटर को परीक्षण के तहत हमारी इकाई में पहले ही लागू कर दिया गया है (क्योंकि आयात समय पर ऐसा होता है)। उस से छुटकारा पाने के लिए, हमें डेकोरेटर को मैन्युअल रूप से डेकोरेटर के मॉड्यूल में बदलने की आवश्यकता होगी और फिर उसके यूयूटी वाले मॉड्यूल को फिर से आयात करना होगा।

हमारे परीक्षण मॉड्यूल:

#  test_uut.py

from unittest import TestCase
from app import uut  # Module with our thing to test
from app import decorators  # Module with the decorator we need to replace
import imp  # Library to help us reload our UUT module
from mock import patch


class TestUUT(TestCase):
    def setUp(self):
        # Do cleanup first so it is ready if an exception is raised
        def kill_patches():  # Create a cleanup callback that undoes our patches
            patch.stopall()  # Stops all patches started with start()
            imp.reload(uut)  # Reload our UUT module which restores the original decorator
        self.addCleanup(kill_patches)  # We want to make sure this is run so we do this in addCleanup instead of tearDown

        # Now patch the decorator where the decorator is being imported from
        patch('app.decorators.func_decor', lambda x: x).start()  # The lambda makes our decorator into a pass-thru. Also, don't forget to call start()          
        # HINT: if you're patching a decor with params use something like:
        # lambda *x, **y: lambda f: f
        imp.reload(uut)  # Reloads the uut.py module which applies our patched decorator

क्लीनअप कॉलबैक, किल_पैचेस, मूल डेकोरेटर को पुनर्स्थापित करता है और इसे उस इकाई पर फिर से लागू करता है जिसे हम परीक्षण कर रहे थे। इस तरह, हमारा पैच पूरे सत्र के बजाय केवल एक ही परीक्षण के माध्यम से बना रहता है - जो कि किसी अन्य पैच को कैसे व्यवहार करना चाहिए। इसके अलावा, चूंकि क्लीन अप पैच.स्टॉपॉल () को कॉल करता है, हम सेटअप () में किसी अन्य पैच को शुरू कर सकते हैं, जिसकी हमें आवश्यकता है और वे एक ही स्थान पर सभी को साफ कर देंगे।

इस पद्धति के बारे में समझने के लिए महत्वपूर्ण बात यह है कि रीलोडिंग चीजों को कैसे प्रभावित करेगा। यदि कोई मॉड्यूल बहुत लंबा है या तर्क है जो आयात पर चलता है, तो आपको इकाई के भाग के रूप में डेकोरेटर को सिकोड़ने और परीक्षण करने की आवश्यकता हो सकती है। :( उम्मीद है कि आपका कोड इससे बेहतर लिखा गया है।

यदि कोई परवाह नहीं करता है कि पैच पूरे परीक्षण सत्र में लागू होता है , तो ऐसा करने का सबसे आसान तरीका परीक्षण फ़ाइल के शीर्ष पर है:

# test_uut.py

from mock import patch
patch('app.decorators.func_decor', lambda x: x).start()  # MUST BE BEFORE THE UUT GETS IMPORTED ANYWHERE!

from app import uut

UUT के स्थानीय दायरे के बजाय डेकोरेटर के साथ फ़ाइल को पैच करना सुनिश्चित करें और डेकोरेटर के साथ यूनिट को आयात करने से पहले पैच को शुरू करें।

दिलचस्प है, भले ही पैच रोक दिया गया हो, पहले से ही आयात की गई सभी फाइलें अभी भी पैच को डेकोरेटर पर लागू होंगी, जो उस स्थिति के विपरीत है जो हमने शुरू किया था। विदित हो कि यह विधि परीक्षण रन में किसी अन्य फाइल को पैच करेगी जो बाद में आयात की जाती है - भले ही वे खुद को पैच घोषित न करें।


1
user2859458, इससे मुझे काफी मदद मिली। स्वीकृत उत्तर अच्छा है, लेकिन इसने मेरे लिए चीजों को सार्थक तरीके से पेश किया, और कई उपयोग के मामलों को शामिल किया जहां आप कुछ अलग करना चाहते हैं।
मैल्कम जोन्स

1
इस प्रतिक्रिया के लिए धन्यवाद! यदि यह दूसरों के लिए उपयोगी है, तो मैंने पैच का एक विस्तार किया, जो अभी भी एक संदर्भ प्रबंधक के रूप में काम करेगा और आपके लिए पुनः लोडिंग करेगा
Geekfish

13

जब मैं पहली बार इस समस्या को लेकर भागा, तो मैं अपने दिमाग को घंटों तक रैक करने के लिए इस्तेमाल करता हूं। मुझे इसे संभालने का बहुत आसान तरीका मिला।

यह डेकोरेटर को पूरी तरह से बायपास कर देगा, जैसे कि लक्ष्य को पहले से सजाया नहीं गया था।

यह दो भागों में टूट गया है। मैं निम्नलिखित लेख पढ़ने का सुझाव देता हूं।

http://alexmarandon.com/articles/python_mock_gotchas/

दो गोत्र जिन्हें मैं चला रहा था:

1.) अपने फ़ंक्शन / मॉड्यूल के आयात से पहले डेकोरेटर को मॉक करें।

मॉड्यूल लोड होने के समय डेकोरेटर और फ़ंक्शंस परिभाषित किए जाते हैं। यदि आप आयात करने से पहले मजाक नहीं करते हैं, तो यह नकली की उपेक्षा करेगा। लोड होने के बाद, आपको एक अजीब mock.patch.object करना होगा, जो और भी निराशाजनक हो जाता है।

2.) सुनिश्चित करें कि आप डेकोरेटर को सही रास्ता दिखा रहे हैं।

याद रखें कि आप जिस डेकोरेटर का मजाक उड़ा रहे हैं, वह इस बात पर आधारित है कि आपका मॉड्यूल डेकोरेटर को कैसे लोड करता है, न कि यह कि कैसे आपका टेस्ट डेकोरेटर को लोड करता है। यही कारण है कि मैं हमेशा आयात के लिए पूर्ण पथ का उपयोग करने का सुझाव देता हूं। यह चीजों को परीक्षण के लिए बहुत आसान बनाता है।

कदम:

1.) नकली समारोह:

from functools import wraps

def mock_decorator(*args, **kwargs):
    def decorator(f):
        @wraps(f)
        def decorated_function(*args, **kwargs):
            return f(*args, **kwargs)
        return decorated_function
    return decorator

2.) सजावट करने वाले का मजाक उड़ाना:

2a।) पथ अंदर से।

with mock.patch('path.to.my.decorator', mock_decorator):
     from mymodule import myfunction

2 बी।) फ़ाइल के शीर्ष पर, या TestCase.setUp में पैच

mock.patch('path.to.my.decorator', mock_decorator).start()

इनमें से कोई भी तरीका आपको टेस्टकेस या इसके तरीके / परीक्षण मामलों में किसी भी समय अपने फ़ंक्शन को आयात करने की अनुमति देगा।

from mymodule import myfunction

2.) mock.patch के साइड इफेक्ट के रूप में एक अलग फ़ंक्शन का उपयोग करें।

अब आप प्रत्येक डेकोरेटर के लिए mock_decorator का उपयोग कर सकते हैं जिसे आप मॉक करना चाहते हैं। आपको प्रत्येक डेकोरेटर को अलग से मॉक करना होगा, इसलिए जो आप याद करते हैं, उसे देखें।


1
आपके द्वारा उद्धृत ब्लॉग पोस्ट ने मुझे इसे बेहतर समझने में मदद की!
अनुष्ठान

2

निम्नलिखित ने मेरे लिए काम किया:

  1. परीक्षण लक्ष्य को लोड करने वाले आयात विवरण को हटा दें।
  2. परीक्षण स्टार्टअप पर डेकोरेटर को ऊपर से लागू करें।
  3. परीक्षण लक्ष्य को लोड करने के लिए पैचिंग के तुरंत बाद importlib.import_module () लागू करें।
  4. परीक्षण सामान्य रूप से चलाएं।

इसने एक जादू की तरह काम किया।


1

हमने एक डेकोरेटर का मजाक उड़ाने की कोशिश की जिसे कभी-कभी एक स्ट्रिंग की तरह एक और पैरामीटर मिल जाता है, और कुछ समय नहीं, उदाहरण के लिए:

@myDecorator('my-str')
def function()

OR

@myDecorator
def function()

उपरोक्त उत्तरों में से एक के लिए धन्यवाद, हमने एक नकली फ़ंक्शन लिखा और इस मॉक फ़ंक्शन के साथ डेकोरेटर को पैच करें:

from mock import patch

def mock_decorator(f):

    def decorated_function(g):
        return g

    if callable(f): # if no other parameter, just return the decorated function
        return decorated_function(f)
    return decorated_function # if there is a parametr (eg. string), ignore it and return the decorated function

patch('path.to.myDecorator', mock_decorator).start()

from mymodule import myfunction

ध्यान दें कि यह उदाहरण एक डेकोरेटर के लिए अच्छा है जो सजाए गए फ़ंक्शन को नहीं चलाता है, केवल वास्तविक रन से पहले कुछ सामान करते हैं। मामले में डेकोरेटर सजाए गए फ़ंक्शन को भी चलाता है, और इसलिए इसे फ़ंक्शन के मापदंडों को स्थानांतरित करने की आवश्यकता है, mock_decorator फ़ंक्शन को थोड़ा अलग होना चाहिए।

आशा है कि यह दूसरों की मदद करेगा ...


0

हो सकता है कि आप अपने सभी डेकोरेटर की परिभाषा पर एक और डेकोरेटर लगा सकते हैं जो मूल रूप से कुछ कॉन्फ़िगरेशन वेरिएबल की जांच करता है कि क्या परीक्षण मोड का उपयोग किया जाना है।
यदि हां, तो यह डेकोरेटर को बदल देता है, यह एक डमी डेकोरेटर के साथ सजा रहा है जो कुछ भी नहीं करता है।
अन्यथा, यह इस डेकोरेटर के माध्यम से देता है।


0

संकल्पना

यह थोड़ा अजीब लग सकता है sys.path, लेकिन कोई स्वयं की प्रतिलिपि के साथ पैच कर सकता है, और परीक्षण फ़ंक्शन के दायरे में आयात कर सकता है। निम्नलिखित कोड अवधारणा को दर्शाता है।

from unittest.mock import patch
import sys

@patch('sys.modules', sys.modules.copy())
def testImport():
 oldkeys = set(sys.modules.keys())
 import MODULE
 newkeys = set(sys.modules.keys())
 print((newkeys)-(oldkeys))

oldkeys = set(sys.modules.keys())
testImport()                       -> ("MODULE") # Set contains MODULE
newkeys = set(sys.modules.keys())
print((newkeys)-(oldkeys))         -> set()      # An empty set

MODULEतब आपके द्वारा परीक्षण किए जा रहे मॉड्यूल के साथ प्रतिस्थापित किया जा सकता है। (यह उदाहरण के साथ MODULEप्रतिस्थापित पायथन 3.6 में काम करता है xml)

सेशन

आपके मामले के लिए, मॉड्यूल में डेकोरेटर समारोह बसता था मान लीजिए prettyऔर में सजाया समारोह बसता था present, तो आप पैच हैं pretty.decoratorनकली मशीनरी और विकल्प का उपयोग कर MODULEके साथ present। निम्नलिखित की तरह कुछ काम करना चाहिए (अनटाइटेड)।

वर्ग TestDecorator (unittest.TestCase): ...

  @patch(`pretty.decorator`, decorator)
  @patch(`sys.path`, sys.path.copy())
  def testFunction(self, decorator) :
   import present
   ...

व्याख्या

परीक्षण मॉड्यूल sys.pathकी वर्तमान sys.pathकी एक प्रति का उपयोग करके, प्रत्येक परीक्षण फ़ंक्शन के लिए "क्लीन" प्रदान करके यह काम करता है । यह प्रतिलिपि तब बनाई जाती है जब मॉड्यूल को पहले sys.pathसभी परीक्षणों के लिए एक सुसंगत सुनिश्चित किया जाता है ।

बारीकियों

हालांकि, कुछ निहितार्थ हैं। यदि परीक्षण ढाँचा एक ही पायथन सत्र के तहत कई परीक्षण मॉड्यूल चलाता है तो कोई भी परीक्षण मॉड्यूल जो MODULEवैश्विक स्तर पर आयात करता है वह किसी भी परीक्षण मॉड्यूल को तोड़ता है जो इसे स्थानीय स्तर पर आयात करता है। यह हर जगह स्थानीय स्तर पर आयात करने के लिए मजबूर करता है। यदि फ्रेमवर्क प्रत्येक परीक्षण मॉड्यूल को एक अलग अजगर सत्र के तहत चलाता है तो यह काम करना चाहिए। इसी प्रकार आप MODULEविश्व स्तर पर एक परीक्षण मॉड्यूल में आयात नहीं कर सकते हैं जहां आप MODULEस्थानीय स्तर पर आयात कर रहे हैं ।

एक उपवर्ग के भीतर प्रत्येक परीक्षण समारोह के लिए स्थानीय आयात किया जाना चाहिए unittest.TestCaseunittest.TestCaseवर्ग के भीतर सभी परीक्षण कार्यों के लिए उपलब्ध मॉड्यूल का एक विशेष आयात सीधे उपवर्ग में इसे लागू करना संभव है ।

बिल्ट इन

उन लोगों के साथ खिलवाड़ builtinके आयात की जगह मिलेगा MODULEसाथ sys, osके बाद से इन पर alread कर रहे हैं, आदि असफल हो जायेगी sys.pathजब आप इसे कॉपी करने के लिए प्रयास करें। यहाँ बनाया गया है कि निष्क्रिय किए गए आयातों के साथ अजगर को आमंत्रित python -X test.pyकरना है , मुझे लगता है कि यह कर देगा लेकिन मैं उचित ध्वज (देखें python --help) को भूल गया । इन्हें बाद में स्थानीय रूप से import builtinsIIRC का उपयोग करके आयात किया जा सकता है ।


0

डेकोरेटर को पैच करने के लिए, आपको मॉड्यूल को आयात करने या फिर से लोड करने की आवश्यकता होती है जो उस डेकोरेटर को पैच करने के बाद उपयोग करता है या पूरी तरह से उस डेकोरेटर के मॉड्यूल के संदर्भ को फिर से परिभाषित करता है।

डेकोरेटर्स को उस समय लागू किया जाता है जब एक मॉड्यूल आयात किया जाता है। यही कारण है कि यदि आप एक मॉड्यूल आयात करते हैं जो एक डेकोरेटर का उपयोग करता है जिसे आप अपनी फ़ाइल के शीर्ष पर पैच करना चाहते हैं और बाद में इसे फिर से लोड किए बिना इसे पैच करने का प्रयास करते हैं, तो पैच का कोई प्रभाव नहीं होगा।

यहाँ ऐसा करने के पहले तरीके का एक उदाहरण दिया गया है - एक डैकोरेटर को पैच के बाद फिर से लोड करना जो इसका उपयोग करता है:

import moduleA
...

  # 1. patch the decorator
  @patch('decoratorWhichIsUsedInModuleA', examplePatchValue)
  def setUp(self)
    # 2. reload the module which uses the decorator
    reload(moduleA)

  def testFunctionA(self):
    # 3. tests...
    assert(moduleA.functionA()...

उपयोगी संदर्भ:


-2

@lru_cache (max_size = 1000) के लिए


class MockedLruCache(object):

def __init__(self, maxsize=0, timeout=0):
    pass

def __call__(self, func):
    return func

cache.LruCache = MockedLruCache

यदि आप डेकोरेटर का उपयोग करते हैं, जो परम नहीं है, तो आपको निम्न करना चाहिए:

def MockAuthenticated(func):
    return func

from tornado import web web.authenticated = MockAuthenticated


1
मुझे इस उत्तर में बहुत सारे मुद्दे दिखाई देते हैं। पहला (और बड़ा एक) यह है कि यदि आप अभी तक सजाया गया है (तो यह ओपी मुद्दा है) मूल फ़ंक्शन तक आपकी पहुंच नहीं हो सकती। इसके अलावा आप परीक्षण किए जाने के बाद पैच को नहीं हटाते हैं और जब आप इसे टेस्ट सूट में चलाते हैं तो समस्या हो सकती है।
मिशेल डी'मिको
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.