1

I have a helper class Decontext that I am using to turn a context manager into a decorator (pyton 2.6).

class Decontext(object):
    """
    makes a context manager also act as decorator
    """

    def __init__(self, context_manager):
        self._cm = context_manager

    def __enter__(self):
        return self._cm.__enter__()

    def __exit__(self, *args, **kwds):
        return self._cm.__exit__(*args, **kwds)

    def __call__(self, func):
        def wrapper(*args, **kwds):
            with self:
                return func(*args, **kwds)

        return wrapper

My contextmanager takes an argument and I am trying to figure out how to pass that argument when using this decorator ?

@contextmanager
def sample_t(arg1):
print "<%s>" % arg1
        yield

This is how I am using it which fails:

my_deco =  Decontext(sample_t)

@my_deco(arg1='example')
def some_func()
     print 'works'

EDIT:

I would like the Decontext class to pass all *args in the context_manager when the __call__ function executes.

Example:

decorator_example = Decontext(sample_t) // I don't want to pass in the argument here but when the decorator is created. How can I modify my class to make this enhancement

Edit 2:

Example of what I expected

my_deco =  Decontext(sample_t)

@my_deco(arg1='example')
def some_func()
     print 'works'

Expected output:

'example' // running and passing argument to context_manager
'works' // after yield executing some_func 
Warz
  • 7,386
  • 14
  • 68
  • 120
  • Can you give an example of how you want to use this? It's not clear to me what your class is supposed to do. – BrenBarn Aug 07 '14 at 19:59
  • It almost sounds like you just want this: https://docs.python.org/2/library/contextlib.html#module-contextlib – Gerrat Aug 07 '14 at 20:05
  • @Gerrat - If i define my context manager and just try to use it as a decorator without converting into one, i get an error `object is not callable` – Warz Aug 07 '14 at 20:09
  • Maybe you could show both your example function (eg your `some_func`), as well as exactly how you'd call this & what output you'd like. It's really unclear exactly what you're hoping for. – Gerrat Aug 07 '14 at 20:10
  • @Gerrat, not to sound too confusing and my poor understanding of decorators and contextmanagers but I am looking for a way to define a context manager that takes an argument and I understand I can just use it by the `with` statement but would like to make it a decorator instead if possible, preserving the ability to pass arguments – Warz Aug 07 '14 at 20:17

3 Answers3

2

The issue you're having is that the _cm attribute you're setting up in your __init__ method isn't actually storing a context manager instance, but rather the the type of the context manager (or possibly a factory function that produces context manager instances). You need to call the type or factory later, to get an instance.

Try this, which should work for both context manager instances (assuming they're not also callable) and context manager types that require arguments:

class Decontext(object):
    """
    makes a context manager also act as decorator
    """

    def __init__(self, context_manager):
        self._cm = context_manager   # this may be a cm factory or type, but that's OK

    def __enter__(self):
        return self._cm.__enter__()

    def __exit__(self, *args, **kwds):
        return self._cm.__exit__(*args, **kwds)

    def __call__(self, *cm_args, **cm_kwargs):
        try:
            self._cm = self._cm(*cm_args, **cm_kwargs) # try calling the cm like a type
        except TypeError:
            pass
        def decorator(func):
            def wrapper(*args, **kwds):
                with self:
                    return func(*args, **kwds)

            return wrapper
        return decorator

There's a fairly silly level of nesting going on in there, but it's what you need given the way you want to call the thing. Here's an example with it in action:

from contextlib import contextmanager

@contextmanager
def foo(arg):
    print("entered foo({!r})".format(arg))
    yield
    print("exited foo({!r})".format(arg))

foo_deco_factory = Decontext(foo)

@foo_deco_factory("bar")
def baz(arg):
    print("called baz({!r})".format(arg))

baz("quux")

It will output:

entered foo("bar")
called baz("quux")
exited foo("bar")

Note that trying to use foo_deco_factory as a context manager will not work (similarly to how using with foo won't work). Using the context manager protocol on a Decontext instance will work only if it was initialized with a context manager instance (rather than a type or factory) or if it has been called already as a decorator with appropriate arguments.

If you didn't need the decorator to be able to act as a context manager itself, you could pretty easily turn the whole class into a function (with __call__ becoming an extra level of closure, rather than a method):

def decontext(cm_factory):
    def factory(*cm_args, **cm_kwargs):
        cm = cm_factory(*cm_args, **cm_kwargs)
        def decorator(func):
            def wrapper(*args, **kwargs):
                with cm:
                    return func(*args, **kwargs)
            return wrapper
        return decorator
    return factory

To simplify the code in this case, I always assume you're passing in a context manager factory, rather than a context manager instance.

Blckknght
  • 100,903
  • 11
  • 120
  • 169
  • Like the 2nd example & I think that's exactly what he's asked for (sounded like his helper had a sole purpose of turning his parameter-taking context manager into a decorator). Not that this is code golf, but was their a particular reason for instantiating the context manager first, then using it, versus just: `with cm_factory(*cm_args, **cm_kwargs):`? – Gerrat Aug 07 '14 at 21:18
  • @Gerrat: Hmm, I suppose there isn't a true *need* to create the context manager ahead of time. But one advantage of doing it this way though is that any exception (due to invalid `cm_args`, perhaps) will appear early on when the factory is called, not only later on when the wrapped function is called. – Blckknght Aug 07 '14 at 21:44
1

I think the following is what you're looking for. The main key is that you can't pass the context manager argument you want directly to the __call__ method on your Decontext class, so we use a helper function to do that. There's likely a way to simplify this ( but I'll leave that as an exercise to the reader :) )

from contextlib import contextmanager
from functools import partial

class _Decontext(object):
    """
    makes a context manager also act as decorator
    """

    def __init__(self, cm, *args, **kwargs):
        self._cm = cm
        self.args = args
        self.kwargs = kwargs

    def __call__(self, func):

        def wrapper(*args, **kwds):    
            with self._cm(*self.args, **self.kwargs):
                func(*args, **kwds)

        return wrapper

# helper for our helper :)
def decontext(cm=None):
    def wrapper(cm, *args, **kwargs):
        return _Decontext(cm, *args, **kwargs)
    return partial(wrapper, cm)


@contextmanager
def sample_t(arg1):
    print "<%s>" % arg1
    yield 

my_deco = decontext(sample_t)

@my_deco(arg1='example')
def some_func():
     print 'works'

if __name__ == '__main__':    
    some_func()

This outputs:

<example>
works
Gerrat
  • 28,863
  • 9
  • 73
  • 101
0

If I understand right, you should be doing:

@my_deco
def func(arg1):
   print "blah"

The decorator has to decorate something (like a function). However, there are some other problems with your class, but I'm not sure how to fix them, because it's a little hard to understand what it's supposed to do.

BrenBarn
  • 242,874
  • 37
  • 412
  • 384
  • I want to pass the argument to the contextmanager `sample_t` through the decorator if possible. – Warz Aug 07 '14 at 19:58
  • @Warz: Have you looked at for instance [this question](http://stackoverflow.com/questions/10176226/how-to-pass-extra-arguments-to-python-decorator) about how to write decorators that take arguments? – BrenBarn Aug 07 '14 at 20:03