Programing

함수를 래핑하기 전에 Python 데코레이터를 패치 할 수 있습니까?

lottogame 2020. 11. 25. 07:26
반응형

함수를 래핑하기 전에 Python 데코레이터를 패치 할 수 있습니까?


Python Mock 라이브러리 의 도움으로 테스트를 시도중인 데코레이터가있는 함수가 있습니다. mock.patch를 사용하여 실제 데코레이터를 함수를 호출하는 모의 '바이 패스'데코레이터로 대체하고 싶습니다. 내가 알아낼 수없는 것은 실제 데코레이터가 함수를 래핑하기 전에 패치를 적용하는 방법입니다. 패치 대상에 대해 몇 가지 다른 변형을 시도하고 패치 및 가져 오기 문을 재정렬했지만 성공하지 못했습니다. 어떤 아이디어?


데코레이터는 함수 정의 시간에 적용됩니다. 대부분의 기능에서 이것은 모듈이로드 될 때입니다. (다른 함수에 정의 된 함수는 둘러싸는 함수가 호출 될 때마다 적용되는 데코레이터가 있습니다.)

따라서 데코레이터에 원숭이 패치를 적용하려면 다음을 수행해야합니다.

  1. 포함 된 모듈 가져 오기
  2. 모의 데코레이터 함수 정의
  3. 예를 들어 설정 module.decorator = mymockdecorator
  4. 데코레이터를 사용하는 모듈을 가져 오거나 자체 모듈에서 사용합니다.

데코레이터가 포함 된 모듈에이를 사용하는 함수도 포함되어있는 경우 볼 수있을 때 이미 데코레이터가 장식되어 있으며 아마도 SOL 일 것입니다.

내가 원래 작성한 이후 Python에 대한 변경 사항을 반영하도록 편집하십시오. 데코레이터가 사용 functools.wraps()하고 Python 버전이 충분히 __wrapped__새롭다면 attritube를 사용하여 원래 함수를 파 내고 다시 장식 할 수 있지만 이것은 결코 아닙니다 그리고 교체하려는 데코레이터가 유일한 데코레이터가 아닐 수도 있습니다.


여기에있는 몇 가지 답변은 단일 테스트 인스턴스가 아닌 전체 테스트 세션에 대한 데코레이터를 패치한다는 점에 유의해야합니다. 바람직하지 않을 수 있습니다. 단일 테스트를 통해서만 지속되는 데코레이터를 패치하는 방법은 다음과 같습니다.

원하지 않는 데코레이터로 테스트 할 유닛 :

# 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

테스트 실행 중에 테스트가 수집 될 때까지 원하지 않는 데코레이터가 이미 테스트중인 단위에 적용되었습니다 (임포트시 발생하기 때문). 이를 제거하려면 데코레이터 모듈에서 데코레이터를 수동으로 교체 한 다음 UUT가 포함 된 모듈을 다시 가져와야합니다.

테스트 모듈 :

#  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

정리 콜백 인 kill_patches는 원래 데코레이터를 복원하고 테스트중인 유닛에 다시 적용합니다. 이렇게하면 패치가 전체 세션이 아닌 단일 테스트를 통해서만 지속됩니다. 이는 다른 패치가 정확히 작동하는 방식입니다. 또한 정리가 patch.stopall ()을 호출하기 때문에 필요한 setUp ()에서 다른 패치를 시작할 수 있으며 한곳에서 모두 정리됩니다.

이 방법에 대해 이해해야 할 중요한 것은 다시로드가 사물에 미치는 영향입니다. 모듈이 너무 오래 걸리거나 가져 오기시 실행되는 로직이있는 경우 단위의 일부로 데코레이터를 어깨를 으쓱하고 테스트해야 할 수도 있습니다. :( 당신의 코드가 그것보다 더 잘 작성 되었으면합니다. 맞죠?

패치가 전체 테스트 세션에 적용되는지 신경 쓰지 않는다면 가장 쉬운 방법은 테스트 파일의 맨 위에있는 것입니다.

# 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의 로컬 범위가 아닌 데코레이터로 파일을 패치하고 데코레이터로 유닛을 가져 오기 전에 패치를 시작해야합니다.

흥미롭게도 패치가 중지 되더라도 이미 가져온 모든 파일에는 데코레이터에 패치가 적용되어 있습니다. 이는 우리가 시작한 상황과 반대입니다. 이 방법은 패치 자체를 선언하지 않더라도 나중에 가져 오는 테스트 실행의 다른 모든 파일을 패치합니다.


이 문제를 처음 접했을 때 나는 몇 시간 동안 두뇌를 긁어 모으는 데 사용합니다. 나는 이것을 처리하는 훨씬 더 쉬운 방법을 찾았습니다.

이것은 대상이 처음에 장식되지 않은 것처럼 장식자를 완전히 우회합니다.

이것은 두 부분으로 나뉩니다. 다음 기사를 읽는 것이 좋습니다.

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

내가 계속 마주 친 두 가지 고차 :

1.) 함수 / 모듈을 가져 오기 전에 Decorator를 모의하십시오.

