pythondecoratorinstance-methods

How to decorate the __enter__ and __exit__ instance methods?


I would like to use the decorator deco to reshape the __enter__ and __exit__ instance methods. The code runs but the wrapper is not executed in the with section. Find the actual output below, followed by expected output and finally the code.

Current output:

In __init__
-------------------
In __enter__
-------------------
In wrapper
Blah, blah, blah
-------------------
In __exit__

Expected output:

In __init__
-------------------
In wrapper
In __enter__
-------------------
In wrapper
Blah, blah, blah
-------------------
In wrapper
In __exit__
class contMgr():
    def __init__(self):
        print("In __init__")
        pass

    def __enter__(self):
        print("In __enter__")
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        print("In __exit__")
        pass

    def __call__(self, *args, **kwargs):
        print("In __call_")

    def brol():
        print("brol")

def deco(func):
    def wrapper(*args,**kwargs):
        print("In wrapper")
        result = func(*args, **kwargs)
        return result
    
    return wrapper

@deco
def test():
    print("Blah, blah, blah")

mgr = contMgr()
mgr.__enter__ = deco(mgr.__enter__)
mgr.__exit__ = deco(mgr.__exit__)
print("------------------------")
with mgr:
    print("------------------------")
    test()
    print("------------------------")

Solution

  • The short answer

    As others have pointed out, the problem is that it's the __enter__ and __exit__ methods of the class that will get called, not those of the instance.

    But you said this is for testing purposes, and you don't want the decorators to be there permanently, so the solution is to alter the class methods temporarily.

    You can do it manually, of course: change the function on the class, run the test, and change it back, but unittest.mock.patch can automate that for you.

    Using patch

    Here's how you can use patch as a context manager which will restore the original function once you leave the context manager's scope:

    with (patch("__main__.contMgr.__enter__", deco(contMgr.__enter__)),
          patch("__main__.contMgr.__exit__", deco(contMgr.__exit__))):
        with mgr:
            test()
    

    will call the decorated enter and exit only inside this with statement.

    Here's a longer example with output:

    print("Using patch")
    with (patch("__main__.contMgr.__enter__", deco(contMgr.__enter__)),
          patch("__main__.contMgr.__exit__", deco(contMgr.__exit__))):
        with mgr:
            test()
    
    print("\nNot using patch")
    with mgr:
        test()
    

    Output:

    Using patch
    In wrapper
    In __enter__
    In wrapper
    Blah, blah, blah
    In wrapper
    In __exit__
    
    Not using patch
    In __enter__
    In wrapper
    Blah, blah, blah
    In __exit__
    

    Documentation

    https://docs.python.org/3/library/unittest.mock.html#patch