@contextlib.contextmanager(with_context=True) - passing a context to contextmanager

contextlib.contextmanager in Python 3 added the ability to define functions which work both as decorators and context managers, which is great and one of the features I appreciate the most about Python 3 (seriously). I am facing a use case where I find contextlib.contextmanager has some limitations though. I have a decorator which looks like this: def wrap_exceptions(fun): """Decorator which translates bare OSError and IOError exceptions into NoSuchProcess and AccessDenied. """ @functools.wraps(fun) def wrapper(self, *args, **kwargs): # here "self" is the Process class instance try: return fun(self, *args, **kwargs) except EnvironmentError as err: if err.errno in (errno.ENOENT, errno.ESRCH): raise NoSuchProcess(self.pid, self._name) if err.errno in (errno.EPERM, errno.EACCES): raise AccessDenied(self.pid, self._name) raise return wrapper ...and I use it like this: class Process: @wrap_exceptions def exe(): ... The two key things about this decorator are: - it's designed to be used with class methods - it has a reference to the method's class instance (self) I would like to push this a bit further and make wrap_exceptions() work also a contextmanager, which is what contextlib.contextmanager should allow me to do in an easy way. As contextlib.contextmanager stands right it won't allow my use case though as there's no way to pass a reference of the class instance (Process) to wrap_exceptions. So here is my proposal: what if we add a new "with_context" argument to contextlib.contextmanager? The resulting code would look like this: @contextlib.contextmanager(with_context=True) def wrap_exceptions(ctx): # ctx is the Process class instance try: yield except EnvironmentError as err: pid = ctx.pid name = ctx.name if err.errno in (errno.ENOENT, errno.ESRCH): raise NoSuchProcess(pid, name) if err.errno in (errno.EPERM, errno.EACCES): raise AccessDenied(pid, name) raise class Process: @wrap_exceptions() def exe(): ... class Process: def exe(self): with wrap_exceptions(self): ... It must be noted that: - when with_context=True and wrap_exceptions is used as a decorator it can only be used to decorate class methods and not regular functions - when used as a decorator "self" is automatically passed as the first argument for wrap_exceptions I'm not sure if this is actually possible as I haven't gone through contextlib.contextmanager in details (it's quite magical). Thoughts? -- Giampaolo - http://grodola.blogspot.com

On 10 July 2015 at 07:34, Giampaolo Rodola' <g.rodola@gmail.com> wrote:
There isn't anything in this syntax which says to me "designed to be used as a class method decorator", nor in the invocation that says "this CM gets access to the class or instance object" :) The mechanism underlying the context-manager-or-decorator behaviour is actually https://docs.python.org/3/library/contextlib.html#contextlib.ContextDecorato..., and that's entirely unaware of both the function being decorated *and* its runtime arguments. This means it is unaware of the details of method invocations as well. If you want a method aware context manager, you'll likely want to write a decorator factory that accepts the relevant CM as a parameter. For example (untested): def with_cm(cm): def decorator(f): @functools.wraps(f) def wrapper(self, *args, **kwargs): with cm(self): return f(self, *args, **kwargs) Used as: class Process: @with_cm(wrap_exceptions) def exe(): ... Regardless, I'd advise against trying to hide the fact that there's an extra step going on in order to make the function being wrapped and/or one or more of its arguments available to the context manager, as that doesn't happen by default. Cheers, Nick. -- Nick Coghlan | ncoghlan@gmail.com | Brisbane, Australia

On 10 July 2015 at 07:34, Giampaolo Rodola' <g.rodola@gmail.com> wrote:
There isn't anything in this syntax which says to me "designed to be used as a class method decorator", nor in the invocation that says "this CM gets access to the class or instance object" :) The mechanism underlying the context-manager-or-decorator behaviour is actually https://docs.python.org/3/library/contextlib.html#contextlib.ContextDecorato..., and that's entirely unaware of both the function being decorated *and* its runtime arguments. This means it is unaware of the details of method invocations as well. If you want a method aware context manager, you'll likely want to write a decorator factory that accepts the relevant CM as a parameter. For example (untested): def with_cm(cm): def decorator(f): @functools.wraps(f) def wrapper(self, *args, **kwargs): with cm(self): return f(self, *args, **kwargs) Used as: class Process: @with_cm(wrap_exceptions) def exe(): ... Regardless, I'd advise against trying to hide the fact that there's an extra step going on in order to make the function being wrapped and/or one or more of its arguments available to the context manager, as that doesn't happen by default. Cheers, Nick. -- Nick Coghlan | ncoghlan@gmail.com | Brisbane, Australia
participants (2)
-
Giampaolo Rodola'
-
Nick Coghlan