데코레이터와 함수는 모듈이로드 될 때 정의됩니다. 가져 오기 전에 모의하지 않으면 모의를 무시합니다. 로드 후에는 이상한 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

2b.) 파일 상단 또는 TestCase.setUp의 패치

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

이러한 방법 중 하나를 사용하면 TestCase 또는 해당 메서드 / 테스트 케이스 내에서 언제든지 함수를 가져올 수 있습니다.

from mymodule import myfunction

2.) mock.patch의 부작용으로 별도의 함수를 사용하십시오.

이제 모의하려는 각 데코레이터에 대해 mock_decorator를 사용할 수 있습니다. 각 데코레이터를 개별적으로 조롱해야하므로 놓친 데코레이터를 조심하세요.


다음은 나를 위해 일했습니다.

  1. 테스트 대상을로드하는 import 문을 제거하십시오.
  2. 위에 적용된대로 테스트 시작시 데코레이터를 패치합니다.
  3. 패치 후 즉시 importlib.import_module ()을 호출하여 테스트 대상을로드합니다.
  4. 정상적으로 테스트를 실행합니다.

그것은 매력처럼 작동했습니다.


Maybe you can apply another decorator onto the definitions of all your decorators that basically checks some configuration variable to see if testing mode is meant to be used.
If yes, it replaces the decorator it is decorating with a dummy decorator that does nothing.
Otherwise, it lets this decorator through.


Concept

This may sound a bit odd but one can patch sys.path, with a copy of itself, and perform an import within the scope of the test function. The following code shows the concept.

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 may then be substituted with the module you are testing. (This works in Python 3.6 with MODULE substituted with xml for example)

OP

For your case, let's say the decorator function resides in the module pretty and the decorated function resides in present, then you would patch pretty.decorator using the mock machinery and substitute MODULE with present. Something like the following should work (Untested).

class TestDecorator(unittest.TestCase) : ...

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

Explanation

This works by providing a "clean" sys.path for each test function, using a copy of the current sys.path of the test module. This copy is made when the module is first parsed ensuring a consistent sys.path for all the tests.

Nuances

There are a few implications, however. If the testing framework runs multiple test modules under the same python session any test module that imports MODULE globally breaks any test module that imports it locally. This forces one to perform the import locally everywhere. If the framework runs each test module under a separate python session then this should work. Similarly you may not import MODULE globally within a test module where you're importing MODULE locally.

The local imports must be done for each test function within a subclass of unittest.TestCase. It is perhaps possible to apply this to the unittest.TestCase subclass directly making a particular import of the module available for all of the test functions within the class.

Built Ins

Those messing with builtin imports will find replacing MODULE with sys, os etc. will fail, since these are alread on sys.path when you try to copy it. The trick here is to invoke Python with the builtin imports disabled, I think python -X test.py will do it but I forget the appropriate flag (See python --help). These may subsequently be imported locally using import builtins, IIRC.


To patch a decorator, you need to either import or reload the module which uses that decorator after patching it OR redefine the module's reference to that decorator altogether.

Decorators are applied at the time that a module is imported. This is why if you imported a module which uses a decorator you want to patch at the top of your file and attempt it to patch it later on without reloading it, the patch would have no effect.

Here is an example of the first way mentioned of doing this - reloading a module after patching a decorator it uses:

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()...

Helpful References:


for @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

if use decorator which haven't params, you should:

def MockAuthenticated(func):
    return func

from tornado import web web.authenticated = MockAuthenticated

참고URL : https://stackoverflow.com/questions/7667567/can-i-patch-a-python-decorator-before-it-wraps-a-function

반응형